From f25fbe4a9e2eab08424f48030d271681a37ca2ba Mon Sep 17 00:00:00 2001 From: Youssef Raafat Date: Tue, 25 May 2021 08:26:19 +0200 Subject: [PATCH 001/171] Fix Header Option Casting (#260) Co-authored-by: Ivan Terekhin --- chopper/lib/src/base.dart | 2 +- chopper_generator/lib/chopper_generator.dart | 2 +- chopper_generator/lib/src/generator.dart | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/chopper/lib/src/base.dart b/chopper/lib/src/base.dart index d936c669..ff4bb051 100644 --- a/chopper/lib/src/base.dart +++ b/chopper/lib/src/base.dart @@ -340,7 +340,7 @@ class ChopperClient { Map headers = const {}, Map parameters = const {}, String? baseUrl, - dynamic? body, + dynamic body, }) => send( Request( diff --git a/chopper_generator/lib/chopper_generator.dart b/chopper_generator/lib/chopper_generator.dart index e19b3343..2facc8b0 100644 --- a/chopper_generator/lib/chopper_generator.dart +++ b/chopper_generator/lib/chopper_generator.dart @@ -4,4 +4,4 @@ import 'package:build/build.dart'; import 'src/generator.dart'; Builder chopperGeneratorFactory(BuilderOptions options) => - chopperGeneratorFactoryBuilder(header: options.config['header'] as String); + chopperGeneratorFactoryBuilder(header: options.config['header']); diff --git a/chopper_generator/lib/src/generator.dart b/chopper_generator/lib/src/generator.dart index 1ad77a42..cadcfcbc 100644 --- a/chopper_generator/lib/src/generator.dart +++ b/chopper_generator/lib/src/generator.dart @@ -503,7 +503,7 @@ class ChopperGenerator extends GeneratorForAnnotation { } } -Builder chopperGeneratorFactoryBuilder({String header = ''}) => PartBuilder( +Builder chopperGeneratorFactoryBuilder({String? header}) => PartBuilder( [ChopperGenerator()], '.chopper.dart', header: header, From 3d4773562502a0f2996aa26d1dce157d5b30ea2f Mon Sep 17 00:00:00 2001 From: Ivan Terekhin Date: Mon, 31 May 2021 18:09:28 +0300 Subject: [PATCH 002/171] Fix for #259 (#263) --- chopper/lib/src/base.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/chopper/lib/src/base.dart b/chopper/lib/src/base.dart index ff4bb051..be1d720e 100644 --- a/chopper/lib/src/base.dart +++ b/chopper/lib/src/base.dart @@ -314,11 +314,11 @@ class ChopperClient { var updatedRequest = await authenticator!.authenticate(request, res); if (updatedRequest != null) { - res = await send(updatedRequest); + res = await send(updatedRequest); } } - if (_responseIsSuccessful(response.statusCode)) { + if (_responseIsSuccessful(res.statusCode)) { res = await _handleSuccessResponse( res, responseConverter, From 8be965b94a38ab60ad17af04c8263ddb16eb14c9 Mon Sep 17 00:00:00 2001 From: Ivan Terekhin Date: Tue, 1 Jun 2021 10:49:58 +0300 Subject: [PATCH 003/171] 4.0.1 fixes (#264) --- chopper/CHANGELOG.md | 15 +++++++++------ chopper/pubspec.yaml | 2 +- chopper_generator/CHANGELOG.md | 13 ++++++++----- chopper_generator/pubspec.yaml | 2 +- 4 files changed, 19 insertions(+), 13 deletions(-) diff --git a/chopper/CHANGELOG.md b/chopper/CHANGELOG.md index a3067275..6af68839 100644 --- a/chopper/CHANGELOG.md +++ b/chopper/CHANGELOG.md @@ -1,5 +1,8 @@ # Changelog +## 4.0.1 + +- Fix for the null safety support ## 4.0.0 - **Null safety support** @@ -52,7 +55,7 @@ New way to handle errors ## 2.4.2 -- Fix on JsonConverter +- Fix on JsonConverter If content type header overrided using @Post(headers: {'content-type': '...'}) The converter won't add json header and won't apply json.encode if content type is not JSON @@ -101,10 +104,10 @@ New way to handle errors ## 2.2.0 - Fix converter issue on List - - ***Breaking Change*** - on `Converter.convertResponse(response)`, + - ***Breaking Change*** + on `Converter.convertResponse(response)`, it take a new generic type => `Converter.convertResponse(response)` - + - deprecated `Chopper.service(Type)`, use `Chopper.getservice()` instead thanks to @MichaelDark @@ -139,12 +142,12 @@ thanks to @MichaelDark - ***BreakingChange*** Removed `name` parameter on `ChopperApi` New way to instanciate a service - + @ChopperApi() abstract class MyService extends ChopperService { static MyService create([ChopperClient client]) => _$MyService(client); } - + ## 1.0.0 diff --git a/chopper/pubspec.yaml b/chopper/pubspec.yaml index 18ebf7d0..70d13169 100644 --- a/chopper/pubspec.yaml +++ b/chopper/pubspec.yaml @@ -1,6 +1,6 @@ name: chopper description: Chopper is an http client generator using source_gen, inspired by Retrofit -version: 4.0.0 +version: 4.0.1 documentation: https://hadrien-lejard.gitbook.io/chopper repository: https://github.com/lejard-h/chopper author: Hadrien Lejard diff --git a/chopper_generator/CHANGELOG.md b/chopper_generator/CHANGELOG.md index 48198a10..c6c3e8aa 100644 --- a/chopper_generator/CHANGELOG.md +++ b/chopper_generator/CHANGELOG.md @@ -1,5 +1,8 @@ # Changelog +## 4.0.1 + +- Fix for the null safety support ## 4.0.0 - **Null safety support** @@ -39,7 +42,7 @@ ## 2.4.2 -- Fix on JsonConverter +- Fix on JsonConverter If content type header overrided using @Post(headers: {'content-type': '...'}) The converter won't add json header and won't apply json.encode if content type is not JSON @@ -59,7 +62,7 @@ ## 2.3.4 fix trailing slash when empty path - + ## 2.3.3 - update analyzer to `0.35.0` @@ -91,10 +94,10 @@ ## 2.2.0 - Fix converter issue on List - - ***Breaking Change*** - on `Converter.convertResponse(response)`, + - ***Breaking Change*** + on `Converter.convertResponse(response)`, it take a new generic type => `Converter.convertResponse(response)` - + - deprecated `Chopper.service(Type)`, use `Chopper.getservice()` instead thanks to @MichaelDark diff --git a/chopper_generator/pubspec.yaml b/chopper_generator/pubspec.yaml index 11ab6e4c..f399b818 100644 --- a/chopper_generator/pubspec.yaml +++ b/chopper_generator/pubspec.yaml @@ -1,6 +1,6 @@ name: chopper_generator description: Chopper is an http client generator using source_gen, inspired by Retrofit -version: 4.0.0 +version: 4.0.1 documentation: https://hadrien-lejard.gitbook.io/chopper repository: https://github.com/lejard-h/chopper author: Hadrien Lejard From bcb030489a4905d5c54c837e8a18149daefc3a9d Mon Sep 17 00:00:00 2001 From: luis901101 Date: Sat, 18 Sep 2021 02:16:37 -0400 Subject: [PATCH 004/171] analyzer dependency upgraded (#296) --- chopper_generator/pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chopper_generator/pubspec.yaml b/chopper_generator/pubspec.yaml index f399b818..c877e588 100644 --- a/chopper_generator/pubspec.yaml +++ b/chopper_generator/pubspec.yaml @@ -9,7 +9,7 @@ environment: sdk: ">=2.12.0 <3.0.0" dependencies: - analyzer: ^1.2.0 + analyzer: ^2.0.0 build: ^2.0.0 built_collection: ^5.0.0 chopper: ^4.0.0 From 4e4607e9c53012b3457542390af9b9886e0d1bc5 Mon Sep 17 00:00:00 2001 From: melvspace Date: Sat, 18 Sep 2021 09:28:55 +0300 Subject: [PATCH 005/171] fix(generator): fix PartValueFile value not nullable if arg is (#288) (#293) --- chopper_generator/lib/src/generator.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chopper_generator/lib/src/generator.dart b/chopper_generator/lib/src/generator.dart index cadcfcbc..40f722f6 100644 --- a/chopper_generator/lib/src/generator.dart +++ b/chopper_generator/lib/src/generator.dart @@ -456,7 +456,7 @@ class ChopperGenerator extends GeneratorForAnnotation { ]; list.add( - refer('PartValueFile<${p.type.getDisplayString(withNullability: false)}>') + refer('PartValueFile<${p.type.getDisplayString(withNullability: p.type.isNullable)}>') .newInstance(params), ); }); From 939a83f11154d21a976cba52c9ac27e3111b5de1 Mon Sep 17 00:00:00 2001 From: Ivan Terekhin Date: Thu, 23 Sep 2021 11:57:22 +0300 Subject: [PATCH 006/171] Chopper generator release 4.0.2 (#297) --- chopper_generator/CHANGELOG.md | 5 +++++ chopper_generator/pubspec.yaml | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/chopper_generator/CHANGELOG.md b/chopper_generator/CHANGELOG.md index c6c3e8aa..41173021 100644 --- a/chopper_generator/CHANGELOG.md +++ b/chopper_generator/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## 4.0.2 + +- Analyzer dependency upgrade +- PartValueFile nullability fix + ## 4.0.1 - Fix for the null safety support diff --git a/chopper_generator/pubspec.yaml b/chopper_generator/pubspec.yaml index c877e588..708c683c 100644 --- a/chopper_generator/pubspec.yaml +++ b/chopper_generator/pubspec.yaml @@ -1,6 +1,6 @@ name: chopper_generator description: Chopper is an http client generator using source_gen, inspired by Retrofit -version: 4.0.1 +version: 4.0.2 documentation: https://hadrien-lejard.gitbook.io/chopper repository: https://github.com/lejard-h/chopper author: Hadrien Lejard From ff0ec87317d969e402ff70321d3a7c3872561f8a Mon Sep 17 00:00:00 2001 From: melvspace Date: Thu, 23 Sep 2021 16:35:10 +0300 Subject: [PATCH 007/171] fix: fix this.body cast of null value when response body is null (#291) (#292) --- chopper/lib/src/response.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chopper/lib/src/response.dart b/chopper/lib/src/response.dart index 5b0e99c8..e4d9a56f 100644 --- a/chopper/lib/src/response.dart +++ b/chopper/lib/src/response.dart @@ -40,7 +40,7 @@ class Response { }) => Response( base ?? this.base, - body ?? (this.body as NewBodyType), + body ?? (this.body as NewBodyType?), error: bodyError ?? error, ); From ffcd94512586e5af41766f6f467ff5a2fcf922db Mon Sep 17 00:00:00 2001 From: Ivan Terekhin Date: Thu, 23 Sep 2021 17:42:24 +0300 Subject: [PATCH 008/171] Interpolation fixes (#275) --- chopper_generator/lib/src/generator.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chopper_generator/lib/src/generator.dart b/chopper_generator/lib/src/generator.dart index 40f722f6..19630d2f 100644 --- a/chopper_generator/lib/src/generator.dart +++ b/chopper_generator/lib/src/generator.dart @@ -366,7 +366,7 @@ class ChopperGenerator extends GeneratorForAnnotation { var path = getMethodPath(method); paths.forEach((p, ConstantReader r) { final name = r.peek('name')?.stringValue ?? p.displayName; - path = path.replaceFirst('{$name}', '\$${p.displayName}'); + path = path.replaceFirst('{$name}', '\${${p.displayName}}'); }); if (path.startsWith('http://') || path.startsWith('https://')) { From 82b951fe278fcd783de5285b4ef2b7cbbede61c9 Mon Sep 17 00:00:00 2001 From: Ivan Terekhin Date: Thu, 23 Sep 2021 17:42:36 +0300 Subject: [PATCH 009/171] encodeQueryComponent now encodeComponent (#278) --- chopper/lib/src/utils.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chopper/lib/src/utils.dart b/chopper/lib/src/utils.dart index 4e68b350..21b0605f 100644 --- a/chopper/lib/src/utils.dart +++ b/chopper/lib/src/utils.dart @@ -91,7 +91,7 @@ Iterable<_Pair> _iterableToQuery( ) => values.map((v) => _Pair(name, _normalizeValue(v))); -String _normalizeValue(value) => Uri.encodeQueryComponent(value.toString()); +String _normalizeValue(value) => Uri.encodeComponent(value.toString()); class _Pair { final A first; From f7255b768ce9ea6f153783575dd3f61ed8c77917 Mon Sep 17 00:00:00 2001 From: Ivan Terekhin Date: Sun, 10 Oct 2021 09:20:43 +0300 Subject: [PATCH 010/171] Prevent double call on token refreshment (#276) --- chopper/lib/src/base.dart | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/chopper/lib/src/base.dart b/chopper/lib/src/base.dart index be1d720e..1be36cb3 100644 --- a/chopper/lib/src/base.dart +++ b/chopper/lib/src/base.dart @@ -315,6 +315,13 @@ class ChopperClient { if (updatedRequest != null) { res = await send(updatedRequest); + // To prevent double call with typed response + if (_responseIsSuccessful(res.statusCode)) { + return _processResponse(res); + } else { + res = await _handleErrorResponse(res); + return _processResponse(res); + } } } @@ -327,10 +334,13 @@ class ChopperClient { res = await _handleErrorResponse(res); } - res = await _interceptResponse(res); + return _processResponse(res); + } + Future> _processResponse( + dynamic res) async { + res = await _interceptResponse(res); _responseController.add(res); - return res; } From ddefa948e58cdb96232e96cdfeb429e66c0537e2 Mon Sep 17 00:00:00 2001 From: Ivan Terekhin Date: Tue, 7 Dec 2021 19:57:13 +0300 Subject: [PATCH 011/171] Fixes for #309 #308 (#310) --- chopper/lib/src/base.dart | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/chopper/lib/src/base.dart b/chopper/lib/src/base.dart index 1be36cb3..c5cbeb06 100644 --- a/chopper/lib/src/base.dart +++ b/chopper/lib/src/base.dart @@ -311,10 +311,14 @@ class ChopperClient { dynamic res = Response(response, response.body); if (authenticator != null) { - var updatedRequest = await authenticator!.authenticate(request, res); + var updatedRequest = await authenticator!.authenticate(req, res); if (updatedRequest != null) { - res = await send(updatedRequest); + res = await send( + updatedRequest, + requestConverter: requestConverter, + responseConverter: responseConverter, + ); // To prevent double call with typed response if (_responseIsSuccessful(res.statusCode)) { return _processResponse(res); From 0386c74a53a74842a924f8711884ff9b6af45b21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20=C5=A0r=C5=AFtek?= <35694712+michalsrutek@users.noreply.github.com> Date: Mon, 3 Jan 2022 21:06:32 +0100 Subject: [PATCH 012/171] Remove new keyword from interceptors.md (#312) --- interceptors.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/interceptors.md b/interceptors.md index 541d5263..2aadab1b 100644 --- a/interceptors.md +++ b/interceptors.md @@ -7,7 +7,7 @@ Implement `RequestInterceptor` class or define function with following signature Request interceptor are called just before sending request ```dart -final chopper = new ChopperClient( +final chopper = ChopperClient( interceptors: [ (request) async => request.copyWith(body: {}), ] @@ -23,7 +23,7 @@ Called after successful or failed request {% endhint %} ```dart -final chopper = new ChopperClient( +final chopper = ChopperClient( interceptors: [ (Response response) async => response.replace(body: {}), ] From d4dab0d5ac7273227853f3f9801eba73b81aa282 Mon Sep 17 00:00:00 2001 From: Ivan Terekhin Date: Sun, 16 Jan 2022 09:21:04 +0300 Subject: [PATCH 013/171] Analyzer upgrade (#320) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Istvรกn Juhos --- .github/workflows/dart.yml | 167 +++++++++++++---------- chopper/example/main.dart | 1 - chopper/mono_pkg.yaml | 10 +- chopper/pubspec.yaml | 1 - chopper/test/base_test.dart | 7 - chopper/test/converter_test.dart | 4 - chopper/test/test_service.dart | 1 - chopper_built_value/mono_pkg.yaml | 8 +- chopper_built_value/pubspec.yaml | 1 - chopper_generator/lib/src/generator.dart | 1 - chopper_generator/mono_pkg.yaml | 8 +- chopper_generator/pubspec.yaml | 3 +- docs/ci/ci_setup.md | 9 ++ tool/ci.sh | 59 ++++---- 14 files changed, 153 insertions(+), 127 deletions(-) create mode 100644 docs/ci/ci_setup.md diff --git a/.github/workflows/dart.yml b/.github/workflows/dart.yml index bfe02c10..9bf38a8f 100644 --- a/.github/workflows/dart.yml +++ b/.github/workflows/dart.yml @@ -1,4 +1,4 @@ -# Created with package:mono_repo v3.4.7 +# Created with package:mono_repo v6.0.0 name: Dart CI on: push: @@ -21,161 +21,185 @@ jobs: runs-on: ubuntu-latest steps: - name: Cache Pub hosted dependencies - uses: actions/cache@v2 + uses: actions/cache@v2.1.7 with: path: "~/.pub-cache/hosted" - key: "os:ubuntu-latest;pub-cache-hosted;dart:stable" + key: "os:ubuntu-latest;pub-cache-hosted;sdk:stable" restore-keys: | os:ubuntu-latest;pub-cache-hosted os:ubuntu-latest - - uses: dart-lang/setup-dart@v1.0 + - uses: dart-lang/setup-dart@v1.3 with: sdk: stable - id: checkout - uses: actions/checkout@v2 + uses: actions/checkout@v2.4.0 - name: mono_repo self validate - run: pub global activate mono_repo 3.4.7 + run: dart pub global activate mono_repo 6.0.0 - name: mono_repo self validate - run: pub global run mono_repo generate --validate + run: dart pub global run mono_repo generate --validate job_002: - name: "analyzer_and_format; PKGS: chopper, chopper_built_value, chopper_generator; `dartfmt -n --set-exit-if-changed .`, `dartanalyzer --fatal-infos .`" + name: "analyzer_and_format; PKGS: chopper_built_value, chopper_generator; `dart format --output=none --set-exit-if-changed .`, `dart analyze --fatal-infos .`" runs-on: ubuntu-latest steps: - name: Cache Pub hosted dependencies - uses: actions/cache@v2 + uses: actions/cache@v2.1.7 with: path: "~/.pub-cache/hosted" - key: "os:ubuntu-latest;pub-cache-hosted;dart:stable;packages:chopper-chopper_built_value-chopper_generator;commands:dartfmt-dartanalyzer" + key: "os:ubuntu-latest;pub-cache-hosted;sdk:stable;packages:chopper_built_value-chopper_generator;commands:format-analyze" restore-keys: | - os:ubuntu-latest;pub-cache-hosted;dart:stable;packages:chopper-chopper_built_value-chopper_generator - os:ubuntu-latest;pub-cache-hosted;dart:stable + os:ubuntu-latest;pub-cache-hosted;sdk:stable;packages:chopper_built_value-chopper_generator + os:ubuntu-latest;pub-cache-hosted;sdk:stable os:ubuntu-latest;pub-cache-hosted os:ubuntu-latest - - uses: dart-lang/setup-dart@v1.0 + - uses: dart-lang/setup-dart@v1.3 with: sdk: stable - id: checkout - uses: actions/checkout@v2 - - id: chopper_pub_upgrade - name: "chopper; pub upgrade --no-precompile" - if: "always() && steps.checkout.conclusion == 'success'" - working-directory: chopper - run: pub upgrade --no-precompile - - name: "chopper; dartfmt -n --set-exit-if-changed ." - if: "always() && steps.chopper_pub_upgrade.conclusion == 'success'" - working-directory: chopper - run: dartfmt -n --set-exit-if-changed . - - name: "chopper; dartanalyzer --fatal-infos ." - if: "always() && steps.chopper_pub_upgrade.conclusion == 'success'" - working-directory: chopper - run: dartanalyzer --fatal-infos . + uses: actions/checkout@v2.4.0 - id: chopper_built_value_pub_upgrade - name: "chopper_built_value; pub upgrade --no-precompile" + name: chopper_built_value; dart pub upgrade if: "always() && steps.checkout.conclusion == 'success'" working-directory: chopper_built_value - run: pub upgrade --no-precompile - - name: "chopper_built_value; dartfmt -n --set-exit-if-changed ." + run: dart pub upgrade + - name: "chopper_built_value; dart format --output=none --set-exit-if-changed ." if: "always() && steps.chopper_built_value_pub_upgrade.conclusion == 'success'" working-directory: chopper_built_value - run: dartfmt -n --set-exit-if-changed . - - name: "chopper_built_value; dartanalyzer --fatal-infos ." + run: "dart format --output=none --set-exit-if-changed ." + - name: "chopper_built_value; dart analyze --fatal-infos ." if: "always() && steps.chopper_built_value_pub_upgrade.conclusion == 'success'" working-directory: chopper_built_value - run: dartanalyzer --fatal-infos . + run: dart analyze --fatal-infos . - id: chopper_generator_pub_upgrade - name: "chopper_generator; pub upgrade --no-precompile" + name: chopper_generator; dart pub upgrade if: "always() && steps.checkout.conclusion == 'success'" working-directory: chopper_generator - run: pub upgrade --no-precompile - - name: "chopper_generator; dartfmt -n --set-exit-if-changed ." + run: dart pub upgrade + - name: "chopper_generator; dart format --output=none --set-exit-if-changed ." if: "always() && steps.chopper_generator_pub_upgrade.conclusion == 'success'" working-directory: chopper_generator - run: dartfmt -n --set-exit-if-changed . - - name: "chopper_generator; dartanalyzer --fatal-infos ." + run: "dart format --output=none --set-exit-if-changed ." + - name: "chopper_generator; dart analyze --fatal-infos ." if: "always() && steps.chopper_generator_pub_upgrade.conclusion == 'success'" working-directory: chopper_generator - run: dartanalyzer --fatal-infos . + run: dart analyze --fatal-infos . job_003: - name: "unit_test; PKGS: chopper, chopper_built_value; `pub run test`" + name: "analyze_and_format; PKG: chopper; `dart format --output=none --set-exit-if-changed .`, `dart analyze --fatal-infos .`" runs-on: ubuntu-latest steps: - name: Cache Pub hosted dependencies - uses: actions/cache@v2 + uses: actions/cache@v2.1.7 with: path: "~/.pub-cache/hosted" - key: "os:ubuntu-latest;pub-cache-hosted;dart:stable;packages:chopper-chopper_built_value;commands:test_0" + key: "os:ubuntu-latest;pub-cache-hosted;sdk:stable;packages:chopper;commands:format-analyze" restore-keys: | - os:ubuntu-latest;pub-cache-hosted;dart:stable;packages:chopper-chopper_built_value - os:ubuntu-latest;pub-cache-hosted;dart:stable + os:ubuntu-latest;pub-cache-hosted;sdk:stable;packages:chopper + os:ubuntu-latest;pub-cache-hosted;sdk:stable os:ubuntu-latest;pub-cache-hosted os:ubuntu-latest - - uses: dart-lang/setup-dart@v1.0 + - uses: dart-lang/setup-dart@v1.3 with: sdk: stable - id: checkout - uses: actions/checkout@v2 + uses: actions/checkout@v2.4.0 + - id: chopper_pub_upgrade + name: chopper; dart pub upgrade + if: "always() && steps.checkout.conclusion == 'success'" + working-directory: chopper + run: dart pub upgrade + - name: "chopper; dart format --output=none --set-exit-if-changed ." + if: "always() && steps.chopper_pub_upgrade.conclusion == 'success'" + working-directory: chopper + run: "dart format --output=none --set-exit-if-changed ." + - name: "chopper; dart analyze --fatal-infos ." + if: "always() && steps.chopper_pub_upgrade.conclusion == 'success'" + working-directory: chopper + run: dart analyze --fatal-infos . + needs: + - job_001 + - job_002 + job_004: + name: "unit_test; PKGS: chopper, chopper_built_value; `dart test -p chrome`" + runs-on: ubuntu-latest + steps: + - name: Cache Pub hosted dependencies + uses: actions/cache@v2.1.7 + with: + path: "~/.pub-cache/hosted" + key: "os:ubuntu-latest;pub-cache-hosted;sdk:stable;packages:chopper-chopper_built_value;commands:test_1" + restore-keys: | + os:ubuntu-latest;pub-cache-hosted;sdk:stable;packages:chopper-chopper_built_value + os:ubuntu-latest;pub-cache-hosted;sdk:stable + os:ubuntu-latest;pub-cache-hosted + os:ubuntu-latest + - uses: dart-lang/setup-dart@v1.3 + with: + sdk: stable + - id: checkout + uses: actions/checkout@v2.4.0 - id: chopper_pub_upgrade - name: "chopper; pub upgrade --no-precompile" + name: chopper; dart pub upgrade if: "always() && steps.checkout.conclusion == 'success'" working-directory: chopper - run: pub upgrade --no-precompile - - name: chopper; pub run test + run: dart pub upgrade + - name: "chopper; dart test -p chrome" if: "always() && steps.chopper_pub_upgrade.conclusion == 'success'" working-directory: chopper - run: pub run test + run: dart test -p chrome - id: chopper_built_value_pub_upgrade - name: "chopper_built_value; pub upgrade --no-precompile" + name: chopper_built_value; dart pub upgrade if: "always() && steps.checkout.conclusion == 'success'" working-directory: chopper_built_value - run: pub upgrade --no-precompile - - name: chopper_built_value; pub run test + run: dart pub upgrade + - name: "chopper_built_value; dart test -p chrome" if: "always() && steps.chopper_built_value_pub_upgrade.conclusion == 'success'" working-directory: chopper_built_value - run: pub run test + run: dart test -p chrome needs: - job_001 - job_002 - job_004: - name: "unit_test; PKGS: chopper, chopper_built_value; `pub run test -p chrome`" + - job_003 + job_005: + name: "unit_test; PKGS: chopper, chopper_built_value; `dart test`" runs-on: ubuntu-latest steps: - name: Cache Pub hosted dependencies - uses: actions/cache@v2 + uses: actions/cache@v2.1.7 with: path: "~/.pub-cache/hosted" - key: "os:ubuntu-latest;pub-cache-hosted;dart:stable;packages:chopper-chopper_built_value;commands:test_1" + key: "os:ubuntu-latest;pub-cache-hosted;sdk:stable;packages:chopper-chopper_built_value;commands:test_0" restore-keys: | - os:ubuntu-latest;pub-cache-hosted;dart:stable;packages:chopper-chopper_built_value - os:ubuntu-latest;pub-cache-hosted;dart:stable + os:ubuntu-latest;pub-cache-hosted;sdk:stable;packages:chopper-chopper_built_value + os:ubuntu-latest;pub-cache-hosted;sdk:stable os:ubuntu-latest;pub-cache-hosted os:ubuntu-latest - - uses: dart-lang/setup-dart@v1.0 + - uses: dart-lang/setup-dart@v1.3 with: sdk: stable - id: checkout - uses: actions/checkout@v2 + uses: actions/checkout@v2.4.0 - id: chopper_pub_upgrade - name: "chopper; pub upgrade --no-precompile" + name: chopper; dart pub upgrade if: "always() && steps.checkout.conclusion == 'success'" working-directory: chopper - run: pub upgrade --no-precompile - - name: "chopper; pub run test -p chrome" + run: dart pub upgrade + - name: chopper; dart test if: "always() && steps.chopper_pub_upgrade.conclusion == 'success'" working-directory: chopper - run: pub run test -p chrome + run: dart test - id: chopper_built_value_pub_upgrade - name: "chopper_built_value; pub upgrade --no-precompile" + name: chopper_built_value; dart pub upgrade if: "always() && steps.checkout.conclusion == 'success'" working-directory: chopper_built_value - run: pub upgrade --no-precompile - - name: "chopper_built_value; pub run test -p chrome" + run: dart pub upgrade + - name: chopper_built_value; dart test if: "always() && steps.chopper_built_value_pub_upgrade.conclusion == 'success'" working-directory: chopper_built_value - run: pub run test -p chrome + run: dart test needs: - job_001 - job_002 - job_005: + - job_003 + job_006: name: Coverage runs-on: ubuntu-latest steps: @@ -195,3 +219,4 @@ jobs: - job_002 - job_003 - job_004 + - job_005 diff --git a/chopper/example/main.dart b/chopper/example/main.dart index 01070815..d1831859 100644 --- a/chopper/example/main.dart +++ b/chopper/example/main.dart @@ -1,5 +1,4 @@ import 'package:chopper/chopper.dart'; -import 'package:chopper/src/interceptor.dart'; import 'definition.dart'; Future main() async { diff --git a/chopper/mono_pkg.yaml b/chopper/mono_pkg.yaml index f1dd9d0f..ed853726 100644 --- a/chopper/mono_pkg.yaml +++ b/chopper/mono_pkg.yaml @@ -1,11 +1,11 @@ -dart: -- stable +sdk: + - stable stages: -- analyzer_and_format: +- analyze_and_format: - group: - - dartfmt - - dartanalyzer: --fatal-infos . + - format + - analyze: --fatal-infos . - unit_test: - test: - test: -p chrome diff --git a/chopper/pubspec.yaml b/chopper/pubspec.yaml index 70d13169..d2e41a97 100644 --- a/chopper/pubspec.yaml +++ b/chopper/pubspec.yaml @@ -3,7 +3,6 @@ description: Chopper is an http client generator using source_gen, inspired by R version: 4.0.1 documentation: https://hadrien-lejard.gitbook.io/chopper repository: https://github.com/lejard-h/chopper -author: Hadrien Lejard environment: sdk: ">=2.12.0 <3.0.0" diff --git a/chopper/test/base_test.dart b/chopper/test/base_test.dart index d487184d..b591168a 100644 --- a/chopper/test/base_test.dart +++ b/chopper/test/base_test.dart @@ -22,13 +22,6 @@ void main() { errorConverter: errorConverter, ); group('Base', () { - test('get service', () async { - final chopper = buildClient(); - final service = chopper.getService(); - - expect(service is HttpTestService, isTrue); - }); - test('get service errors', () async { final chopper = ChopperClient( baseUrl: baseUrl, diff --git a/chopper/test/converter_test.dart b/chopper/test/converter_test.dart index 1941445b..7bc98b58 100644 --- a/chopper/test/converter_test.dart +++ b/chopper/test/converter_test.dart @@ -72,7 +72,6 @@ void main() { final res = Response(http.Response('"$value"', 200), '"$value"'); final converted = jsonConverter.convertResponse(res); - expect(converted is Response, isTrue); expect(converted.body, equals(value)); }); @@ -84,7 +83,6 @@ void main() { final converted = jsonConverter.convertResponse, String>(res); - expect(converted is Response>, isTrue); expect(converted.body, equals(['foo', 'bar'])); }); @@ -92,7 +90,6 @@ void main() { final res = Response(http.Response('[1,2]', 200), '[1,2]'); final converted = jsonConverter.convertResponse, int>(res); - expect(converted is Response>, isTrue); expect(converted.body, equals([1, 2])); }); @@ -104,7 +101,6 @@ void main() { final converted = jsonConverter.convertResponse, String>(res); - expect(converted is Response>, isTrue); expect(converted.body, equals({'foo': 'bar'})); }); }); diff --git a/chopper/test/test_service.dart b/chopper/test/test_service.dart index 426b045d..1c68866a 100644 --- a/chopper/test/test_service.dart +++ b/chopper/test/test_service.dart @@ -1,7 +1,6 @@ import 'dart:async'; import 'dart:convert'; import 'package:chopper/chopper.dart'; -import 'package:chopper/src/constants.dart'; import 'package:http/http.dart' show MultipartFile; diff --git a/chopper_built_value/mono_pkg.yaml b/chopper_built_value/mono_pkg.yaml index f1dd9d0f..3d4d539a 100644 --- a/chopper_built_value/mono_pkg.yaml +++ b/chopper_built_value/mono_pkg.yaml @@ -1,11 +1,11 @@ -dart: -- stable +sdk: + - stable stages: - analyzer_and_format: - group: - - dartfmt - - dartanalyzer: --fatal-infos . + - format + - analyze: --fatal-infos . - unit_test: - test: - test: -p chrome diff --git a/chopper_built_value/pubspec.yaml b/chopper_built_value/pubspec.yaml index 4f8b59b7..88ccd7fc 100644 --- a/chopper_built_value/pubspec.yaml +++ b/chopper_built_value/pubspec.yaml @@ -3,7 +3,6 @@ description: A built_value based Converter for Chopper. version: 1.0.0 documentation: https://hadrien-lejard.gitbook.io/chopper/converters/built-value-converter repository: https://github.com/lejard-h/chopper -author: Hadrien Lejard environment: sdk: ">=2.12.0 <3.0.0" diff --git a/chopper_generator/lib/src/generator.dart b/chopper_generator/lib/src/generator.dart index 19630d2f..bae30db8 100644 --- a/chopper_generator/lib/src/generator.dart +++ b/chopper_generator/lib/src/generator.dart @@ -6,7 +6,6 @@ import 'package:analyzer/dart/element/nullability_suffix.dart'; import 'package:analyzer/dart/element/type.dart'; import 'package:build/build.dart'; -import 'package:build/src/builder/build_step.dart'; import 'package:built_collection/built_collection.dart'; import 'package:dart_style/dart_style.dart'; diff --git a/chopper_generator/mono_pkg.yaml b/chopper_generator/mono_pkg.yaml index be29992e..0620d98d 100644 --- a/chopper_generator/mono_pkg.yaml +++ b/chopper_generator/mono_pkg.yaml @@ -1,11 +1,11 @@ -dart: -- stable +sdk: + - stable stages: - analyzer_and_format: - group: - - dartfmt - - dartanalyzer: --fatal-infos . + - format + - analyze: --fatal-infos . cache: directories: diff --git a/chopper_generator/pubspec.yaml b/chopper_generator/pubspec.yaml index 708c683c..a88b8b97 100644 --- a/chopper_generator/pubspec.yaml +++ b/chopper_generator/pubspec.yaml @@ -3,13 +3,12 @@ description: Chopper is an http client generator using source_gen, inspired by R version: 4.0.2 documentation: https://hadrien-lejard.gitbook.io/chopper repository: https://github.com/lejard-h/chopper -author: Hadrien Lejard environment: sdk: ">=2.12.0 <3.0.0" dependencies: - analyzer: ^2.0.0 + analyzer: ^3.0.0 build: ^2.0.0 built_collection: ^5.0.0 chopper: ^4.0.0 diff --git a/docs/ci/ci_setup.md b/docs/ci/ci_setup.md new file mode 100644 index 00000000..99dca171 --- /dev/null +++ b/docs/ci/ci_setup.md @@ -0,0 +1,9 @@ +# The CI setup of the project + +โš ๏ธ This document is heavily WIP. It will contain the full CI setup guide for this project. + +## Generating the CI config + +We use the [`mono_repo`](https://pub.dev/packages/mono_repo) Dart package project for generating the GitHub CI config. + +To install and use `mono_repo`, refer to its official documentation linked above. \ No newline at end of file diff --git a/tool/ci.sh b/tool/ci.sh index f7a6d3f0..d614ed80 100755 --- a/tool/ci.sh +++ b/tool/ci.sh @@ -1,26 +1,35 @@ #!/bin/bash -# Created with package:mono_repo v3.4.7 +# Created with package:mono_repo v6.0.0 # Support built in commands on windows out of the box. +# When it is a flutter repo (check the pubspec.yaml for "sdk: flutter") +# then "flutter" is called instead of "pub". +# This assumes that the Flutter SDK has been installed in a previous step. function pub() { - if [[ $TRAVIS_OS_NAME == "windows" ]]; then - command pub.bat "$@" + if grep -Fq "sdk: flutter" "${PWD}/pubspec.yaml"; then + command flutter pub "$@" else - command pub "$@" + command dart pub "$@" fi } -function dartfmt() { - if [[ $TRAVIS_OS_NAME == "windows" ]]; then - command dartfmt.bat "$@" +# When it is a flutter repo (check the pubspec.yaml for "sdk: flutter") +# then "flutter" is called instead of "pub". +# This assumes that the Flutter SDK has been installed in a previous step. +function format() { + if grep -Fq "sdk: flutter" "${PWD}/pubspec.yaml"; then + command flutter format "$@" else - command dartfmt "$@" + command dart format "$@" fi } -function dartanalyzer() { - if [[ $TRAVIS_OS_NAME == "windows" ]]; then - command dartanalyzer.bat "$@" +# When it is a flutter repo (check the pubspec.yaml for "sdk: flutter") +# then "flutter" is called instead of "pub". +# This assumes that the Flutter SDK has been installed in a previous step. +function analyze() { + if grep -Fq "sdk: flutter" "${PWD}/pubspec.yaml"; then + command flutter analyze "$@" else - command dartanalyzer "$@" + command dart analyze "$@" fi } @@ -47,32 +56,32 @@ for PKG in ${PKGS}; do exit 64 fi - pub upgrade --no-precompile || EXIT_CODE=$? + dart pub upgrade || EXIT_CODE=$? if [[ ${EXIT_CODE} -ne 0 ]]; then - echo -e "\033[31mPKG: ${PKG}; 'pub upgrade' - FAILED (${EXIT_CODE})\033[0m" - FAILURES+=("${PKG}; 'pub upgrade'") + echo -e "\033[31mPKG: ${PKG}; 'dart pub upgrade' - FAILED (${EXIT_CODE})\033[0m" + FAILURES+=("${PKG}; 'dart pub upgrade'") else for TASK in "$@"; do EXIT_CODE=0 echo echo -e "\033[1mPKG: ${PKG}; TASK: ${TASK}\033[22m" case ${TASK} in - dartanalyzer) - echo 'dartanalyzer --fatal-infos .' - dartanalyzer --fatal-infos . || EXIT_CODE=$? + analyze) + echo 'dart analyze --fatal-infos .' + dart analyze --fatal-infos . || EXIT_CODE=$? ;; - dartfmt) - echo 'dartfmt -n --set-exit-if-changed .' - dartfmt -n --set-exit-if-changed . || EXIT_CODE=$? + format) + echo 'dart format --output=none --set-exit-if-changed .' + dart format --output=none --set-exit-if-changed . || EXIT_CODE=$? ;; test_0) - echo 'pub run test' - pub run test || EXIT_CODE=$? + echo 'dart test' + dart test || EXIT_CODE=$? ;; test_1) - echo 'pub run test -p chrome' - pub run test -p chrome || EXIT_CODE=$? + echo 'dart test -p chrome' + dart test -p chrome || EXIT_CODE=$? ;; *) echo -e "\033[31mUnknown TASK '${TASK}' - TERMINATING JOB\033[0m" From bbe2c7ad92a1a4839b2c6eb5bf0d67f05c639ef1 Mon Sep 17 00:00:00 2001 From: Andre Date: Sun, 16 Jan 2022 15:21:56 -0500 Subject: [PATCH 014/171] Add unnecessary_brace_in_string_interps to lint ignores (#317) --- chopper_generator/lib/src/generator.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chopper_generator/lib/src/generator.dart b/chopper_generator/lib/src/generator.dart index bae30db8..ccf42daf 100644 --- a/chopper_generator/lib/src/generator.dart +++ b/chopper_generator/lib/src/generator.dart @@ -82,7 +82,7 @@ class ChopperGenerator extends GeneratorForAnnotation { }); final ignore = - '// ignore_for_file: always_put_control_body_on_new_line, always_specify_types, prefer_const_declarations'; + '// ignore_for_file: always_put_control_body_on_new_line, always_specify_types, prefer_const_declarations, unnecessary_brace_in_string_interps'; final emitter = DartEmitter(); return DartFormatter().format('$ignore\n${classBuilder.accept(emitter)}'); } From 6bdbd635e254d62b91b47b14e41fe94c9dcb2697 Mon Sep 17 00:00:00 2001 From: John Wimer Date: Sat, 22 Jan 2022 10:36:47 +0100 Subject: [PATCH 015/171] Extend pragma to quiet the linter (#318) Co-authored-by: Ivan Terekhin From 4185d140f7e639e66ab6d4457391171573ad717e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Max=20R=C3=B6hrl?= Date: Sun, 30 Jan 2022 09:40:41 +0100 Subject: [PATCH 016/171] Fix converter getting called twice if using an authenticator with a JsonConverter on the request (#324) --- chopper/lib/src/authenticator.dart | 3 ++- chopper/lib/src/base.dart | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/chopper/lib/src/authenticator.dart b/chopper/lib/src/authenticator.dart index 62d5c692..db225a49 100644 --- a/chopper/lib/src/authenticator.dart +++ b/chopper/lib/src/authenticator.dart @@ -5,5 +5,6 @@ import 'package:chopper/chopper.dart'; /// This method should return a [Request] that includes credentials to satisfy an authentication challenge received in /// [response]. It should return `null` if the challenge cannot be satisfied. abstract class Authenticator { - FutureOr authenticate(Request request, Response response); + FutureOr authenticate(Request request, Response response, + [Request? originalRequest]); } diff --git a/chopper/lib/src/base.dart b/chopper/lib/src/base.dart index c5cbeb06..b0f90848 100644 --- a/chopper/lib/src/base.dart +++ b/chopper/lib/src/base.dart @@ -311,7 +311,7 @@ class ChopperClient { dynamic res = Response(response, response.body); if (authenticator != null) { - var updatedRequest = await authenticator!.authenticate(req, res); + var updatedRequest = await authenticator!.authenticate(req, res, request); if (updatedRequest != null) { res = await send( From dbf427225eb02e1bb67ea93b97b90713bfd5958e Mon Sep 17 00:00:00 2001 From: ipcjs Date: Wed, 23 Feb 2022 02:54:36 +0800 Subject: [PATCH 017/171] migrate example to nullsafety (#331) --- README.md | 1 - chopper/README.md | 1 - example/bin/main_built_value.dart | 17 +- example/bin/main_json_serializable.dart | 8 +- example/lib/angular_example.dart | 22 - example/lib/built_value_resource.chopper.dart | 18 +- example/lib/built_value_resource.dart | 4 +- example/lib/built_value_resource.g.dart | 109 +++-- example/lib/built_value_serializers.g.dart | 2 +- example/lib/json_serializable.chopper.dart | 23 +- example/lib/json_serializable.dart | 7 +- example/lib/json_serializable.g.dart | 21 +- example/pubspec.lock | 425 ++++++++++++++++++ example/pubspec.yaml | 29 +- example/web/index.html | 20 - example/web/main.dart | 34 -- 16 files changed, 553 insertions(+), 188 deletions(-) delete mode 100644 example/lib/angular_example.dart create mode 100644 example/pubspec.lock delete mode 100644 example/web/index.html delete mode 100644 example/web/main.dart diff --git a/README.md b/README.md index a477ce40..a0b3f6a5 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,6 @@ Please refer to the installation guide at [pub.dev](https://pub.dev/packages/cho * [json serializable Converter](https://github.com/lejard-h/chopper/blob/master/example/bin/main_json_serializable.dart) * [built value Converter](https://github.com/lejard-h/chopper/blob/master/example/bin/main_built_value.dart) -* [Angular](https://github.com/lejard-h/chopper/blob/master/example/web/main.dart) ## [Issue Tracker](https://github.com/lejard-h/chopper/issues) diff --git a/chopper/README.md b/chopper/README.md index eaac504c..b3453bef 100644 --- a/chopper/README.md +++ b/chopper/README.md @@ -45,6 +45,5 @@ Latest versions: * [json_serializable Converter](https://github.com/lejard-h/chopper/blob/master/example/bin/main_json_serializable.dart) * [built_value Converter](https://github.com/lejard-h/chopper/blob/master/example/bin/main_built_value.dart) -* [Angular](https://github.com/lejard-h/chopper/blob/master/example/web/main.dart) ## If you encounter any issues, or need a feature implemented, please visit [Chopper's Issue Tracker on GitHub](https://github.com/lejard-h/chopper/issues). diff --git a/example/bin/main_built_value.dart b/example/bin/main_built_value.dart index 99ec99be..8ebaff5b 100644 --- a/example/bin/main_built_value.dart +++ b/example/bin/main_built_value.dart @@ -1,4 +1,5 @@ import 'package:built_collection/built_collection.dart'; +import 'package:built_value/serializer.dart'; import 'package:chopper/chopper.dart'; import 'package:chopper_example/built_value_resource.dart'; import 'package:chopper_example/built_value_serializers.dart'; @@ -52,16 +53,22 @@ main() async { } class BuiltValueConverter extends JsonConverter { - T _deserialize(dynamic value) => jsonSerializers.deserializeWith( - jsonSerializers.serializerForType(T), - value, - ); + T? _deserialize(dynamic value) { + final serializer = jsonSerializers.serializerForType(T) as Serializer?; + if (serializer == null) { + throw Exception('No serializer for type ${T}'); + } + return jsonSerializers.deserializeWith( + serializer, + value, + ); + } BuiltList _deserializeListOf(Iterable value) => BuiltList( value.map((value) => _deserialize(value)).toList(growable: false), ); - dynamic _decode(entity) { + dynamic _decode(dynamic entity) { /// handle case when we want to access to Map directly /// getResource or getMapResource /// Avoid dynamic or unconverted value, this could lead to several issues diff --git a/example/bin/main_json_serializable.dart b/example/bin/main_json_serializable.dart index 8fab276d..eb7fccfc 100644 --- a/example/bin/main_json_serializable.dart +++ b/example/bin/main_json_serializable.dart @@ -67,7 +67,7 @@ class JsonSerializableConverter extends JsonConverter { JsonSerializableConverter(this.factories); - T _decodeMap(Map values) { + T? _decodeMap(Map values) { /// Get jsonFactory using Type parameters /// if not found or invalid, throw error or return null final jsonFactory = factories[T]; @@ -79,13 +79,13 @@ class JsonSerializableConverter extends JsonConverter { return jsonFactory(values); } - List _decodeList(List values) => + List _decodeList(Iterable values) => values.where((v) => v != null).map((v) => _decode(v)).toList(); - dynamic _decode(entity) { + dynamic _decode(dynamic entity) { if (entity is Iterable) return _decodeList(entity); - if (entity is Map) return _decodeMap(entity); + if (entity is Map) return _decodeMap(entity); return entity; } diff --git a/example/lib/angular_example.dart b/example/lib/angular_example.dart deleted file mode 100644 index 09d688f3..00000000 --- a/example/lib/angular_example.dart +++ /dev/null @@ -1,22 +0,0 @@ -import 'package:angular/angular.dart'; -import 'package:chopper/chopper.dart'; -import 'package:chopper_example/built_value_resource.dart'; - -// ignore: uri_has_not_been_generated -import 'angular_example.template.dart' as ng; - -final appFactory = ng.ChopperExampleComponentNgFactory; - -MyService serviceFactory(ChopperClient client) => MyService.create(client); - -@Component( - selector: 'app-component', - template: '{{client}} {{service}}', - providers: [FactoryProvider(MyService, serviceFactory)], -) -class ChopperExampleComponent { - final ChopperClient client; - final MyService service; - - ChopperExampleComponent(this.client, this.service); -} diff --git a/example/lib/built_value_resource.chopper.dart b/example/lib/built_value_resource.chopper.dart index f18fe2b4..c092d4b0 100644 --- a/example/lib/built_value_resource.chopper.dart +++ b/example/lib/built_value_resource.chopper.dart @@ -6,9 +6,9 @@ part of resource; // ChopperGenerator // ************************************************************************** -// ignore_for_file: always_put_control_body_on_new_line, always_specify_types, prefer_const_declarations +// ignore_for_file: always_put_control_body_on_new_line, always_specify_types, prefer_const_declarations, unnecessary_brace_in_string_interps class _$MyService extends MyService { - _$MyService([ChopperClient client]) { + _$MyService([ChopperClient? client]) { if (client == null) return; this.client = client; } @@ -18,7 +18,7 @@ class _$MyService extends MyService { @override Future> getResource(String id) { - final $url = '/resources/$id/'; + final $url = '/resources/${id}/'; final $request = Request('GET', $url, client.baseUrl); return client.send($request); } @@ -33,15 +33,21 @@ class _$MyService extends MyService { @override Future> getTypedResource() { final $url = '/resources/'; - final $headers = {'foo': 'bar'}; + final $headers = { + 'foo': 'bar', + }; + final $request = Request('GET', $url, client.baseUrl, headers: $headers); return client.send($request); } @override - Future> newResource(Resource resource, {String name}) { + Future> newResource(Resource resource, {String? name}) { final $url = '/resources'; - final $headers = {'name': name}; + final $headers = { + if (name != null) 'name': name, + }; + final $body = resource; final $request = Request('POST', $url, client.baseUrl, body: $body, headers: $headers); diff --git a/example/lib/built_value_resource.dart b/example/lib/built_value_resource.dart index f49131e5..6ea4e35b 100644 --- a/example/lib/built_value_resource.dart +++ b/example/lib/built_value_resource.dart @@ -33,7 +33,7 @@ abstract class ResourceError @ChopperApi(baseUrl: "/resources") abstract class MyService extends ChopperService { - static MyService create([ChopperClient client]) => _$MyService(client); + static MyService create([ChopperClient? client]) => _$MyService(client); @Get(path: "/{id}/") Future getResource(@Path() String id); @@ -46,5 +46,5 @@ abstract class MyService extends ChopperService { @Post() Future> newResource(@Body() Resource resource, - {@Header() String name}); + {@Header() String? name}); } diff --git a/example/lib/built_value_resource.g.dart b/example/lib/built_value_resource.g.dart index 90a3d5e4..8030092d 100644 --- a/example/lib/built_value_resource.g.dart +++ b/example/lib/built_value_resource.g.dart @@ -17,9 +17,9 @@ class _$ResourceSerializer implements StructuredSerializer { final String wireName = 'Resource'; @override - Iterable serialize(Serializers serializers, Resource object, + Iterable serialize(Serializers serializers, Resource object, {FullType specifiedType = FullType.unspecified}) { - final result = [ + final result = [ 'id', serializers.serialize(object.id, specifiedType: const FullType(String)), 'name', @@ -30,7 +30,7 @@ class _$ResourceSerializer implements StructuredSerializer { } @override - Resource deserialize(Serializers serializers, Iterable serialized, + Resource deserialize(Serializers serializers, Iterable serialized, {FullType specifiedType = FullType.unspecified}) { final result = new ResourceBuilder(); @@ -38,7 +38,7 @@ class _$ResourceSerializer implements StructuredSerializer { while (iterator.moveNext()) { final key = iterator.current as String; iterator.moveNext(); - final dynamic value = iterator.current; + final Object? value = iterator.current; switch (key) { case 'id': result.id = serializers.deserialize(value, @@ -62,9 +62,9 @@ class _$ResourceErrorSerializer implements StructuredSerializer { final String wireName = 'ResourceError'; @override - Iterable serialize(Serializers serializers, ResourceError object, + Iterable serialize(Serializers serializers, ResourceError object, {FullType specifiedType = FullType.unspecified}) { - final result = [ + final result = [ 'type', serializers.serialize(object.type, specifiedType: const FullType(String)), 'message', @@ -77,7 +77,7 @@ class _$ResourceErrorSerializer implements StructuredSerializer { @override ResourceError deserialize( - Serializers serializers, Iterable serialized, + Serializers serializers, Iterable serialized, {FullType specifiedType = FullType.unspecified}) { final result = new ResourceErrorBuilder(); @@ -85,7 +85,7 @@ class _$ResourceErrorSerializer implements StructuredSerializer { while (iterator.moveNext()) { final key = iterator.current as String; iterator.moveNext(); - final dynamic value = iterator.current; + final Object? value = iterator.current; switch (key) { case 'type': result.type = serializers.deserialize(value, @@ -108,16 +108,12 @@ class _$Resource extends Resource { @override final String name; - factory _$Resource([void Function(ResourceBuilder) updates]) => + factory _$Resource([void Function(ResourceBuilder)? updates]) => (new ResourceBuilder()..update(updates)).build(); - _$Resource._({this.id, this.name}) : super._() { - if (id == null) { - throw new BuiltValueNullFieldError('Resource', 'id'); - } - if (name == null) { - throw new BuiltValueNullFieldError('Resource', 'name'); - } + _$Resource._({required this.id, required this.name}) : super._() { + BuiltValueNullFieldError.checkNotNull(id, 'Resource', 'id'); + BuiltValueNullFieldError.checkNotNull(name, 'Resource', 'name'); } @override @@ -148,22 +144,23 @@ class _$Resource extends Resource { } class ResourceBuilder implements Builder { - _$Resource _$v; + _$Resource? _$v; - String _id; - String get id => _$this._id; - set id(String id) => _$this._id = id; + String? _id; + String? get id => _$this._id; + set id(String? id) => _$this._id = id; - String _name; - String get name => _$this._name; - set name(String name) => _$this._name = name; + String? _name; + String? get name => _$this._name; + set name(String? name) => _$this._name = name; ResourceBuilder(); ResourceBuilder get _$this { - if (_$v != null) { - _id = _$v.id; - _name = _$v.name; + final $v = _$v; + if ($v != null) { + _id = $v.id; + _name = $v.name; _$v = null; } return this; @@ -171,20 +168,22 @@ class ResourceBuilder implements Builder { @override void replace(Resource other) { - if (other == null) { - throw new ArgumentError.notNull('other'); - } + ArgumentError.checkNotNull(other, 'other'); _$v = other as _$Resource; } @override - void update(void Function(ResourceBuilder) updates) { + void update(void Function(ResourceBuilder)? updates) { if (updates != null) updates(this); } @override _$Resource build() { - final _$result = _$v ?? new _$Resource._(id: id, name: name); + final _$result = _$v ?? + new _$Resource._( + id: BuiltValueNullFieldError.checkNotNull(id, 'Resource', 'id'), + name: BuiltValueNullFieldError.checkNotNull( + name, 'Resource', 'name')); replace(_$result); return _$result; } @@ -196,16 +195,12 @@ class _$ResourceError extends ResourceError { @override final String message; - factory _$ResourceError([void Function(ResourceErrorBuilder) updates]) => + factory _$ResourceError([void Function(ResourceErrorBuilder)? updates]) => (new ResourceErrorBuilder()..update(updates)).build(); - _$ResourceError._({this.type, this.message}) : super._() { - if (type == null) { - throw new BuiltValueNullFieldError('ResourceError', 'type'); - } - if (message == null) { - throw new BuiltValueNullFieldError('ResourceError', 'message'); - } + _$ResourceError._({required this.type, required this.message}) : super._() { + BuiltValueNullFieldError.checkNotNull(type, 'ResourceError', 'type'); + BuiltValueNullFieldError.checkNotNull(message, 'ResourceError', 'message'); } @override @@ -239,22 +234,23 @@ class _$ResourceError extends ResourceError { class ResourceErrorBuilder implements Builder { - _$ResourceError _$v; + _$ResourceError? _$v; - String _type; - String get type => _$this._type; - set type(String type) => _$this._type = type; + String? _type; + String? get type => _$this._type; + set type(String? type) => _$this._type = type; - String _message; - String get message => _$this._message; - set message(String message) => _$this._message = message; + String? _message; + String? get message => _$this._message; + set message(String? message) => _$this._message = message; ResourceErrorBuilder(); ResourceErrorBuilder get _$this { - if (_$v != null) { - _type = _$v.type; - _message = _$v.message; + final $v = _$v; + if ($v != null) { + _type = $v.type; + _message = $v.message; _$v = null; } return this; @@ -262,23 +258,26 @@ class ResourceErrorBuilder @override void replace(ResourceError other) { - if (other == null) { - throw new ArgumentError.notNull('other'); - } + ArgumentError.checkNotNull(other, 'other'); _$v = other as _$ResourceError; } @override - void update(void Function(ResourceErrorBuilder) updates) { + void update(void Function(ResourceErrorBuilder)? updates) { if (updates != null) updates(this); } @override _$ResourceError build() { - final _$result = _$v ?? new _$ResourceError._(type: type, message: message); + final _$result = _$v ?? + new _$ResourceError._( + type: BuiltValueNullFieldError.checkNotNull( + type, 'ResourceError', 'type'), + message: BuiltValueNullFieldError.checkNotNull( + message, 'ResourceError', 'message')); replace(_$result); return _$result; } } -// ignore_for_file: always_put_control_body_on_new_line,always_specify_types,annotate_overrides,avoid_annotating_with_dynamic,avoid_as,avoid_catches_without_on_clauses,avoid_returning_this,lines_longer_than_80_chars,omit_local_variable_types,prefer_expression_function_bodies,sort_constructors_first,test_types_in_equals,unnecessary_const,unnecessary_new +// ignore_for_file: always_put_control_body_on_new_line,always_specify_types,annotate_overrides,avoid_annotating_with_dynamic,avoid_as,avoid_catches_without_on_clauses,avoid_returning_this,deprecated_member_use_from_same_package,lines_longer_than_80_chars,omit_local_variable_types,prefer_expression_function_bodies,sort_constructors_first,test_types_in_equals,unnecessary_const,unnecessary_new diff --git a/example/lib/built_value_serializers.g.dart b/example/lib/built_value_serializers.g.dart index 2863e337..dbad2232 100644 --- a/example/lib/built_value_serializers.g.dart +++ b/example/lib/built_value_serializers.g.dart @@ -11,4 +11,4 @@ Serializers _$serializers = (new Serializers().toBuilder() ..add(ResourceError.serializer)) .build(); -// ignore_for_file: always_put_control_body_on_new_line,always_specify_types,annotate_overrides,avoid_annotating_with_dynamic,avoid_as,avoid_catches_without_on_clauses,avoid_returning_this,lines_longer_than_80_chars,omit_local_variable_types,prefer_expression_function_bodies,sort_constructors_first,test_types_in_equals,unnecessary_const,unnecessary_new +// ignore_for_file: always_put_control_body_on_new_line,always_specify_types,annotate_overrides,avoid_annotating_with_dynamic,avoid_as,avoid_catches_without_on_clauses,avoid_returning_this,deprecated_member_use_from_same_package,lines_longer_than_80_chars,omit_local_variable_types,prefer_expression_function_bodies,sort_constructors_first,test_types_in_equals,unnecessary_const,unnecessary_new diff --git a/example/lib/json_serializable.chopper.dart b/example/lib/json_serializable.chopper.dart index 55ee0d31..bd44d2f3 100644 --- a/example/lib/json_serializable.chopper.dart +++ b/example/lib/json_serializable.chopper.dart @@ -6,9 +6,9 @@ part of 'json_serializable.dart'; // ChopperGenerator // ************************************************************************** -// ignore_for_file: always_put_control_body_on_new_line, always_specify_types, prefer_const_declarations +// ignore_for_file: always_put_control_body_on_new_line, always_specify_types, prefer_const_declarations, unnecessary_brace_in_string_interps class _$MyService extends MyService { - _$MyService([ChopperClient client]) { + _$MyService([ChopperClient? client]) { if (client == null) return; this.client = client; } @@ -18,7 +18,7 @@ class _$MyService extends MyService { @override Future> getResource(String id) { - final $url = '/resources/$id/'; + final $url = '/resources/${id}/'; final $request = Request('GET', $url, client.baseUrl); return client.send($request); } @@ -26,7 +26,10 @@ class _$MyService extends MyService { @override Future>> getResources() { final $url = '/resources/all'; - final $headers = {'test': 'list'}; + final $headers = { + 'test': 'list', + }; + final $request = Request('GET', $url, client.baseUrl, headers: $headers); return client.send, Resource>($request); } @@ -42,15 +45,21 @@ class _$MyService extends MyService { @override Future> getTypedResource() { final $url = '/resources/'; - final $headers = {'foo': 'bar'}; + final $headers = { + 'foo': 'bar', + }; + final $request = Request('GET', $url, client.baseUrl, headers: $headers); return client.send($request); } @override - Future> newResource(Resource resource, {String name}) { + Future> newResource(Resource resource, {String? name}) { final $url = '/resources'; - final $headers = {'name': name}; + final $headers = { + if (name != null) 'name': name, + }; + final $body = resource; final $request = Request('POST', $url, client.baseUrl, body: $body, headers: $headers); diff --git a/example/lib/json_serializable.dart b/example/lib/json_serializable.dart index ceca95d0..361f166a 100644 --- a/example/lib/json_serializable.dart +++ b/example/lib/json_serializable.dart @@ -16,6 +16,9 @@ class Resource { static const fromJsonFactory = _$ResourceFromJson; Map toJson() => _$ResourceToJson(this); + + @override + String toString() => 'Resource{id: $id, name: $name}'; } @JsonSerializable() @@ -32,7 +35,7 @@ class ResourceError { @ChopperApi(baseUrl: "/resources") abstract class MyService extends ChopperService { - static MyService create([ChopperClient client]) => _$MyService(client); + static MyService create([ChopperClient? client]) => _$MyService(client); @Get(path: "/{id}/") Future getResource(@Path() String id); @@ -48,5 +51,5 @@ abstract class MyService extends ChopperService { @Post() Future> newResource(@Body() Resource resource, - {@Header() String name}); + {@Header() String? name}); } diff --git a/example/lib/json_serializable.g.dart b/example/lib/json_serializable.g.dart index e8cb2986..bb2b4f95 100644 --- a/example/lib/json_serializable.g.dart +++ b/example/lib/json_serializable.g.dart @@ -6,24 +6,21 @@ part of 'json_serializable.dart'; // JsonSerializableGenerator // ************************************************************************** -Resource _$ResourceFromJson(Map json) { - return Resource( - json['id'] as String, - json['name'] as String, - ); -} +Resource _$ResourceFromJson(Map json) => Resource( + json['id'] as String, + json['name'] as String, + ); Map _$ResourceToJson(Resource instance) => { 'id': instance.id, 'name': instance.name, }; -ResourceError _$ResourceErrorFromJson(Map json) { - return ResourceError( - json['type'] as String, - json['message'] as String, - ); -} +ResourceError _$ResourceErrorFromJson(Map json) => + ResourceError( + json['type'] as String, + json['message'] as String, + ); Map _$ResourceErrorToJson(ResourceError instance) => { diff --git a/example/pubspec.lock b/example/pubspec.lock new file mode 100644 index 00000000..fff2f649 --- /dev/null +++ b/example/pubspec.lock @@ -0,0 +1,425 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + url: "https://pub.dartlang.org" + source: hosted + version: "34.0.0" + analyzer: + dependency: "direct main" + description: + name: analyzer + url: "https://pub.dartlang.org" + source: hosted + version: "3.2.0" + args: + dependency: transitive + description: + name: args + url: "https://pub.dartlang.org" + source: hosted + version: "2.3.0" + async: + dependency: transitive + description: + name: async + url: "https://pub.dartlang.org" + source: hosted + version: "2.8.2" + build: + dependency: transitive + description: + name: build + url: "https://pub.dartlang.org" + source: hosted + version: "2.2.1" + build_config: + dependency: transitive + description: + name: build_config + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" + build_daemon: + dependency: transitive + description: + name: build_daemon + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.1" + build_resolvers: + dependency: transitive + description: + name: build_resolvers + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.6" + build_runner: + dependency: "direct dev" + description: + name: build_runner + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.7" + build_runner_core: + dependency: transitive + description: + name: build_runner_core + url: "https://pub.dartlang.org" + source: hosted + version: "7.2.3" + built_collection: + dependency: transitive + description: + name: built_collection + url: "https://pub.dartlang.org" + source: hosted + version: "5.1.1" + built_value: + dependency: "direct main" + description: + name: built_value + url: "https://pub.dartlang.org" + source: hosted + version: "8.1.4" + built_value_generator: + dependency: "direct dev" + description: + name: built_value_generator + url: "https://pub.dartlang.org" + source: hosted + version: "8.1.4" + charcode: + dependency: transitive + description: + name: charcode + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.1" + checked_yaml: + dependency: transitive + description: + name: checked_yaml + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.1" + chopper: + dependency: "direct main" + description: + path: "../chopper" + relative: true + source: path + version: "4.0.1" + chopper_generator: + dependency: "direct dev" + description: + path: "../chopper_generator" + relative: true + source: path + version: "4.0.2" + cli_util: + dependency: transitive + description: + name: cli_util + url: "https://pub.dartlang.org" + source: hosted + version: "0.3.5" + code_builder: + dependency: transitive + description: + name: code_builder + url: "https://pub.dartlang.org" + source: hosted + version: "4.1.0" + collection: + dependency: transitive + description: + name: collection + url: "https://pub.dartlang.org" + source: hosted + version: "1.15.0" + convert: + dependency: transitive + description: + name: convert + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.1" + crypto: + dependency: transitive + description: + name: crypto + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.1" + dart_style: + dependency: transitive + description: + name: dart_style + url: "https://pub.dartlang.org" + source: hosted + version: "2.2.1" + file: + dependency: transitive + description: + name: file + url: "https://pub.dartlang.org" + source: hosted + version: "6.1.2" + fixnum: + dependency: transitive + description: + name: fixnum + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" + frontend_server_client: + dependency: transitive + description: + name: frontend_server_client + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.2" + glob: + dependency: transitive + description: + name: glob + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.2" + graphs: + dependency: transitive + description: + name: graphs + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + http: + dependency: transitive + description: + name: http + url: "https://pub.dartlang.org" + source: hosted + version: "0.13.4" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + url: "https://pub.dartlang.org" + source: hosted + version: "3.2.0" + http_parser: + dependency: transitive + description: + name: http_parser + url: "https://pub.dartlang.org" + source: hosted + version: "4.0.0" + io: + dependency: transitive + description: + name: io + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.3" + js: + dependency: transitive + description: + name: js + url: "https://pub.dartlang.org" + source: hosted + version: "0.6.4" + json_annotation: + dependency: "direct main" + description: + name: json_annotation + url: "https://pub.dartlang.org" + source: hosted + version: "4.4.0" + json_serializable: + dependency: "direct dev" + description: + name: json_serializable + url: "https://pub.dartlang.org" + source: hosted + version: "6.1.4" + logging: + dependency: transitive + description: + name: logging + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.2" + matcher: + dependency: transitive + description: + name: matcher + url: "https://pub.dartlang.org" + source: hosted + version: "0.12.11" + meta: + dependency: transitive + description: + name: meta + url: "https://pub.dartlang.org" + source: hosted + version: "1.7.0" + mime: + dependency: transitive + description: + name: mime + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.1" + package_config: + dependency: transitive + description: + name: package_config + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.2" + path: + dependency: transitive + description: + name: path + url: "https://pub.dartlang.org" + source: hosted + version: "1.8.1" + pool: + dependency: transitive + description: + name: pool + url: "https://pub.dartlang.org" + source: hosted + version: "1.5.0" + pub_semver: + dependency: transitive + description: + name: pub_semver + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + pubspec_parse: + dependency: transitive + description: + name: pubspec_parse + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.0" + quiver: + dependency: transitive + description: + name: quiver + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.1+1" + shelf: + dependency: transitive + description: + name: shelf + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.0" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.1" + source_gen: + dependency: transitive + description: + name: source_gen + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.1" + source_helper: + dependency: transitive + description: + name: source_helper + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.1" + source_span: + dependency: transitive + description: + name: source_span + url: "https://pub.dartlang.org" + source: hosted + version: "1.8.2" + stack_trace: + dependency: transitive + description: + name: stack_trace + url: "https://pub.dartlang.org" + source: hosted + version: "1.10.0" + stream_channel: + dependency: transitive + description: + name: stream_channel + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + stream_transform: + dependency: transitive + description: + name: stream_transform + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" + string_scanner: + dependency: transitive + description: + name: string_scanner + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0" + term_glyph: + dependency: transitive + description: + name: term_glyph + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.0" + timing: + dependency: transitive + description: + name: timing + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" + typed_data: + dependency: transitive + description: + name: typed_data + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.0" + watcher: + dependency: transitive + description: + name: watcher + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.1" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + yaml: + dependency: transitive + description: + name: yaml + url: "https://pub.dartlang.org" + source: hosted + version: "3.1.0" +sdks: + dart: ">=2.16.0-100.0.dev <3.0.0" diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 515ae565..9a88feed 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -2,28 +2,25 @@ name: chopper_example description: Example usage of the Chopper package version: 0.0.1 documentation: https://hadrien-lejard.gitbook.io/chopper/ -author: Hadrien Lejard +#author: Hadrien Lejard environment: sdk: '>=2.12.0 <3.0.0' dependencies: - angular: ^6.0.1 - chopper: ^3.0.0 - json_annotation: ^4.0.0 - built_value: ^8.0.0 - analyzer: ^1.2.0 + chopper: + json_annotation: + built_value: + analyzer: dev_dependencies: - build_runner: ^1.12.1 - chopper_generator: ^3.0.6 - build_web_compilers: ^2.0.0 - json_serializable: ^4.0.2 - built_value_generator: ^8.0.0 + build_runner: + chopper_generator: + json_serializable: + built_value_generator: dependency_overrides: -# chopper: -# path: ../chopper -# chopper_generator: -# path: ../chopper_generator - \ No newline at end of file + chopper: + path: ../chopper + chopper_generator: + path: ../chopper_generator diff --git a/example/web/index.html b/example/web/index.html deleted file mode 100644 index 79681fa5..00000000 --- a/example/web/index.html +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - Chopper - - - - - - - - - \ No newline at end of file diff --git a/example/web/main.dart b/example/web/main.dart deleted file mode 100644 index 243cf917..00000000 --- a/example/web/main.dart +++ /dev/null @@ -1,34 +0,0 @@ -import 'package:angular/angular.dart'; -import 'package:chopper/chopper.dart'; -import 'package:chopper_example/angular_example.dart'; -import 'package:http/http.dart' as http; -import 'package:http/browser_client.dart'; - -// ignore: uri_has_not_been_generated -import 'main.template.dart' as ng; - -ChopperClient chopperClientFactory(http.Client httpClient) => ChopperClient( - converter: JsonConverter(), - baseUrl: 'http://localhost:9000', - client: httpClient, - ); - -@GenerateInjector([ - ClassProvider(http.Client, useClass: BrowserClient), - FactoryProvider(ChopperClient, chopperClientFactory), -]) -final InjectorFactory chopperApp = ng.chopperApp$Injector; - -ComponentRef _app; - -void main() { - _app = runApp( - appFactory, - createInjector: chopperApp, - ); -} - -Object hot$onDestroy() { - _app.destroy(); - return null; -} From 976d4573ecf7fa636569117f72459109c7314b14 Mon Sep 17 00:00:00 2001 From: ibadin Date: Sun, 24 Apr 2022 21:10:29 +0500 Subject: [PATCH 018/171] Resolve problem in main_json_serializable example (#328) --- example/bin/main_json_serializable.dart | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/example/bin/main_json_serializable.dart b/example/bin/main_json_serializable.dart index eb7fccfc..0d09e8f8 100644 --- a/example/bin/main_json_serializable.dart +++ b/example/bin/main_json_serializable.dart @@ -60,12 +60,12 @@ Future authHeader(Request request) async => applyHeader( "42", ); -typedef T JsonFactory(Map json); +typedef JsonFactory = T Function(Map json); class JsonSerializableConverter extends JsonConverter { final Map factories; - JsonSerializableConverter(this.factories); + const JsonSerializableConverter(this.factories); T? _decodeMap(Map values) { /// Get jsonFactory using Type parameters @@ -82,10 +82,10 @@ class JsonSerializableConverter extends JsonConverter { List _decodeList(Iterable values) => values.where((v) => v != null).map((v) => _decode(v)).toList(); - dynamic _decode(dynamic entity) { - if (entity is Iterable) return _decodeList(entity); + dynamic _decode(entity) { + if (entity is Iterable) return _decodeList(entity as List); - if (entity is Map) return _decodeMap(entity); + if (entity is Map) return _decodeMap(entity as Map); return entity; } From cc2da20861ebef4ef636d1a83180ece999d3d920 Mon Sep 17 00:00:00 2001 From: Meysam Karimi <31154534+meysam1717@users.noreply.github.com> Date: Mon, 2 May 2022 00:45:03 +1000 Subject: [PATCH 019/171] Add @FiledMap @PartMap @PartFileMap (#335) Co-authored-by: Meysam Karimi --- chopper/lib/src/annotations.dart | 38 ++++++++++++++++++ chopper_generator/lib/src/generator.dart | 50 +++++++++++++++++++++++- 2 files changed, 86 insertions(+), 2 deletions(-) diff --git a/chopper/lib/src/annotations.dart b/chopper/lib/src/annotations.dart index dfc5f96b..1bd3389d 100644 --- a/chopper/lib/src/annotations.dart +++ b/chopper/lib/src/annotations.dart @@ -342,6 +342,18 @@ class Field { const Field([this.name]); } +/// Provides field parameters of a request as [Map]. +/// +/// ```dart +/// @Post(path: '/something') +/// Future fetch(@FieldMap List> query); +/// ``` +/// +@immutable +class FieldMap { + const FieldMap(); +} + /// Defines a multipart request. /// /// ```dart @@ -368,6 +380,19 @@ class Part { const Part([this.name]); } +/// Provides part parameters of a request as [PartValue]. +/// +/// ```dart +/// @Post(path: '/something') +/// @Multipart +/// Future fetch(@PartMap() List query); +/// ``` +/// +@immutable +class PartMap { + const PartMap(); +} + /// Use [PartFile] to define a file field for a [Multipart] request. /// /// ``` @@ -387,5 +412,18 @@ class PartFile { const PartFile([this.name]); } +/// Provides partFile parameters of a request as [PartValueFile]. +/// +/// ```dart +/// @Post(path: '/something') +/// @Multipart +/// Future fetch(@PartFileMap() List query); +/// ``` +/// +@immutable +class PartFileMap { + const PartFileMap(); +} + const multipart = Multipart(); const body = Body(); diff --git a/chopper_generator/lib/src/generator.dart b/chopper_generator/lib/src/generator.dart index ccf42daf..a3ae9880 100644 --- a/chopper_generator/lib/src/generator.dart +++ b/chopper_generator/lib/src/generator.dart @@ -119,8 +119,11 @@ class ChopperGenerator extends GeneratorForAnnotation { final queries = _getAnnotations(m, chopper.Query); final queryMap = _getAnnotation(m, chopper.QueryMap); final fields = _getAnnotations(m, chopper.Field); + final fieldMap = _getAnnotation(m, chopper.FieldMap); final parts = _getAnnotations(m, chopper.Part); + final partMap = _getAnnotation(m, chopper.PartMap); final fileFields = _getAnnotations(m, chopper.PartFile); + final fileFieldMap = _getAnnotation(m, chopper.PartFileMap); final headers = _generateHeaders(m, method!); final url = _generateUrl(method, paths, baseUrl); @@ -190,7 +193,7 @@ class ChopperGenerator extends GeneratorForAnnotation { final methodOptionalBody = getMethodOptionalBody(method); final methodName = getMethodName(method); final methodUrl = getMethodPath(method); - final hasBody = body.isNotEmpty || fields.isNotEmpty; + var hasBody = body.isNotEmpty || fields.isNotEmpty; if (hasBody) { if (body.isNotEmpty) { blocks.add( @@ -203,13 +206,56 @@ class ChopperGenerator extends GeneratorForAnnotation { } } - final hasParts = + final hasFieldMap = fieldMap.isNotEmpty; + if (hasFieldMap) { + if (hasBody) { + blocks.add(refer('$_bodyVar.addAll').call( + [refer(fieldMap.keys.first)], + ).statement); + } else { + blocks.add( + refer(fieldMap.keys.first).assignFinal(_bodyVar).statement, + ); + } + } + + hasBody = hasBody || hasFieldMap; + + var hasParts = multipart == true && (parts.isNotEmpty || fileFields.isNotEmpty); if (hasParts) { blocks.add( _generateList(parts, fileFields).assignFinal(_partsVar).statement); } + final hasPartMap = multipart == true && partMap.isNotEmpty; + if (hasPartMap) { + if (hasParts) { + blocks.add(refer('$_partsVar.addAll').call( + [refer(partMap.keys.first)], + ).statement); + } else { + blocks.add( + refer(partMap.keys.first).assignFinal(_partsVar).statement, + ); + } + } + + final hasFileFilesMap = multipart == true && fileFieldMap.isNotEmpty; + if (hasFileFilesMap) { + if (hasParts || hasPartMap) { + blocks.add(refer('$_partsVar.addAll').call( + [refer(fileFieldMap.keys.first)], + ).statement); + } else { + blocks.add( + refer(fileFieldMap.keys.first).assignFinal(_partsVar).statement, + ); + } + } + + hasParts = hasParts || hasPartMap || hasFileFilesMap; + if (!methodOptionalBody && !hasBody && !hasParts) { _logger.warning( '$methodName $methodUrl\n' From 659b9f81a5d3ea36e27e3075771a1dd00daba555 Mon Sep 17 00:00:00 2001 From: Ivan Terekhin Date: Mon, 20 Jun 2022 11:58:01 +0300 Subject: [PATCH 020/171] Upgrade of analyzer (#340) --- chopper_generator/CHANGELOG.md | 4 ++++ chopper_generator/pubspec.yaml | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/chopper_generator/CHANGELOG.md b/chopper_generator/CHANGELOG.md index 41173021..cec3211d 100644 --- a/chopper_generator/CHANGELOG.md +++ b/chopper_generator/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 4.0.3 + +- Analyzer dependency upgrade + ## 4.0.2 - Analyzer dependency upgrade diff --git a/chopper_generator/pubspec.yaml b/chopper_generator/pubspec.yaml index a88b8b97..74edf66b 100644 --- a/chopper_generator/pubspec.yaml +++ b/chopper_generator/pubspec.yaml @@ -1,6 +1,6 @@ name: chopper_generator description: Chopper is an http client generator using source_gen, inspired by Retrofit -version: 4.0.2 +version: 4.0.3 documentation: https://hadrien-lejard.gitbook.io/chopper repository: https://github.com/lejard-h/chopper @@ -8,7 +8,7 @@ environment: sdk: ">=2.12.0 <3.0.0" dependencies: - analyzer: ^3.0.0 + analyzer: ^4.1.0 build: ^2.0.0 built_collection: ^5.0.0 chopper: ^4.0.0 From e167ba6ed53de55c39f0e002a70fbca759026c62 Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Thu, 14 Jul 2022 17:32:02 +0100 Subject: [PATCH 021/171] Fix nullable QueryMap fails to compile (#344) --- chopper/example/definition.chopper.dart | 4 +- chopper/test/base_test.dart | 159 +++++++++++++++++++++++ chopper/test/test_service.chopper.dart | 40 +++++- chopper/test/test_service.dart | 19 +++ chopper_generator/lib/src/generator.dart | 24 +++- 5 files changed, 237 insertions(+), 9 deletions(-) diff --git a/chopper/example/definition.chopper.dart b/chopper/example/definition.chopper.dart index 0a26d4a8..f43a754d 100644 --- a/chopper/example/definition.chopper.dart +++ b/chopper/example/definition.chopper.dart @@ -6,7 +6,7 @@ part of 'definition.dart'; // ChopperGenerator // ************************************************************************** -// ignore_for_file: always_put_control_body_on_new_line, always_specify_types, prefer_const_declarations +// ignore_for_file: always_put_control_body_on_new_line, always_specify_types, prefer_const_declarations, unnecessary_brace_in_string_interps class _$MyService extends MyService { _$MyService([ChopperClient? client]) { if (client == null) return; @@ -18,7 +18,7 @@ class _$MyService extends MyService { @override Future> getResource(String id) { - final $url = '/resources/$id'; + final $url = '/resources/${id}'; final $request = Request('GET', $url, client.baseUrl); return client.send($request); } diff --git a/chopper/test/base_test.dart b/chopper/test/base_test.dart index b591168a..24487c36 100644 --- a/chopper/test/base_test.dart +++ b/chopper/test/base_test.dart @@ -591,6 +591,165 @@ void main() { }); }); + test('Query Map 3', () async { + final httpClient = MockClient((request) async { + expect( + request.url.toString(), + equals('$baseUrl/test/query_map?name=foo&number=1234'), + ); + expect(request.method, equals('GET')); + + return http.Response('get response', 200); + }); + + final chopper = buildClient(httpClient); + final service = chopper.getService(); + + final response = await service.getQueryMapTest3( + name: 'foo', + number: 1234, + ); + + expect(response.body, equals('get response')); + expect(response.statusCode, equals(200)); + + httpClient.close(); + }); + + test('Query Map 4 without QueryMap', () async { + final httpClient = MockClient((request) async { + expect( + request.url.toString(), + equals('$baseUrl/test/query_map?name=foo&number=1234'), + ); + expect(request.method, equals('GET')); + + return http.Response('get response', 200); + }); + + final chopper = buildClient(httpClient); + final service = chopper.getService(); + + final response = await service.getQueryMapTest4( + name: 'foo', + number: 1234, + ); + + expect(response.body, equals('get response')); + expect(response.statusCode, equals(200)); + + httpClient.close(); + }); + + test('Query Map 4 with QueryMap', () async { + final httpClient = MockClient((request) async { + expect( + request.url.toString(), + equals( + '$baseUrl/test/query_map?name=foo&number=1234&filter_1=filter_value_1', + ), + ); + expect(request.method, equals('GET')); + + return http.Response('get response', 200); + }); + + final chopper = buildClient(httpClient); + final service = chopper.getService(); + + final response = await service.getQueryMapTest4( + name: 'foo', + number: 1234, + filters: { + 'filter_1': 'filter_value_1', + }, + ); + + expect(response.body, equals('get response')); + expect(response.statusCode, equals(200)); + + httpClient.close(); + }); + + test( + 'Query Map 4 with QueryMap that overwrites a previous value from Query', + () async { + final httpClient = MockClient((request) async { + expect( + request.url.toString(), + equals('$baseUrl/test/query_map?name=bar&number=1234'), + ); + expect(request.method, equals('GET')); + + return http.Response('get response', 200); + }); + + final chopper = buildClient(httpClient); + final service = chopper.getService(); + + final response = await service.getQueryMapTest4( + name: 'foo', + number: 1234, + filters: { + 'name': 'bar', + }, + ); + + expect(response.body, equals('get response')); + expect(response.statusCode, equals(200)); + + httpClient.close(); + }, + ); + + test('Query Map 5 without QueryMap', () async { + final httpClient = MockClient((request) async { + expect( + request.url.toString(), + equals('$baseUrl/test/query_map'), + ); + expect(request.method, equals('GET')); + + return http.Response('get response', 200); + }); + + final chopper = buildClient(httpClient); + final service = chopper.getService(); + + final response = await service.getQueryMapTest5(); + + expect(response.body, equals('get response')); + expect(response.statusCode, equals(200)); + + httpClient.close(); + }); + + test('Query Map 5 with QueryMap', () async { + final httpClient = MockClient((request) async { + expect( + request.url.toString(), + equals('$baseUrl/test/query_map?filter_1=filter_value_1'), + ); + expect(request.method, equals('GET')); + + return http.Response('get response', 200); + }); + + final chopper = buildClient(httpClient); + final service = chopper.getService(); + + final response = await service.getQueryMapTest5( + filters: { + 'filter_1': 'filter_value_1', + }, + ); + + expect(response.body, equals('get response')); + expect(response.statusCode, equals(200)); + + httpClient.close(); + }); + test('onRequest Stream', () async { final client = MockClient((http.Request req) async { return http.Response('ok', 200); diff --git a/chopper/test/test_service.chopper.dart b/chopper/test/test_service.chopper.dart index 2d03d152..f1fd6769 100644 --- a/chopper/test/test_service.chopper.dart +++ b/chopper/test/test_service.chopper.dart @@ -6,7 +6,7 @@ part of 'test_service.dart'; // ChopperGenerator // ************************************************************************** -// ignore_for_file: always_put_control_body_on_new_line, always_specify_types, prefer_const_declarations +// ignore_for_file: always_put_control_body_on_new_line, always_specify_types, prefer_const_declarations, unnecessary_brace_in_string_interps class _$HttpTestService extends HttpTestService { _$HttpTestService([ChopperClient? client]) { if (client == null) return; @@ -18,7 +18,7 @@ class _$HttpTestService extends HttpTestService { @override Future> getTest(String id, {required String dynamicHeader}) { - final $url = '/test/get/$id'; + final $url = '/test/get/${id}'; final $headers = { 'test': dynamicHeader, }; @@ -93,6 +93,36 @@ class _$HttpTestService extends HttpTestService { return client.send($request); } + @override + Future> getQueryMapTest3( + {String name = '', + int? number, + Map filters = const {}}) { + final $url = '/test/query_map'; + final $params = {'name': name, 'number': number}; + $params.addAll(filters); + final $request = Request('GET', $url, client.baseUrl, parameters: $params); + return client.send($request); + } + + @override + Future> getQueryMapTest4( + {String name = '', int? number, Map? filters}) { + final $url = '/test/query_map'; + final $params = {'name': name, 'number': number}; + $params.addAll(filters ?? {}); + final $request = Request('GET', $url, client.baseUrl, parameters: $params); + return client.send($request); + } + + @override + Future> getQueryMapTest5({Map? filters}) { + final $url = '/test/query_map'; + final $params = filters ?? {}; + final $request = Request('GET', $url, client.baseUrl, parameters: $params); + return client.send($request); + } + @override Future> getBody(dynamic body) { final $url = '/test/get_body'; @@ -119,7 +149,7 @@ class _$HttpTestService extends HttpTestService { @override Future> putTest(String test, String data) { - final $url = '/test/put/$test'; + final $url = '/test/put/${test}'; final $body = data; final $request = Request('PUT', $url, client.baseUrl, body: $body); return client.send($request); @@ -127,7 +157,7 @@ class _$HttpTestService extends HttpTestService { @override Future> deleteTest(String id) { - final $url = '/test/delete/$id'; + final $url = '/test/delete/${id}'; final $headers = { 'foo': 'bar', }; @@ -138,7 +168,7 @@ class _$HttpTestService extends HttpTestService { @override Future> patchTest(String id, String data) { - final $url = '/test/patch/$id'; + final $url = '/test/patch/${id}'; final $body = data; final $request = Request('PATCH', $url, client.baseUrl, body: $body); return client.send($request); diff --git a/chopper/test/test_service.dart b/chopper/test/test_service.dart index 1c68866a..fa137441 100644 --- a/chopper/test/test_service.dart +++ b/chopper/test/test_service.dart @@ -48,6 +48,25 @@ abstract class HttpTestService extends ChopperService { @Query('test') bool? test, }); + @Get(path: 'query_map') + Future getQueryMapTest3({ + @Query('name') String name = '', + @Query('number') int? number, + @QueryMap() Map filters = const {}, + }); + + @Get(path: 'query_map') + Future getQueryMapTest4({ + @Query('name') String name = '', + @Query('number') int? number, + @QueryMap() Map? filters, + }); + + @Get(path: 'query_map') + Future getQueryMapTest5({ + @QueryMap() Map? filters, + }); + @Get(path: 'get_body') Future getBody(@Body() dynamic body); diff --git a/chopper_generator/lib/src/generator.dart b/chopper_generator/lib/src/generator.dart index a3ae9880..ab95ee80 100644 --- a/chopper_generator/lib/src/generator.dart +++ b/chopper_generator/lib/src/generator.dart @@ -10,6 +10,7 @@ import 'package:built_collection/built_collection.dart'; import 'package:dart_style/dart_style.dart'; import 'package:source_gen/source_gen.dart'; + // TODO(lejard_h) Code builder not null safe yet // ignore: import_of_legacy_library_into_null_safe import 'package:code_builder/code_builder.dart'; @@ -171,15 +172,34 @@ class ChopperGenerator extends GeneratorForAnnotation { blocks.add(_generateMap(queries).assignFinal(_parametersVar).statement); } + // Build an iterable of all the parameters that are nullable + final optionalNullableParameters = [ + ...m.parameters.where((p) => p.isOptionalPositional), + ...m.parameters.where((p) => p.isNamed), + ].where((el) => el.type.isNullable).map((el) => el.name); + final hasQueryMap = queryMap.isNotEmpty; if (hasQueryMap) { if (queries.isNotEmpty) { blocks.add(refer('$_parametersVar.addAll').call( - [refer(queryMap.keys.first)], + [ + // Check if the parameter is nullable + optionalNullableParameters.contains(queryMap.keys.first) + ? refer(queryMap.keys.first).ifNullThen(refer('{}')) + : refer(queryMap.keys.first), + ], ).statement); } else { blocks.add( - refer(queryMap.keys.first).assignFinal(_parametersVar).statement, + // Check if the parameter is nullable + optionalNullableParameters.contains(queryMap.keys.first) + ? refer(queryMap.keys.first) + .ifNullThen(refer('{}')) + .assignFinal(_parametersVar) + .statement + : refer(queryMap.keys.first) + .assignFinal(_parametersVar) + .statement, ); } } From f9009ce87e1091ee32e075f6b48b3075c41688d2 Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Wed, 7 Sep 2022 09:01:35 +0100 Subject: [PATCH 022/171] Change return type of decodeJson to FutureOr in order to be able to support compute() (#345) --- chopper/lib/src/interceptor.dart | 29 ++++++++++--------- chopper/test/converter_test.dart | 18 +++++++----- .../lib/chopper_built_value.dart | 13 +++++---- chopper_built_value/test/converter_test.dart | 24 +++++++-------- chopper_generator/pubspec.yaml | 2 +- example/bin/main_built_value.dart | 7 +++-- example/bin/main_json_serializable.dart | 9 +++--- 7 files changed, 56 insertions(+), 46 deletions(-) diff --git a/chopper/lib/src/interceptor.dart b/chopper/lib/src/interceptor.dart index 5c89cbc3..8fb147a4 100644 --- a/chopper/lib/src/interceptor.dart +++ b/chopper/lib/src/interceptor.dart @@ -247,7 +247,7 @@ class JsonConverter implements Converter, ErrorConverter { return request; } - Response decodeJson(Response response) { + FutureOr decodeJson(Response response) async { final supportedContentTypes = [jsonHeaders, jsonApiHeaders]; final contentType = response.headers[contentTypeKey]; @@ -264,7 +264,7 @@ class JsonConverter implements Converter, ErrorConverter { body = utf8.decode(response.bodyBytes); } - body = _tryDecodeJson(body); + body = await tryDecodeJson(body); if (isTypeOf>()) { body = body.cast(); } else if (isTypeOf>()) { @@ -275,11 +275,12 @@ class JsonConverter implements Converter, ErrorConverter { } @override - Response convertResponse(Response response) { - return decodeJson(response) as Response; - } + FutureOr> convertResponse( + Response response) async => + (await decodeJson(response)) as Response; - dynamic _tryDecodeJson(String data) { + @protected + FutureOr tryDecodeJson(String data) { try { return json.decode(data); } catch (e) { @@ -289,14 +290,13 @@ class JsonConverter implements Converter, ErrorConverter { } @override - Response convertError(Response response) => - decodeJson(response); + FutureOr convertError( + Response response) async => + await decodeJson(response); - static Response responseFactory( - Response response, - ) { - return const JsonConverter().convertResponse(response); - } + static FutureOr> responseFactory( + Response response) => + const JsonConverter().convertResponse(response); static Request requestFactory(Request request) { return const JsonConverter().convertRequest(request); @@ -339,7 +339,8 @@ class FormUrlEncodedConverter implements Converter, ErrorConverter { } @override - Response convertResponse(Response response) => + FutureOr> convertResponse( + Response response) => response as Response; @override diff --git a/chopper/test/converter_test.dart b/chopper/test/converter_test.dart index 7bc98b58..5448c472 100644 --- a/chopper/test/converter_test.dart +++ b/chopper/test/converter_test.dart @@ -67,39 +67,41 @@ void main() { group('JsonConverter', () { final jsonConverter = JsonConverter(); - test('decode String', () { + test('decode String', () async { final value = 'foo'; final res = Response(http.Response('"$value"', 200), '"$value"'); - final converted = jsonConverter.convertResponse(res); + final converted = + await jsonConverter.convertResponse(res); expect(converted.body, equals(value)); }); - test('decode List String', () { + test('decode List String', () async { final res = Response( http.Response('["foo","bar"]', 200), '["foo","bar"]', ); final converted = - jsonConverter.convertResponse, String>(res); + await jsonConverter.convertResponse, String>(res); expect(converted.body, equals(['foo', 'bar'])); }); - test('decode List int', () { + test('decode List int', () async { final res = Response(http.Response('[1,2]', 200), '[1,2]'); - final converted = jsonConverter.convertResponse, int>(res); + final converted = + await jsonConverter.convertResponse, int>(res); expect(converted.body, equals([1, 2])); }); - test('decode Map', () { + test('decode Map', () async { final res = Response( http.Response('{"foo":"bar"}', 200), '{"foo":"bar"}', ); final converted = - jsonConverter.convertResponse, String>(res); + await jsonConverter.convertResponse, String>(res); expect(converted.body, equals({'foo': 'bar'})); }); diff --git a/chopper_built_value/lib/chopper_built_value.dart b/chopper_built_value/lib/chopper_built_value.dart index 0468d0a0..47d6a173 100644 --- a/chopper_built_value/lib/chopper_built_value.dart +++ b/chopper_built_value/lib/chopper_built_value.dart @@ -1,7 +1,8 @@ -import 'package:chopper/chopper.dart'; +import 'dart:async'; import 'package:built_collection/built_collection.dart'; import 'package:built_value/serializer.dart'; +import 'package:chopper/chopper.dart'; /// A custom [Converter] and [ErrorConverter] that handles conversion for classes /// having a serializer implementation made with the built_value package. @@ -52,15 +53,17 @@ class BuiltValueConverter implements Converter, ErrorConverter { } @override - Response convertResponse(Response response) { - final jsonResponse = jsonConverter.convertResponse(response); + FutureOr> convertResponse( + Response response) async { + final jsonResponse = await jsonConverter.convertResponse(response); final body = deserialize(jsonResponse.body); return jsonResponse.copyWith(body: body); } @override - Response convertError(Response response) { - final jsonResponse = jsonConverter.convertResponse(response); + FutureOr convertError( + Response response) async { + final jsonResponse = await jsonConverter.convertResponse(response); var body; diff --git a/chopper_built_value/test/converter_test.dart b/chopper_built_value/test/converter_test.dart index f16b4171..2766f645 100644 --- a/chopper_built_value/test/converter_test.dart +++ b/chopper_built_value/test/converter_test.dart @@ -2,8 +2,8 @@ import 'package:built_collection/built_collection.dart'; import 'package:built_value/standard_json_plugin.dart'; import 'package:chopper/chopper.dart'; import 'package:chopper_built_value/chopper_built_value.dart'; -import 'package:test/test.dart'; import 'package:http/http.dart' as http; +import 'package:test/test.dart'; import 'data.dart'; import 'serializers.dart'; @@ -31,31 +31,31 @@ void main() { expect(request.body, '{"\$":"DataModel","id":42,"name":"foo"}'); }); - test('convert response with wireName', () { + test('convert response with wireName', () async { final string = '{"\$":"DataModel","id":42,"name":"foo"}'; final response = Response(http.Response(string, 200), string); final convertedResponse = - converter.convertResponse(response); + await converter.convertResponse(response); expect(convertedResponse.body?.id, equals(42)); expect(convertedResponse.body?.name, equals('foo')); }); - test('convert response without wireName', () { + test('convert response without wireName', () async { final string = '{"id":42,"name":"foo"}'; final response = Response(http.Response(string, 200), string); final convertedResponse = - converter.convertResponse(response); + await converter.convertResponse(response); expect(convertedResponse.body?.id, equals(42)); expect(convertedResponse.body?.name, equals('foo')); }); - test('convert response List', () { + test('convert response List', () async { final string = '[{"id":42,"name":"foo"},{"id":25,"name":"bar"}]'; final response = Response(http.Response(string, 200), string); - final convertedResponse = - converter.convertResponse, DataModel>(response); + final convertedResponse = await converter + .convertResponse, DataModel>(response); final list = convertedResponse.body; expect(list?.first.id, equals(42)); @@ -71,19 +71,19 @@ void main() { expect(request.headers['content-type'], equals('application/json')); }); - test('convert error with wire name', () { + test('convert error with wire name', () async { final string = '{"\$":"DataModel","id":42,"name":"foo"}'; final response = Response(http.Response(string, 200), string); - final convertedResponse = converter.convertError(response); + final convertedResponse = await converter.convertError(response); expect(convertedResponse.body.id, equals(42)); expect(convertedResponse.body.name, equals('foo')); }); - test('convert error using provided type', () { + test('convert error using provided type', () async { final string = '{"message":"Error message"}'; final response = Response(http.Response(string, 200), string); - final convertedResponse = converter.convertError(response); + final convertedResponse = await converter.convertError(response); expect(convertedResponse.body.message, equals('Error message')); }); diff --git a/chopper_generator/pubspec.yaml b/chopper_generator/pubspec.yaml index 74edf66b..ef2adb98 100644 --- a/chopper_generator/pubspec.yaml +++ b/chopper_generator/pubspec.yaml @@ -8,7 +8,7 @@ environment: sdk: ">=2.12.0 <3.0.0" dependencies: - analyzer: ^4.1.0 + analyzer: ">=4.1.0 <4.3.0" build: ^2.0.0 built_collection: ^5.0.0 chopper: ^4.0.0 diff --git a/example/bin/main_built_value.dart b/example/bin/main_built_value.dart index 8ebaff5b..a92630ea 100644 --- a/example/bin/main_built_value.dart +++ b/example/bin/main_built_value.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:built_collection/built_collection.dart'; import 'package:built_value/serializer.dart'; import 'package:chopper/chopper.dart'; @@ -84,9 +86,10 @@ class BuiltValueConverter extends JsonConverter { } @override - Response convertResponse(Response response) { + FutureOr> convertResponse( + Response response) async { // use [JsonConverter] to decode json - final jsonRes = super.convertResponse(response); + final jsonRes = await super.convertResponse(response); final body = _decode(jsonRes.body); return jsonRes.copyWith(body: body); } diff --git a/example/bin/main_json_serializable.dart b/example/bin/main_json_serializable.dart index 0d09e8f8..75851acd 100644 --- a/example/bin/main_json_serializable.dart +++ b/example/bin/main_json_serializable.dart @@ -91,9 +91,10 @@ class JsonSerializableConverter extends JsonConverter { } @override - Response convertResponse(Response response) { + FutureOr> convertResponse( + Response response) async { // use [JsonConverter] to decode json - final jsonRes = super.convertResponse(response); + final jsonRes = await super.convertResponse(response); return jsonRes.copyWith(body: _decode(jsonRes.body)); } @@ -102,9 +103,9 @@ class JsonSerializableConverter extends JsonConverter { // all objects should implements toJson method Request convertRequest(Request request) => super.convertRequest(request); - Response convertError(Response response) { + FutureOr convertError(Response response) async { // use [JsonConverter] to decode json - final jsonRes = super.convertError(response); + final jsonRes = await super.convertError(response); return jsonRes.copyWith( body: ResourceError.fromJsonFactory(jsonRes.body), From 5f2eb829fe13ccc11569dcaf20ddb86b62d9304a Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Wed, 7 Sep 2022 16:26:29 +0100 Subject: [PATCH 023/171] Migrate from pedantic to lints ^2.0.0 with lints/recommended.yaml (#349) --- chopper/analysis_options.yaml | 35 +- chopper/example/definition.dart | 1 + chopper/example/main.dart | 3 +- chopper/lib/chopper.dart | 2 +- chopper/lib/src/annotations.dart | 96 ++--- chopper/lib/src/authenticator.dart | 7 +- chopper/lib/src/base.dart | 82 ++-- chopper/lib/src/constants.dart | 10 +- chopper/lib/src/interceptor.dart | 94 +++-- chopper/lib/src/request.dart | 83 ++-- chopper/lib/src/response.dart | 2 +- chopper/lib/src/utils.dart | 24 +- chopper/pubspec.yaml | 5 +- chopper/test/base_test.dart | 45 ++- chopper/test/client_test.dart | 7 +- chopper/test/converter_test.dart | 19 +- chopper/test/form_test.dart | 29 +- chopper/test/interceptors_test.dart | 30 +- chopper/test/json_test.dart | 12 +- chopper/test/multipart_test.dart | 87 ++-- chopper/test/test_service.dart | 7 +- chopper_built_value/analysis_options.yaml | 35 +- .../lib/chopper_built_value.dart | 36 +- chopper_built_value/pubspec.yaml | 5 +- chopper_built_value/test/data.g.dart | 40 +- chopper_built_value/test/serializers.dart | 1 + chopper_built_value/test/serializers.g.dart | 2 +- chopper_generator/analysis_options.yaml | 36 +- chopper_generator/lib/chopper_generator.dart | 1 + chopper_generator/lib/src/generator.dart | 377 +++++++++--------- chopper_generator/pubspec.yaml | 9 +- example/analysis_options.yaml | 32 ++ example/bin/main_built_value.dart | 41 +- example/bin/main_json_serializable.dart | 28 +- example/lib/built_value_resource.dart | 25 +- example/lib/built_value_resource.g.dart | 46 ++- example/lib/built_value_serializers.dart | 3 +- example/lib/built_value_serializers.g.dart | 2 +- example/lib/json_serializable.dart | 18 +- example/pubspec.lock | 93 ++++- example/pubspec.yaml | 6 +- 41 files changed, 876 insertions(+), 640 deletions(-) create mode 100644 example/analysis_options.yaml diff --git a/chopper/analysis_options.yaml b/chopper/analysis_options.yaml index d4fcc1ad..7f5a674f 100644 --- a/chopper/analysis_options.yaml +++ b/chopper/analysis_options.yaml @@ -1 +1,34 @@ -include: package:pedantic/analysis_options.yaml \ No newline at end of file +include: package:lints/recommended.yaml + +analyzer: + exclude: + - "**.g.dart" + - "**.chopper.dart" + - "**.mocks.dart" + - "example/**" + plugins: + - dart_code_metrics + +dart_code_metrics: + metrics: + cyclomatic-complexity: 20 + number-of-arguments: 4 + maximum-nesting-level: 5 + number-of-parameters: 7 + metrics-exclude: + - test/** + rules: + - newline-before-return + - no-boolean-literal-compare + - no-empty-block + - prefer-trailing-comma + - prefer-conditional-expressions + - no-equal-then-else + anti-patterns: + - long-method + - long-parameter-list + +linter: + rules: + avoid_print: true + prefer_single_quotes: true diff --git a/chopper/example/definition.dart b/chopper/example/definition.dart index 5103ca4d..ef8fcf9d 100644 --- a/chopper/example/definition.dart +++ b/chopper/example/definition.dart @@ -1,4 +1,5 @@ import 'dart:async'; + import 'package:chopper/chopper.dart'; part 'definition.chopper.dart'; diff --git a/chopper/example/main.dart b/chopper/example/main.dart index d1831859..16f66abc 100644 --- a/chopper/example/main.dart +++ b/chopper/example/main.dart @@ -1,4 +1,5 @@ import 'package:chopper/chopper.dart'; + import 'definition.dart'; Future main() async { @@ -6,7 +7,7 @@ Future main() async { baseUrl: 'http://localhost:8000', services: [ // the generated service - MyService.create(ChopperClient()) + MyService.create(ChopperClient()), ], converter: JsonConverter(), ); diff --git a/chopper/lib/chopper.dart b/chopper/lib/chopper.dart index 9d1d9826..c004d675 100644 --- a/chopper/lib/chopper.dart +++ b/chopper/lib/chopper.dart @@ -6,8 +6,8 @@ library chopper; export 'src/annotations.dart'; export 'src/authenticator.dart'; export 'src/base.dart'; +export 'src/constants.dart'; export 'src/interceptor.dart'; export 'src/request.dart'; export 'src/response.dart'; export 'src/utils.dart' hide mapToQuery; -export 'src/constants.dart'; diff --git a/chopper/lib/src/annotations.dart b/chopper/lib/src/annotations.dart index 1bd3389d..dafed63b 100644 --- a/chopper/lib/src/annotations.dart +++ b/chopper/lib/src/annotations.dart @@ -1,8 +1,10 @@ import 'dart:async'; + import 'package:meta/meta.dart'; + +import 'constants.dart'; import 'request.dart'; import 'response.dart'; -import 'constants.dart'; /// Defines a Chopper API. /// @@ -171,15 +173,10 @@ class Method { @immutable class Get extends Method { const Get({ - bool optionalBody = true, - String path = '', - Map headers = const {}, - }) : super( - HttpMethod.Get, - optionalBody: optionalBody, - path: path, - headers: headers, - ); + super.optionalBody = true, + super.path, + super.headers, + }) : super(HttpMethod.Get); } /// Defines a method as an HTTP POST request. @@ -188,30 +185,20 @@ class Get extends Method { @immutable class Post extends Method { const Post({ - bool optionalBody = false, - String path = '', - Map headers = const {}, - }) : super( - HttpMethod.Post, - optionalBody: optionalBody, - path: path, - headers: headers, - ); + super.optionalBody, + super.path, + super.headers, + }) : super(HttpMethod.Post); } /// Defines a method as an HTTP DELETE request. @immutable class Delete extends Method { const Delete({ - bool optionalBody = true, - String path = '', - Map headers = const {}, - }) : super( - HttpMethod.Delete, - optionalBody: optionalBody, - path: path, - headers: headers, - ); + super.optionalBody = true, + super.path, + super.headers, + }) : super(HttpMethod.Delete); } /// Defines a method as an HTTP PUT request. @@ -220,15 +207,10 @@ class Delete extends Method { @immutable class Put extends Method { const Put({ - bool optionalBody = false, - String path = '', - Map headers = const {}, - }) : super( - HttpMethod.Put, - optionalBody: optionalBody, - path: path, - headers: headers, - ); + super.optionalBody, + super.path, + super.headers, + }) : super(HttpMethod.Put); } /// Defines a method as an HTTP PATCH request. @@ -236,44 +218,29 @@ class Put extends Method { @immutable class Patch extends Method { const Patch({ - bool optionalBody = false, - String path = '', - Map headers = const {}, - }) : super( - HttpMethod.Patch, - optionalBody: optionalBody, - path: path, - headers: headers, - ); + super.optionalBody, + super.path, + super.headers, + }) : super(HttpMethod.Patch); } /// Defines a method as an HTTP HEAD request. @immutable class Head extends Method { const Head({ - bool optionalBody = true, - String path = '', - Map headers = const {}, - }) : super( - HttpMethod.Head, - optionalBody: optionalBody, - path: path, - headers: headers, - ); + super.optionalBody = true, + super.path, + super.headers, + }) : super(HttpMethod.Head); } @immutable class Options extends Method { const Options({ - bool optionalBody = true, - String path = '', - Map headers = const {}, - }) : super( - HttpMethod.Options, - optionalBody: optionalBody, - path: path, - headers: headers, - ); + super.optionalBody = true, + super.path, + super.headers, + }) : super(HttpMethod.Options); } /// A function that should convert the body of a [Request] to the HTTP representation. @@ -377,6 +344,7 @@ class Multipart { @immutable class Part { final String? name; + const Part([this.name]); } diff --git a/chopper/lib/src/authenticator.dart b/chopper/lib/src/authenticator.dart index db225a49..d69e6d76 100644 --- a/chopper/lib/src/authenticator.dart +++ b/chopper/lib/src/authenticator.dart @@ -5,6 +5,9 @@ import 'package:chopper/chopper.dart'; /// This method should return a [Request] that includes credentials to satisfy an authentication challenge received in /// [response]. It should return `null` if the challenge cannot be satisfied. abstract class Authenticator { - FutureOr authenticate(Request request, Response response, - [Request? originalRequest]); + FutureOr authenticate( + Request request, + Response response, [ + Request? originalRequest, + ]); } diff --git a/chopper/lib/src/base.dart b/chopper/lib/src/base.dart index b0f90848..3fa4e998 100644 --- a/chopper/lib/src/base.dart +++ b/chopper/lib/src/base.dart @@ -1,19 +1,20 @@ import 'dart:async'; -import 'package:meta/meta.dart'; + import 'package:http/http.dart' as http; -import 'constants.dart'; +import 'package:meta/meta.dart'; +import 'annotations.dart'; +import 'authenticator.dart'; +import 'constants.dart'; import 'interceptor.dart'; import 'request.dart'; import 'response.dart'; -import 'annotations.dart'; -import 'authenticator.dart'; import 'utils.dart'; Type _typeOf() => T; @visibleForTesting -final allowedInterceptorsType = [ +final List allowedInterceptorsType = [ RequestInterceptor, RequestInterceptorFunc, ResponseInterceptor, @@ -120,7 +121,7 @@ class ChopperClient { Iterable services = const [], }) : httpClient = client ?? http.Client(), _clientIsInternal = client == null { - if (interceptors.every(_isAnInterceptor) == false) { + if (!interceptors.every(_isAnInterceptor)) { throw ArgumentError( 'Unsupported type for interceptors, it only support the following types:\n' '${allowedInterceptorsType.join('\n - ')}', @@ -162,31 +163,28 @@ class ChopperClient { /// final todoService = chopper.getService(); /// ``` ServiceType getService() { - final serviceType = _typeOf(); + final Type serviceType = _typeOf(); if (serviceType == dynamic || serviceType == ChopperService) { throw Exception( - 'Service type should be provided, `dynamic` is not allowed.'); + 'Service type should be provided, `dynamic` is not allowed.', + ); } - final service = _services[serviceType]; + final ChopperService? service = _services[serviceType]; if (service == null) { throw Exception('Service of type \'$serviceType\' not found.'); } + return service as ServiceType; } - Future _encodeRequest(Request request) async { - return converter?.convertRequest(request) ?? request; - } + Future _encodeRequest(Request request) async => + converter?.convertRequest(request) ?? request; Future> _decodeResponse( Response response, Converter withConverter, - ) async { - final converted = - await withConverter.convertResponse(response); - - return converted; - } + ) async => + await withConverter.convertResponse(response); Future _interceptRequest(Request req) async { final body = req.body; @@ -203,6 +201,7 @@ class ChopperClient { 'Interceptors should not transform the body of the request' 'Use Request converter instead', ); + return req; } @@ -242,11 +241,7 @@ class ChopperClient { error = errorRes?.error ?? errorRes?.body; } - return Response( - response.base, - null, - error: error, - ); + return Response(response.base, null, error: error); } Future> _handleSuccessResponse( @@ -269,17 +264,12 @@ class ChopperClient { Future _handleRequestConverter( Request request, ConvertRequest? requestConverter, - ) async { - if (request.body != null || request.parts.isNotEmpty) { - if (requestConverter != null) { - request = await requestConverter(request); - } else { - request = (await _encodeRequest(request)); - } - } - - return request; - } + ) async => + request.body != null || request.parts.isNotEmpty + ? requestConverter != null + ? await requestConverter(request) + : await _encodeRequest(request) + : request; /// Sends a pre-build [Request], applying all provided [Interceptor]s and /// [Converter]s. @@ -298,8 +288,9 @@ class ChopperClient { ConvertRequest? requestConverter, ConvertResponse? responseConverter, }) async { - var req = await _handleRequestConverter(request, requestConverter); - req = await _interceptRequest(req); + var req = await _interceptRequest( + await _handleRequestConverter(request, requestConverter), + ); _requestController.add(req); final streamRes = await httpClient.send(await req.toBaseRequest()); @@ -324,27 +315,28 @@ class ChopperClient { return _processResponse(res); } else { res = await _handleErrorResponse(res); + return _processResponse(res); } } } - if (_responseIsSuccessful(res.statusCode)) { - res = await _handleSuccessResponse( - res, - responseConverter, - ); - } else { - res = await _handleErrorResponse(res); - } + res = _responseIsSuccessful(res.statusCode) + ? await _handleSuccessResponse( + res, + responseConverter, + ) + : await _handleErrorResponse(res); return _processResponse(res); } Future> _processResponse( - dynamic res) async { + dynamic res, + ) async { res = await _interceptResponse(res); _responseController.add(res); + return res; } diff --git a/chopper/lib/src/constants.dart b/chopper/lib/src/constants.dart index 21f388aa..e7e8faec 100644 --- a/chopper/lib/src/constants.dart +++ b/chopper/lib/src/constants.dart @@ -1,9 +1,11 @@ -const contentTypeKey = 'content-type'; -const jsonHeaders = 'application/json'; -const formEncodedHeaders = 'application/x-www-form-urlencoded'; +// ignore_for_file: constant_identifier_names + +const String contentTypeKey = 'content-type'; +const String jsonHeaders = 'application/json'; +const String formEncodedHeaders = 'application/x-www-form-urlencoded'; // Represent the header for a json api response https://jsonapi.org/#mime-types -const jsonApiHeaders = 'application/vnd.api+json'; +const String jsonApiHeaders = 'application/vnd.api+json'; class HttpMethod { static const String Get = 'GET'; diff --git a/chopper/lib/src/interceptor.dart b/chopper/lib/src/interceptor.dart index 8fb147a4..9d393afc 100644 --- a/chopper/lib/src/interceptor.dart +++ b/chopper/lib/src/interceptor.dart @@ -1,12 +1,13 @@ import 'dart:async'; import 'dart:convert'; -import 'package:meta/meta.dart'; + import 'package:http/http.dart' as http; +import 'package:meta/meta.dart'; +import 'constants.dart'; import 'request.dart'; import 'response.dart'; import 'utils.dart'; -import 'constants.dart'; /// An interface for implementing response interceptors. /// @@ -136,14 +137,11 @@ typedef RequestInterceptorFunc = FutureOr Function(Request request); class CurlInterceptor implements RequestInterceptor { @override Future onRequest(Request request) async { - final baseRequest = await request.toBaseRequest(); - final method = baseRequest.method; - final url = baseRequest.url.toString(); - final headers = baseRequest.headers; - var curl = ''; - curl += 'curl'; - curl += ' -v'; - curl += ' -X $method'; + final http.BaseRequest baseRequest = await request.toBaseRequest(); + final String method = baseRequest.method; + final String url = baseRequest.url.toString(); + final Map headers = baseRequest.headers; + String curl = 'curl -v -X $method'; headers.forEach((k, v) { curl += ' -H \'$k: $v\''; }); @@ -154,8 +152,9 @@ class CurlInterceptor implements RequestInterceptor { curl += ' -d \'$body\''; } } - curl += ' \"$url\"'; + curl += ' "$url"'; chopperLogger.info(curl); + return request; } } @@ -172,11 +171,11 @@ class HttpLoggingInterceptor implements RequestInterceptor, ResponseInterceptor { @override FutureOr onRequest(Request request) async { - final base = await request.toBaseRequest(); + final http.BaseRequest base = await request.toBaseRequest(); chopperLogger.info('--> ${base.method} ${base.url}'); base.headers.forEach((k, v) => chopperLogger.info('$k: $v')); - var bytes = ''; + String bytes = ''; if (base is http.Request) { final body = base.body; if (body.isNotEmpty) { @@ -186,17 +185,18 @@ class HttpLoggingInterceptor } chopperLogger.info('--> END ${base.method}$bytes'); + return request; } @override FutureOr onResponse(Response response) { - final base = response.base.request; + final http.BaseRequest? base = response.base.request; chopperLogger.info('<-- ${response.statusCode} ${base!.url}'); response.base.headers.forEach((k, v) => chopperLogger.info('$k: $v')); - var bytes; + String bytes = ''; if (response.base is http.Response) { final resp = response.base as http.Response; if (resp.body.isNotEmpty) { @@ -206,6 +206,7 @@ class HttpLoggingInterceptor } chopperLogger.info('--> END ${base.method}$bytes'); + return response; } } @@ -228,29 +229,27 @@ class JsonConverter implements Converter, ErrorConverter { const JsonConverter(); @override - Request convertRequest(Request request) { - final req = applyHeader( - request, - contentTypeKey, - jsonHeaders, - override: false, - ); - - return encodeJson(req); - } + Request convertRequest(Request request) => encodeJson( + applyHeader( + request, + contentTypeKey, + jsonHeaders, + override: false, + ), + ); Request encodeJson(Request request) { - var contentType = request.headers[contentTypeKey]; - if (contentType != null && contentType.contains(jsonHeaders)) { - return request.copyWith(body: json.encode(request.body)); - } - return request; + final String? contentType = request.headers[contentTypeKey]; + + return (contentType?.contains(jsonHeaders) ?? false) + ? request.copyWith(body: json.encode(request.body)) + : request; } FutureOr decodeJson(Response response) async { - final supportedContentTypes = [jsonHeaders, jsonApiHeaders]; + final List supportedContentTypes = [jsonHeaders, jsonApiHeaders]; - final contentType = response.headers[contentTypeKey]; + final String? contentType = response.headers[contentTypeKey]; var body = response.body; if (supportedContentTypes.contains(contentType)) { @@ -276,7 +275,8 @@ class JsonConverter implements Converter, ErrorConverter { @override FutureOr> convertResponse( - Response response) async => + Response response, + ) async => (await decodeJson(response)) as Response; @protected @@ -285,22 +285,24 @@ class JsonConverter implements Converter, ErrorConverter { return json.decode(data); } catch (e) { chopperLogger.warning(e); + return data; } } @override FutureOr convertError( - Response response) async => + Response response, + ) async => await decodeJson(response); static FutureOr> responseFactory( - Response response) => + Response response, + ) => const JsonConverter().convertResponse(response); - static Request requestFactory(Request request) { - return const JsonConverter().convertRequest(request); - } + static Request requestFactory(Request request) => + const JsonConverter().convertRequest(request); } /// A [Converter] implementation that converts only [Request]s having a [Map] as their body. @@ -314,7 +316,7 @@ class FormUrlEncodedConverter implements Converter, ErrorConverter { @override Request convertRequest(Request request) { - var req = applyHeader( + final Request req = applyHeader( request, contentTypeKey, formEncodedHeaders, @@ -324,15 +326,10 @@ class FormUrlEncodedConverter implements Converter, ErrorConverter { if (req.body is Map) return req; if (req.body is Map) { - final body = {}; - - req.body.forEach((key, val) { - if (val != null) { - body[key.toString()] = val.toString(); - } + return req.copyWith(body: { + for (final MapEntry e in req.body.entries) + if (e.value != null) e.key.toString(): e.value.toString(), }); - - req = req.copyWith(body: body); } return req; @@ -340,7 +337,8 @@ class FormUrlEncodedConverter implements Converter, ErrorConverter { @override FutureOr> convertResponse( - Response response) => + Response response, + ) => response as Response; @override diff --git a/chopper/lib/src/request.dart b/chopper/lib/src/request.dart index 88a7ed05..8378e276 100644 --- a/chopper/lib/src/request.dart +++ b/chopper/lib/src/request.dart @@ -1,10 +1,11 @@ import 'dart:async'; import 'dart:convert'; -import 'package:meta/meta.dart'; import 'package:http/http.dart' as http; -import 'utils.dart'; +import 'package:meta/meta.dart'; + import 'constants.dart'; +import 'utils.dart'; /// This class represents an HTTP request that can be made with Chopper. @immutable @@ -54,7 +55,7 @@ class Request { Uri _buildUri() => buildUri(baseUrl, url, parameters); - Map _buildHeaders() => Map.from(headers); + Map _buildHeaders() => {...headers}; /// Converts this Chopper Request into a [http.BaseRequest]. /// @@ -65,32 +66,18 @@ class Request { /// - [http.MultipartRequest] if [multipart] is true /// - or a [http.Request] Future toBaseRequest() async { - final uri = _buildUri(); - final heads = _buildHeaders(); + final Uri uri = _buildUri(); + final Map heads = _buildHeaders(); if (body is Stream>) { - return toStreamedRequest( - body, - method, - uri, - heads, - ); + return toStreamedRequest(body, method, uri, heads); } if (multipart) { - return toMultipartRequest( - parts, - method, - uri, - heads, - ); + return toMultipartRequest(parts, method, uri, heads); } - return toHttpRequest( - body, - method, - uri, - heads, - ); + + return toHttpRequest(body, method, uri, heads); } } @@ -117,33 +104,30 @@ class PartValue { /// Represents a file part in a multipart request. @immutable class PartValueFile extends PartValue { - PartValueFile(String name, T value) : super(name, value); + const PartValueFile(super.name, super.value); } /// Builds a valid URI from [baseUrl], [url] and [parameters]. /// /// If [url] starts with 'http://' or 'https://', baseUrl is ignored. Uri buildUri(String baseUrl, String url, Map parameters) { - var uri; - if (url.startsWith('http://') || url.startsWith('https://')) { - // If the request's url is already a fully qualified URL, we can use it - // as-is and ignore the baseUrl. - uri = Uri.parse(url); - } else { - if (!baseUrl.endsWith('/') && !url.startsWith('/')) { - uri = Uri.parse('$baseUrl/$url'); - } else { - uri = Uri.parse('$baseUrl$url'); - } - } - - var query = mapToQuery(parameters); + // If the request's url is already a fully qualified URL, we can use it + // as-is and ignore the baseUrl. + Uri uri = url.startsWith('http://') || url.startsWith('https://') + ? Uri.parse(url) + : !baseUrl.endsWith('/') && !url.startsWith('/') + ? Uri.parse('$baseUrl/$url') + : Uri.parse('$baseUrl$url'); + + String query = mapToQuery(parameters); if (query.isNotEmpty) { if (uri.hasQuery) { query += '&${uri.query}'; } + return uri.replace(query: query); } + return uri; } @@ -154,8 +138,8 @@ Future toHttpRequest( Uri uri, Map headers, ) async { - final baseRequest = http.Request(method, uri); - baseRequest.headers.addAll(headers); + final http.Request baseRequest = http.Request(method, uri) + ..headers.addAll(headers); if (body != null) { if (body is String) { @@ -168,6 +152,7 @@ Future toHttpRequest( throw ArgumentError.value('$body', 'body'); } } + return baseRequest; } @@ -178,10 +163,10 @@ Future toMultipartRequest( Uri uri, Map headers, ) async { - final baseRequest = http.MultipartRequest(method, uri); - baseRequest.headers.addAll(headers); + final http.MultipartRequest baseRequest = http.MultipartRequest(method, uri) + ..headers.addAll(headers); - for (final part in parts) { + for (final PartValue part in parts) { if (part.value == null) continue; if (part.value is http.MultipartFile) { @@ -210,6 +195,7 @@ Future toMultipartRequest( baseRequest.fields[part.name] = part.value.toString(); } } + return baseRequest; } @@ -220,11 +206,14 @@ Future toStreamedRequest( Uri uri, Map headers, ) async { - final req = http.StreamedRequest(method, uri); - req.headers.addAll(headers); + final http.StreamedRequest req = http.StreamedRequest(method, uri) + ..headers.addAll(headers); - bodyStream.listen(req.sink.add, - onDone: req.sink.close, onError: req.sink.addError); + bodyStream.listen( + req.sink.add, + onDone: req.sink.close, + onError: req.sink.addError, + ); return req; } diff --git a/chopper/lib/src/response.dart b/chopper/lib/src/response.dart index e4d9a56f..1fc29fac 100644 --- a/chopper/lib/src/response.dart +++ b/chopper/lib/src/response.dart @@ -29,7 +29,7 @@ class Response { /// The body of the response if [isSuccessful] is false. final Object? error; - Response(this.base, this.body, {this.error}); + const Response(this.base, this.body, {this.error}); /// Makes a copy of this Response, replacing original values with the given ones. /// This method can also alter the type of the response body. diff --git a/chopper/lib/src/utils.dart b/chopper/lib/src/utils.dart index 21b0605f..e4d17f7d 100644 --- a/chopper/lib/src/utils.dart +++ b/chopper/lib/src/utils.dart @@ -39,16 +39,16 @@ Request applyHeaders( Map headers, { bool override = true, }) { - final h = Map.from(request.headers); + final Map headersCopy = {...request.headers}; - for (var k in headers.keys) { - var val = headers[k]; - if (val == null) continue; - if (!override && h.containsKey(k)) continue; - h[k] = val; + for (String key in headers.keys) { + String? value = headers[key]; + if (value == null) continue; + if (!override && headersCopy.containsKey(key)) continue; + headersCopy[key] = value; } - return request.copyWith(headers: h); + return request.copyWith(headers: headersCopy); } final chopperLogger = Logger('Chopper'); @@ -62,12 +62,11 @@ Iterable<_Pair> _mapToQuery( Map map, { String? prefix, }) { - /// ignore: prefer_collection_literals - final pairs = Set<_Pair>(); + final Set<_Pair> pairs = {}; map.forEach((key, value) { if (value != null) { - var name = Uri.encodeQueryComponent(key); + String name = Uri.encodeQueryComponent(key); if (prefix != null) { name = '$prefix.$name'; @@ -77,11 +76,12 @@ Iterable<_Pair> _mapToQuery( pairs.addAll(_iterableToQuery(name, value)); } else if (value is Map) { pairs.addAll(_mapToQuery(value, prefix: name)); - } else if (value.toString().isNotEmpty == true) { + } else if (value.toString().isNotEmpty) { pairs.add(_Pair(name, _normalizeValue(value))); } } }); + return pairs; } @@ -97,7 +97,7 @@ class _Pair { final A first; final B second; - _Pair(this.first, this.second); + const _Pair(this.first, this.second); @override String toString() => '$first=$second'; diff --git a/chopper/pubspec.yaml b/chopper/pubspec.yaml index d2e41a97..cff2ec03 100644 --- a/chopper/pubspec.yaml +++ b/chopper/pubspec.yaml @@ -5,7 +5,7 @@ documentation: https://hadrien-lejard.gitbook.io/chopper repository: https://github.com/lejard-h/chopper environment: - sdk: ">=2.12.0 <3.0.0" + sdk: ">=2.17.0 <3.0.0" dependencies: http: ">=0.13.0 <1.0.0" @@ -18,6 +18,7 @@ dev_dependencies: build_test: ^2.0.0 coverage: ^1.0.2 http_parser: ^4.0.0 - pedantic: ^1.11.0 + dart_code_metrics: ^4.8.1 + lints: ^2.0.0 chopper_generator: path: ../chopper_generator diff --git a/chopper/test/base_test.dart b/chopper/test/base_test.dart index 24487c36..99d81231 100644 --- a/chopper/test/base_test.dart +++ b/chopper/test/base_test.dart @@ -2,16 +2,19 @@ import 'dart:async'; import 'dart:convert'; import 'package:chopper/chopper.dart'; -import 'package:test/test.dart'; -import 'package:http/testing.dart'; import 'package:http/http.dart' as http; +import 'package:http/testing.dart'; +import 'package:test/test.dart'; + import 'test_service.dart'; const baseUrl = 'http://localhost:8000'; void main() { - final buildClient = ( - [http.Client? httpClient, ErrorConverter? errorConverter]) => + ChopperClient buildClient([ + http.Client? httpClient, + ErrorConverter? errorConverter, + ]) => ChopperClient( baseUrl: baseUrl, services: [ @@ -21,6 +24,7 @@ void main() { client: httpClient, errorConverter: errorConverter, ); + group('Base', () { test('get service errors', () async { final chopper = ChopperClient( @@ -39,8 +43,10 @@ void main() { try { chopper.getService(); } on Exception catch (e) { - expect(e.toString(), - 'Exception: Service type should be provided, `dynamic` is not allowed.'); + expect( + e.toString(), + 'Exception: Service type should be provided, `dynamic` is not allowed.', + ); } }); test('GET', () async { @@ -332,6 +338,7 @@ void main() { final client = MockClient((http.Request req) async { expect(req.headers.containsKey('foo'), isTrue); expect(req.headers['foo'], equals('bar')); + return http.Response('', 200); }); @@ -351,6 +358,7 @@ void main() { final client = MockClient((http.Request req) async { expect(req.headers.containsKey('test'), isTrue); expect(req.headers['test'], equals('42')); + return http.Response('', 200); }); @@ -375,6 +383,7 @@ void main() { req.url.toString(), equals('$baseUrl/test/get/1234'), ); + return http.Response('', 200); }); @@ -392,13 +401,10 @@ void main() { test('applyHeader', () { final req1 = applyHeader( - Request( - 'GET', - '/', - baseUrl, - ), - 'foo', - 'bar'); + Request('GET', '/', baseUrl), + 'foo', + 'bar', + ); expect(req1.headers, equals({'foo': 'bar'})); @@ -565,7 +571,8 @@ void main() { expect( request.url.toString(), equals( - '$baseUrl/test/query_map?test=true&foo=bar&list=1&list=2&inner.test=42'), + '$baseUrl/test/query_map?test=true&foo=bar&list=1&list=2&inner.test=42', + ), ); expect(request.method, equals('GET')); @@ -841,7 +848,7 @@ void main() { final chopper = buildClient(httpClient); final service = chopper.getService(); - final _ = await service.getAll(); + await service.getAll(); }); test('Slash in path gives a trailing slash', () async { @@ -858,12 +865,13 @@ void main() { final chopper = buildClient(httpClient); final service = chopper.getService(); - final _ = await service.getAllWithTrailingSlash(); + await service.getAllWithTrailingSlash(); }); test('timeout', () async { final httpClient = MockClient((http.Request req) async { await Future.delayed(const Duration(minutes: 1)); + return http.Response('ok', 200); }); @@ -872,10 +880,7 @@ void main() { try { await service - .getTest( - '1234', - dynamicHeader: '', - ) + .getTest('1234', dynamicHeader: '') .timeout(const Duration(seconds: 3)); } catch (e) { expect(e is TimeoutException, isTrue); diff --git a/chopper/test/client_test.dart b/chopper/test/client_test.dart index 81c6d0c8..babb172d 100644 --- a/chopper/test/client_test.dart +++ b/chopper/test/client_test.dart @@ -1,14 +1,14 @@ import 'dart:convert'; import 'package:chopper/chopper.dart'; -import 'package:test/test.dart'; -import 'package:http/testing.dart'; import 'package:http/http.dart' as http; +import 'package:http/testing.dart'; +import 'package:test/test.dart'; const baseUrl = 'http://localhost:8000'; void main() { - final buildClient = ([http.Client? httpClient]) => ChopperClient( + ChopperClient buildClient([http.Client? httpClient]) => ChopperClient( baseUrl: baseUrl, client: httpClient, interceptors: [ @@ -16,6 +16,7 @@ void main() { ], converter: JsonConverter(), ); + group('Client methods', () { test('GET', () async { final httpClient = MockClient((request) async { diff --git a/chopper/test/converter_test.dart b/chopper/test/converter_test.dart index 5448c472..2fae6736 100644 --- a/chopper/test/converter_test.dart +++ b/chopper/test/converter_test.dart @@ -1,16 +1,17 @@ import 'dart:convert' as dart_convert; import 'package:chopper/chopper.dart'; -import 'package:test/test.dart'; -import 'package:http/testing.dart'; import 'package:http/http.dart' as http; +import 'package:http/testing.dart'; +import 'package:test/test.dart'; + import 'test_service.dart'; const baseUrl = 'http://localhost:8000'; void main() { group('Converter', () { - final buildClient = (http.BaseClient client) => ChopperClient( + ChopperClient buildClient(http.BaseClient client) => ChopperClient( baseUrl: baseUrl, client: client, converter: TestConverter(), @@ -113,16 +114,16 @@ class TestConverter implements Converter { Response convertResponse(Response res) { if (res.body is String) { return res.copyWith<_Converted>( - body: _Converted(res.body)) as Response; + body: _Converted(res.body), + ) as Response; } + return res as Response; } @override - Request convertRequest(Request req) { - if (req.body is _Converted) return req.copyWith(body: req.body.data); - return req; - } + Request convertRequest(Request req) => + req.body is _Converted ? req.copyWith(body: req.body.data) : req; } class TestErrorConverter implements ErrorConverter { @@ -130,8 +131,10 @@ class TestErrorConverter implements ErrorConverter { Response convertError(Response res) { if (res.body is String) { final error = dart_convert.jsonDecode(res.body); + return res.copyWith<_ConvertedError>(body: _ConvertedError(error)); } + return res; } } diff --git a/chopper/test/form_test.dart b/chopper/test/form_test.dart index 95045d0f..ef5859d1 100644 --- a/chopper/test/form_test.dart +++ b/chopper/test/form_test.dart @@ -1,20 +1,21 @@ -import 'package:test/test.dart'; import 'package:chopper/chopper.dart'; -import 'test_service.dart'; -import 'package:http/testing.dart'; import 'package:http/http.dart' as http; +import 'package:http/testing.dart'; +import 'package:test/test.dart'; + +import 'test_service.dart'; void main() { group('Form', () { - final buildClient = - (http.Client httpClient, {bool isJson = false}) => ChopperClient( - services: [ - // the generated service - HttpTestService.create(), - ], - client: httpClient, - converter: isJson ? JsonConverter() : null, - ); + ChopperClient buildClient(http.Client httpClient, {bool isJson = false}) => + ChopperClient( + services: [ + // the generated service + HttpTestService.create(), + ], + client: httpClient, + converter: isJson ? JsonConverter() : null, + ); test('form-urlencoded default if no converter', () async { final httpClient = MockClient((http.Request req) async { @@ -24,6 +25,7 @@ void main() { 'application/x-www-form-urlencoded; charset=utf-8', ); expect(req.body, 'foo=test&default=hello'); + return http.Response('ok', 200); }); @@ -46,6 +48,7 @@ void main() { 'application/x-www-form-urlencoded; charset=utf-8', ); expect(req.body, 'foo=test&factory=converter'); + return http.Response('ok', 200); }); @@ -68,6 +71,7 @@ void main() { 'application/x-www-form-urlencoded; charset=utf-8', ); expect(req.body, 'foo=test&factory=converter'); + return http.Response('ok', 200); }); @@ -91,6 +95,7 @@ void main() { 'application/x-www-form-urlencoded; charset=utf-8', ); expect(req.body, 'foo=test&bar=42'); + return http.Response('ok', 200); }); diff --git a/chopper/test/interceptors_test.dart b/chopper/test/interceptors_test.dart index a325faeb..4650d89c 100644 --- a/chopper/test/interceptors_test.dart +++ b/chopper/test/interceptors_test.dart @@ -1,9 +1,10 @@ import 'dart:async'; -import 'package:http/testing.dart'; +import 'package:chopper/chopper.dart'; import 'package:http/http.dart' as http; +import 'package:http/testing.dart'; import 'package:test/test.dart'; -import 'package:chopper/chopper.dart'; + import 'test_service.dart'; void main() { @@ -14,6 +15,7 @@ void main() { request.url.toString(), equals('/test/get/1234/intercept'), ); + return http.Response('', 200); }, ); @@ -78,12 +80,13 @@ void main() { }); test('ResponseInterceptorFunc', () async { - var intercepted; + dynamic intercepted; final chopper = ChopperClient( interceptors: [ (Response response) { intercepted = _Intercepted(response.body); + return response; }, ], @@ -102,12 +105,13 @@ void main() { }); test('TypedResponseInterceptorFunc1', () async { - var intercepted; + dynamic intercepted; final chopper = ChopperClient( interceptors: [ (Response response) { intercepted = _Intercepted(response.body); + return response; }, ], @@ -130,7 +134,7 @@ void main() { return http.Response('["1","2"]', 200); }); - var intercepted; + dynamic intercepted; final chopper = ChopperClient( client: client, @@ -139,7 +143,8 @@ void main() { (Response response) { expect(isTypeOf(), isTrue); expect(isTypeOf>(), isTrue); - intercepted = _Intercepted(response.body!); + intercepted = _Intercepted(response.body as BodyType); + return response; }, ], @@ -157,12 +162,13 @@ void main() { final client = MockClient((http.Request req) async { expect(req.headers.containsKey('foo'), isTrue); expect(req.headers['foo'], equals('bar')); + return http.Response('', 200); }); final chopper = ChopperClient( interceptors: [ - HeadersInterceptor({'foo': 'bar'}) + HeadersInterceptor({'foo': 'bar'}), ], services: [ HttpTestService.create(), @@ -223,9 +229,12 @@ void main() { final logger = HttpLoggingInterceptor(); final fakeResponse = Response( - http.Response('responseBodyBase', 200, - headers: {'foo': 'bar'}, - request: await fakeRequest.toBaseRequest()), + http.Response( + 'responseBodyBase', + 200, + headers: {'foo': 'bar'}, + request: await fakeRequest.toBaseRequest(), + ), 'responseBody', ); @@ -254,6 +263,7 @@ class ResponseIntercept implements ResponseInterceptor { @override FutureOr onResponse(Response response) { intercepted = _Intercepted(response.body); + return response; } } diff --git a/chopper/test/json_test.dart b/chopper/test/json_test.dart index c65053f0..6a8c637f 100644 --- a/chopper/test/json_test.dart +++ b/chopper/test/json_test.dart @@ -1,10 +1,11 @@ import 'dart:convert'; -import 'package:test/test.dart'; import 'package:chopper/chopper.dart'; -import 'test_service.dart'; -import 'package:http/testing.dart'; import 'package:http/http.dart' as http; +import 'package:http/testing.dart'; +import 'package:test/test.dart'; + +import 'test_service.dart'; void main() { final sample = { @@ -15,7 +16,8 @@ void main() { 'result': 'ok', }; group('JSON', () { - final buildClient = (bool json, http.Client httpClient) => ChopperClient( + ChopperClient buildClient(bool json, http.Client httpClient) => + ChopperClient( services: [ // the generated service HttpTestService.create(), @@ -30,6 +32,7 @@ void main() { expect(req.url.toString(), equals('/test/map')); expect(req.headers['content-type'], 'application/json; charset=utf-8'); expect(req.body, equals(json.encode(sample))); + return http.Response( json.encode(res), 200, @@ -56,6 +59,7 @@ void main() { expect(req.headers['content-type'], 'application/json; charset=utf-8'); expect(req.headers['customConverter'], 'true'); expect(req.body, equals(json.encode(sample))); + return http.Response( json.encode(res), 200, diff --git a/chopper/test/multipart_test.dart b/chopper/test/multipart_test.dart index ca42cd95..20d28044 100644 --- a/chopper/test/multipart_test.dart +++ b/chopper/test/multipart_test.dart @@ -1,8 +1,9 @@ import 'package:chopper/chopper.dart'; +import 'package:http/http.dart' as http; +import 'package:http/testing.dart'; import 'package:http_parser/http_parser.dart'; import 'package:test/test.dart'; -import 'package:http/testing.dart'; -import 'package:http/http.dart' as http; + import 'test_service.dart'; void main() { @@ -26,6 +27,7 @@ void main() { '{bar: foo}\r\n', ), ); + return http.Response('ok', 200); }); @@ -46,14 +48,14 @@ void main() { contains('content-type: application/octet-stream'), ); expect( - req.body, - contains( - 'content-disposition: form-data; name="file"', - )); + req.body, + contains('content-disposition: form-data; name="file"'), + ); expect( req.body, - contains('${String.fromCharCodes([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])}'), + contains(String.fromCharCodes([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])), ); + return http.Response('ok', 200); }); @@ -79,14 +81,16 @@ void main() { isNot(contains('content-disposition: form-data; name="id"')), ); expect( - req.body, - contains( - 'content-disposition: form-data; name="file_field"; filename="file_name"', - )); + req.body, + contains( + 'content-disposition: form-data; name="file_field"; filename="file_name"', + ), + ); expect( req.body, - contains('${String.fromCharCodes([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])}'), + contains(String.fromCharCodes([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])), ); + return http.Response('ok', 200); }); @@ -109,22 +113,28 @@ void main() { final httpClient = MockClient((http.Request req) async { expect(req.headers['Content-Type'], contains('multipart/form-data;')); - expect(req.body, - contains('content-disposition: form-data; name="id"\r\n\r\n42\r\n')); + expect( + req.body, + contains( + 'content-disposition: form-data; name="id"\r\n\r\n42\r\n', + ), + ); expect( req.body, contains('content-type: application/octet-stream'), ); expect( - req.body, - contains( - 'content-disposition: form-data; name="file_field"; filename="file_name"', - )); + req.body, + contains( + 'content-disposition: form-data; name="file_field"; filename="file_name"', + ), + ); expect( req.body, - contains('${String.fromCharCodes([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])}'), + contains(String.fromCharCodes([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])), ); + return http.Response('ok', 200); }); @@ -166,6 +176,7 @@ void main() { 'World', ), ); + return http.Response('ok', 200); }); @@ -206,23 +217,29 @@ void main() { expect(req.fields['int'], equals('42')); }); - test('PartFile', () async { - final req = await toMultipartRequest( - [ - PartValueFile('foo', 'test/multipart_test.dart'), - PartValueFile>('int', [1, 2]), - ], - HttpMethod.Post, - Uri.parse('/foo'), - {}, - ); + test( + 'PartFile', + () async { + final req = await toMultipartRequest( + [ + PartValueFile('foo', 'test/multipart_test.dart'), + PartValueFile>('int', [1, 2]), + ], + HttpMethod.Post, + Uri.parse('/foo'), + {}, + ); - expect(req.files.firstWhere((f) => f.field == 'foo').filename, - equals('multipart_test.dart')); - final bytes = - await req.files.firstWhere((f) => f.field == 'int').finalize().first; - expect(bytes, equals([1, 2])); - }, testOn: 'vm'); + expect( + req.files.firstWhere((f) => f.field == 'foo').filename, + equals('multipart_test.dart'), + ); + final bytes = + await req.files.firstWhere((f) => f.field == 'int').finalize().first; + expect(bytes, equals([1, 2])); + }, + testOn: 'vm', + ); test('PartValue.replace', () { dynamic part = PartValue('foo', 'bar'); diff --git a/chopper/test/test_service.dart b/chopper/test/test_service.dart index fa137441..03abc239 100644 --- a/chopper/test/test_service.dart +++ b/chopper/test/test_service.dart @@ -1,7 +1,7 @@ import 'dart:async'; import 'dart:convert'; -import 'package:chopper/chopper.dart'; +import 'package:chopper/chopper.dart'; import 'package:http/http.dart' show MultipartFile; part 'test_service.chopper.dart'; @@ -101,7 +101,9 @@ abstract class HttpTestService extends ChopperService { @Post(path: 'map/json') @FactoryConverter( - request: customConvertRequest, response: customConvertResponse) + request: customConvertRequest, + response: customConvertResponse, + ) Future forceJsonTest(@Body() Map map); @Post(path: 'multi') @@ -140,6 +142,7 @@ abstract class HttpTestService extends ChopperService { Request customConvertRequest(Request req) { final r = JsonConverter().convertRequest(req); + return applyHeader(r, 'customConverter', 'true'); } diff --git a/chopper_built_value/analysis_options.yaml b/chopper_built_value/analysis_options.yaml index d4fcc1ad..7f5a674f 100644 --- a/chopper_built_value/analysis_options.yaml +++ b/chopper_built_value/analysis_options.yaml @@ -1 +1,34 @@ -include: package:pedantic/analysis_options.yaml \ No newline at end of file +include: package:lints/recommended.yaml + +analyzer: + exclude: + - "**.g.dart" + - "**.chopper.dart" + - "**.mocks.dart" + - "example/**" + plugins: + - dart_code_metrics + +dart_code_metrics: + metrics: + cyclomatic-complexity: 20 + number-of-arguments: 4 + maximum-nesting-level: 5 + number-of-parameters: 7 + metrics-exclude: + - test/** + rules: + - newline-before-return + - no-boolean-literal-compare + - no-empty-block + - prefer-trailing-comma + - prefer-conditional-expressions + - no-equal-then-else + anti-patterns: + - long-method + - long-parameter-list + +linter: + rules: + avoid_print: true + prefer_single_quotes: true diff --git a/chopper_built_value/lib/chopper_built_value.dart b/chopper_built_value/lib/chopper_built_value.dart index 47d6a173..7beef449 100644 --- a/chopper_built_value/lib/chopper_built_value.dart +++ b/chopper_built_value/lib/chopper_built_value.dart @@ -8,7 +8,7 @@ import 'package:chopper/chopper.dart'; /// having a serializer implementation made with the built_value package. class BuiltValueConverter implements Converter, ErrorConverter { final Serializers serializers; - final JsonConverter jsonConverter = JsonConverter(); + static const JsonConverter jsonConverter = JsonConverter(); final Type? errorType; /// Builds a new BuiltValueConverter instance that uses built_value serializers defined @@ -17,10 +17,10 @@ class BuiltValueConverter implements Converter, ErrorConverter { /// If the error body cannot be converted with serializers and [errorType] is provided /// and it's not `null`, BuiltValueConverter will try to deserialize the error body into /// [errorType]. - BuiltValueConverter(this.serializers, {this.errorType}); + const BuiltValueConverter(this.serializers, {this.errorType}); T? _deserialize(dynamic value) { - var serializer; + dynamic serializer; if (value is Map && value.containsKey('\$')) { serializer = serializers.serializerForWireName(value['\$']); } @@ -34,7 +34,9 @@ class BuiltValueConverter implements Converter, ErrorConverter { } BuiltList _deserializeListOf(Iterable value) { - final deserialized = value.map((value) => _deserialize(value)); + final Iterable deserialized = + value.map((value) => _deserialize(value)); + return BuiltList(deserialized.toList(growable: false)); } @@ -43,29 +45,33 @@ class BuiltValueConverter implements Converter, ErrorConverter { if (entity is Iterable) { return _deserializeListOf(entity) as BodyType; } + return _deserialize(entity); } @override - Request convertRequest(Request request) { - request = request.copyWith(body: serializers.serialize(request.body)); - return jsonConverter.convertRequest(request); - } + Request convertRequest(Request request) => jsonConverter.convertRequest( + request.copyWith(body: serializers.serialize(request.body)), + ); @override FutureOr> convertResponse( - Response response) async { - final jsonResponse = await jsonConverter.convertResponse(response); - final body = deserialize(jsonResponse.body); - return jsonResponse.copyWith(body: body); + Response response, + ) async { + final Response jsonResponse = await jsonConverter.convertResponse(response); + + return jsonResponse.copyWith( + body: deserialize(jsonResponse.body), + ); } @override FutureOr convertError( - Response response) async { - final jsonResponse = await jsonConverter.convertResponse(response); + Response response, + ) async { + final Response jsonResponse = await jsonConverter.convertResponse(response); - var body; + dynamic body; try { // try to deserialize using wireName diff --git a/chopper_built_value/pubspec.yaml b/chopper_built_value/pubspec.yaml index 88ccd7fc..b96ad512 100644 --- a/chopper_built_value/pubspec.yaml +++ b/chopper_built_value/pubspec.yaml @@ -5,7 +5,7 @@ documentation: https://hadrien-lejard.gitbook.io/chopper/converters/built-value- repository: https://github.com/lejard-h/chopper environment: - sdk: ">=2.12.0 <3.0.0" + sdk: ">=2.17.0 <3.0.0" dependencies: built_value: ^8.0.0 @@ -18,7 +18,8 @@ dev_dependencies: build_runner: ^2.0.0 build_test: ^2.0.0 built_value_generator: ^8.0.6 - pedantic: ^1.10.0 + dart_code_metrics: ^4.8.1 + lints: ^2.0.0 dependency_overrides: # Comment before publish diff --git a/chopper_built_value/test/data.g.dart b/chopper_built_value/test/data.g.dart index df30695c..d9413768 100644 --- a/chopper_built_value/test/data.g.dart +++ b/chopper_built_value/test/data.g.dart @@ -35,17 +35,17 @@ class _$DataModelSerializer implements StructuredSerializer { final iterator = serialized.iterator; while (iterator.moveNext()) { - final key = iterator.current as String; + final key = iterator.current! as String; iterator.moveNext(); final Object? value = iterator.current; switch (key) { case 'id': result.id = serializers.deserialize(value, - specifiedType: const FullType(int)) as int; + specifiedType: const FullType(int))! as int; break; case 'name': result.name = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String))! as String; break; } } @@ -79,13 +79,13 @@ class _$ErrorModelSerializer implements StructuredSerializer { final iterator = serialized.iterator; while (iterator.moveNext()) { - final key = iterator.current as String; + final key = iterator.current! as String; iterator.moveNext(); final Object? value = iterator.current; switch (key) { case 'message': result.message = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String))! as String; break; } } @@ -101,11 +101,11 @@ class _$DataModel extends DataModel { final String name; factory _$DataModel([void Function(DataModelBuilder)? updates]) => - (new DataModelBuilder()..update(updates)).build(); + (new DataModelBuilder()..update(updates))._build(); _$DataModel._({required this.id, required this.name}) : super._() { - BuiltValueNullFieldError.checkNotNull(id, 'DataModel', 'id'); - BuiltValueNullFieldError.checkNotNull(name, 'DataModel', 'name'); + BuiltValueNullFieldError.checkNotNull(id, r'DataModel', 'id'); + BuiltValueNullFieldError.checkNotNull(name, r'DataModel', 'name'); } @override @@ -128,7 +128,7 @@ class _$DataModel extends DataModel { @override String toString() { - return (newBuiltValueToStringHelper('DataModel') + return (newBuiltValueToStringHelper(r'DataModel') ..add('id', id) ..add('name', name)) .toString(); @@ -170,12 +170,14 @@ class DataModelBuilder implements Builder { } @override - _$DataModel build() { + DataModel build() => _build(); + + _$DataModel _build() { final _$result = _$v ?? new _$DataModel._( - id: BuiltValueNullFieldError.checkNotNull(id, 'DataModel', 'id'), + id: BuiltValueNullFieldError.checkNotNull(id, r'DataModel', 'id'), name: BuiltValueNullFieldError.checkNotNull( - name, 'DataModel', 'name')); + name, r'DataModel', 'name')); replace(_$result); return _$result; } @@ -186,10 +188,10 @@ class _$ErrorModel extends ErrorModel { final String message; factory _$ErrorModel([void Function(ErrorModelBuilder)? updates]) => - (new ErrorModelBuilder()..update(updates)).build(); + (new ErrorModelBuilder()..update(updates))._build(); _$ErrorModel._({required this.message}) : super._() { - BuiltValueNullFieldError.checkNotNull(message, 'ErrorModel', 'message'); + BuiltValueNullFieldError.checkNotNull(message, r'ErrorModel', 'message'); } @override @@ -212,7 +214,7 @@ class _$ErrorModel extends ErrorModel { @override String toString() { - return (newBuiltValueToStringHelper('ErrorModel')..add('message', message)) + return (newBuiltValueToStringHelper(r'ErrorModel')..add('message', message)) .toString(); } } @@ -247,14 +249,16 @@ class ErrorModelBuilder implements Builder { } @override - _$ErrorModel build() { + ErrorModel build() => _build(); + + _$ErrorModel _build() { final _$result = _$v ?? new _$ErrorModel._( message: BuiltValueNullFieldError.checkNotNull( - message, 'ErrorModel', 'message')); + message, r'ErrorModel', 'message')); replace(_$result); return _$result; } } -// ignore_for_file: always_put_control_body_on_new_line,always_specify_types,annotate_overrides,avoid_annotating_with_dynamic,avoid_as,avoid_catches_without_on_clauses,avoid_returning_this,lines_longer_than_80_chars,omit_local_variable_types,prefer_expression_function_bodies,sort_constructors_first,test_types_in_equals,unnecessary_const,unnecessary_new +// ignore_for_file: always_put_control_body_on_new_line,always_specify_types,annotate_overrides,avoid_annotating_with_dynamic,avoid_as,avoid_catches_without_on_clauses,avoid_returning_this,deprecated_member_use_from_same_package,lines_longer_than_80_chars,no_leading_underscores_for_local_identifiers,omit_local_variable_types,prefer_expression_function_bodies,sort_constructors_first,test_types_in_equals,unnecessary_const,unnecessary_new,unnecessary_lambdas diff --git a/chopper_built_value/test/serializers.dart b/chopper_built_value/test/serializers.dart index 370f4b37..7124a7c1 100644 --- a/chopper_built_value/test/serializers.dart +++ b/chopper_built_value/test/serializers.dart @@ -1,6 +1,7 @@ library serializers; import 'package:built_value/serializer.dart'; + import 'data.dart'; part 'serializers.g.dart'; diff --git a/chopper_built_value/test/serializers.g.dart b/chopper_built_value/test/serializers.g.dart index 6cb3a9e7..55d2a7d3 100644 --- a/chopper_built_value/test/serializers.g.dart +++ b/chopper_built_value/test/serializers.g.dart @@ -11,4 +11,4 @@ Serializers _$serializers = (new Serializers().toBuilder() ..add(ErrorModel.serializer)) .build(); -// ignore_for_file: always_put_control_body_on_new_line,always_specify_types,annotate_overrides,avoid_annotating_with_dynamic,avoid_as,avoid_catches_without_on_clauses,avoid_returning_this,lines_longer_than_80_chars,omit_local_variable_types,prefer_expression_function_bodies,sort_constructors_first,test_types_in_equals,unnecessary_const,unnecessary_new +// ignore_for_file: always_put_control_body_on_new_line,always_specify_types,annotate_overrides,avoid_annotating_with_dynamic,avoid_as,avoid_catches_without_on_clauses,avoid_returning_this,deprecated_member_use_from_same_package,lines_longer_than_80_chars,no_leading_underscores_for_local_identifiers,omit_local_variable_types,prefer_expression_function_bodies,sort_constructors_first,test_types_in_equals,unnecessary_const,unnecessary_new,unnecessary_lambdas diff --git a/chopper_generator/analysis_options.yaml b/chopper_generator/analysis_options.yaml index d4fcc1ad..57256269 100644 --- a/chopper_generator/analysis_options.yaml +++ b/chopper_generator/analysis_options.yaml @@ -1 +1,35 @@ -include: package:pedantic/analysis_options.yaml \ No newline at end of file +include: package:lints/recommended.yaml + +analyzer: + exclude: + - "**.g.dart" + - "**.chopper.dart" + - "**.mocks.dart" + - "example/**" + plugins: + - dart_code_metrics + +dart_code_metrics: + metrics: + cyclomatic-complexity: 20 + number-of-arguments: 4 + maximum-nesting-level: 5 + number-of-parameters: 5 + source-lines-of-code: 200 + metrics-exclude: + - test/** + rules: + - newline-before-return + - no-boolean-literal-compare + - no-empty-block + - prefer-trailing-comma + - prefer-conditional-expressions + - no-equal-then-else + anti-patterns: + - long-method + - long-parameter-list + +linter: + rules: + avoid_print: true + prefer_single_quotes: true diff --git a/chopper_generator/lib/chopper_generator.dart b/chopper_generator/lib/chopper_generator.dart index 2facc8b0..74355389 100644 --- a/chopper_generator/lib/chopper_generator.dart +++ b/chopper_generator/lib/chopper_generator.dart @@ -1,6 +1,7 @@ library chopper_generator.dart; import 'package:build/build.dart'; + import 'src/generator.dart'; Builder chopperGeneratorFactory(BuilderOptions options) => diff --git a/chopper_generator/lib/src/generator.dart b/chopper_generator/lib/src/generator.dart index ab95ee80..13b3c606 100644 --- a/chopper_generator/lib/src/generator.dart +++ b/chopper_generator/lib/src/generator.dart @@ -1,32 +1,28 @@ ///@nodoc import 'dart:async'; +import 'package:analyzer/dart/constant/value.dart'; import 'package:analyzer/dart/element/element.dart'; import 'package:analyzer/dart/element/nullability_suffix.dart'; import 'package:analyzer/dart/element/type.dart'; - import 'package:build/build.dart'; import 'package:built_collection/built_collection.dart'; -import 'package:dart_style/dart_style.dart'; - -import 'package:source_gen/source_gen.dart'; - -// TODO(lejard_h) Code builder not null safe yet -// ignore: import_of_legacy_library_into_null_safe -import 'package:code_builder/code_builder.dart'; import 'package:chopper/chopper.dart' as chopper; +import 'package:code_builder/code_builder.dart'; +import 'package:dart_style/dart_style.dart'; import 'package:logging/logging.dart'; +import 'package:source_gen/source_gen.dart'; -const _clientVar = 'client'; -const _baseUrlVar = 'baseUrl'; -const _parametersVar = '\$params'; -const _headersVar = '\$headers'; -const _requestVar = '\$request'; -const _bodyVar = '\$body'; -const _partsVar = '\$parts'; -const _urlVar = '\$url'; +const String _clientVar = 'client'; +const String _baseUrlVar = 'baseUrl'; +const String _parametersVar = r'$params'; +const String _headersVar = r'$headers'; +const String _requestVar = r'$request'; +const String _bodyVar = r'$body'; +const String _partsVar = r'$parts'; +const String _urlVar = r'$url'; -final _logger = Logger('Chopper Generator'); +final Logger _logger = Logger('Chopper Generator'); class ChopperGenerator extends GeneratorForAnnotation { @override @@ -36,7 +32,7 @@ class ChopperGenerator extends GeneratorForAnnotation { BuildStep buildStep, ) { if (element is! ClassElement) { - final friendlyName = element.displayName; + final String friendlyName = element.displayName; throw InvalidGenerationSourceError( 'Generator cannot target `$friendlyName`.', todo: 'Remove the [ChopperApi] annotation from `$friendlyName`.', @@ -62,18 +58,18 @@ class ChopperGenerator extends GeneratorForAnnotation { ClassElement element, ) { if (!element.allSupertypes.any(_extendsChopperService)) { - final friendlyName = element.displayName; + final String friendlyName = element.displayName; throw InvalidGenerationSourceError( 'Generator cannot target `$friendlyName`.', todo: '`$friendlyName` need to extends the [ChopperService] class.', ); } - final friendlyName = element.name; - final name = '_\$$friendlyName'; - final baseUrl = annotation.peek(_baseUrlVar)?.stringValue ?? ''; + final String friendlyName = element.name; + final String name = '_\$$friendlyName'; + final String baseUrl = annotation.peek(_baseUrlVar)?.stringValue ?? ''; - final classBuilder = Class((builder) { + final Class classBuilder = Class((builder) { builder ..name = name ..extend = refer(friendlyName) @@ -82,57 +78,70 @@ class ChopperGenerator extends GeneratorForAnnotation { ..methods.addAll(_parseMethods(element, baseUrl)); }); - final ignore = + final String ignore = '// ignore_for_file: always_put_control_body_on_new_line, always_specify_types, prefer_const_declarations, unnecessary_brace_in_string_interps'; - final emitter = DartEmitter(); + final DartEmitter emitter = DartEmitter(); + return DartFormatter().format('$ignore\n${classBuilder.accept(emitter)}'); } - Constructor _generateConstructor() => Constructor((constructorBuilder) { - constructorBuilder.optionalParameters.add( - Parameter((paramBuilder) { - paramBuilder.name = _clientVar; - paramBuilder.type = refer('${chopper.ChopperClient}?'); - }), - ); + Constructor _generateConstructor() => Constructor( + (ConstructorBuilder constructorBuilder) { + constructorBuilder.optionalParameters.add( + Parameter((paramBuilder) { + paramBuilder.name = _clientVar; + paramBuilder.type = refer('${chopper.ChopperClient}?'); + }), + ); - constructorBuilder.body = Code( - 'if ($_clientVar == null) return;\nthis.$_clientVar = $_clientVar;', - ); - }); - - Iterable _parseMethods(ClassElement element, String baseUrl) { - return element.methods.where((MethodElement method) { - final methodAnnotation = _getMethodAnnotation(method); - return methodAnnotation != null && - method.isAbstract && - method.returnType.isDartAsyncFuture; - }).map((MethodElement m) => _generateMethod(m, baseUrl)); - } + constructorBuilder.body = Code( + 'if ($_clientVar == null) return;\nthis.$_clientVar = $_clientVar;', + ); + }, + ); + + Iterable _parseMethods(ClassElement element, String baseUrl) => + element.methods + .where( + (MethodElement method) => + _getMethodAnnotation(method) != null && + method.isAbstract && + method.returnType.isDartAsyncFuture, + ) + .map((MethodElement m) => _generateMethod(m, baseUrl)); Method _generateMethod(MethodElement m, String baseUrl) { - final method = _getMethodAnnotation(m); - final multipart = _hasAnnotation(m, chopper.Multipart); - final factoryConverter = _getFactoryConverterAnnotation(m); - - final body = _getAnnotation(m, chopper.Body); - final paths = _getAnnotations(m, chopper.Path); - final queries = _getAnnotations(m, chopper.Query); - final queryMap = _getAnnotation(m, chopper.QueryMap); - final fields = _getAnnotations(m, chopper.Field); - final fieldMap = _getAnnotation(m, chopper.FieldMap); - final parts = _getAnnotations(m, chopper.Part); - final partMap = _getAnnotation(m, chopper.PartMap); - final fileFields = _getAnnotations(m, chopper.PartFile); - final fileFieldMap = _getAnnotation(m, chopper.PartFileMap); - - final headers = _generateHeaders(m, method!); - final url = _generateUrl(method, paths, baseUrl); - final responseType = _getResponseType(m.returnType); - final responseInnerType = + final ConstantReader? method = _getMethodAnnotation(m); + final bool multipart = _hasAnnotation(m, chopper.Multipart); + final ConstantReader? factoryConverter = _getFactoryConverterAnnotation(m); + + final Map body = _getAnnotation(m, chopper.Body); + final Map paths = + _getAnnotations(m, chopper.Path); + final Map queries = + _getAnnotations(m, chopper.Query); + final Map queryMap = + _getAnnotation(m, chopper.QueryMap); + final Map fields = + _getAnnotations(m, chopper.Field); + final Map fieldMap = + _getAnnotation(m, chopper.FieldMap); + final Map parts = + _getAnnotations(m, chopper.Part); + final Map partMap = + _getAnnotation(m, chopper.PartMap); + final Map fileFields = + _getAnnotations(m, chopper.PartFile); + final Map fileFieldMap = + _getAnnotation(m, chopper.PartFileMap); + + final Code? headers = _generateHeaders(m, method!); + final Expression url = _generateUrl(method, paths, baseUrl); + final DartType? responseType = _getResponseType(m.returnType); + final DartType? responseInnerType = _getResponseInnerType(m.returnType) ?? responseType; - return Method((b) { + return Method((MethodBuilder b) { b.annotations.add(refer('override')); b.name = m.displayName; @@ -164,7 +173,7 @@ class ChopperGenerator extends GeneratorForAnnotation { m.parameters.where((p) => p.isNamed).map(buildNamedParam), ); - final blocks = [ + final List blocks = [ url.assignFinal(_urlVar).statement, ]; @@ -173,12 +182,12 @@ class ChopperGenerator extends GeneratorForAnnotation { } // Build an iterable of all the parameters that are nullable - final optionalNullableParameters = [ + final Iterable optionalNullableParameters = [ ...m.parameters.where((p) => p.isOptionalPositional), ...m.parameters.where((p) => p.isNamed), ].where((el) => el.type.isNullable).map((el) => el.name); - final hasQueryMap = queryMap.isNotEmpty; + final bool hasQueryMap = queryMap.isNotEmpty; if (hasQueryMap) { if (queries.isNotEmpty) { blocks.add(refer('$_parametersVar.addAll').call( @@ -204,16 +213,16 @@ class ChopperGenerator extends GeneratorForAnnotation { } } - final hasQuery = hasQueryMap || queries.isNotEmpty; + final bool hasQuery = hasQueryMap || queries.isNotEmpty; if (headers != null) { blocks.add(headers); } - final methodOptionalBody = getMethodOptionalBody(method); - final methodName = getMethodName(method); - final methodUrl = getMethodPath(method); - var hasBody = body.isNotEmpty || fields.isNotEmpty; + final bool methodOptionalBody = getMethodOptionalBody(method); + final String methodName = getMethodName(method); + final String methodUrl = getMethodPath(method); + bool hasBody = body.isNotEmpty || fields.isNotEmpty; if (hasBody) { if (body.isNotEmpty) { blocks.add( @@ -226,7 +235,7 @@ class ChopperGenerator extends GeneratorForAnnotation { } } - final hasFieldMap = fieldMap.isNotEmpty; + final bool hasFieldMap = fieldMap.isNotEmpty; if (hasFieldMap) { if (hasBody) { blocks.add(refer('$_bodyVar.addAll').call( @@ -241,14 +250,14 @@ class ChopperGenerator extends GeneratorForAnnotation { hasBody = hasBody || hasFieldMap; - var hasParts = - multipart == true && (parts.isNotEmpty || fileFields.isNotEmpty); + bool hasParts = multipart && (parts.isNotEmpty || fileFields.isNotEmpty); if (hasParts) { blocks.add( - _generateList(parts, fileFields).assignFinal(_partsVar).statement); + _generateList(parts, fileFields).assignFinal(_partsVar).statement, + ); } - final hasPartMap = multipart == true && partMap.isNotEmpty; + final bool hasPartMap = multipart && partMap.isNotEmpty; if (hasPartMap) { if (hasParts) { blocks.add(refer('$_partsVar.addAll').call( @@ -261,7 +270,7 @@ class ChopperGenerator extends GeneratorForAnnotation { } } - final hasFileFilesMap = multipart == true && fileFieldMap.isNotEmpty; + final bool hasFileFilesMap = multipart && fileFieldMap.isNotEmpty; if (hasFileFilesMap) { if (hasParts || hasPartMap) { blocks.add(refer('$_partsVar.addAll').call( @@ -295,26 +304,30 @@ class ChopperGenerator extends GeneratorForAnnotation { hasParts: hasParts, ).assignFinal(_requestVar).statement); - final namedArguments = {}; + final Map namedArguments = {}; - final requestFactory = factoryConverter?.peek('request'); + final ConstantReader? requestFactory = factoryConverter?.peek('request'); if (requestFactory != null) { - final func = requestFactory.objectValue.toFunctionValue(); + final ExecutableElement? func = + requestFactory.objectValue.toFunctionValue(); namedArguments['requestConverter'] = refer(_factoryForFunction(func!)); } - final responseFactory = factoryConverter?.peek('response'); + final ConstantReader? responseFactory = + factoryConverter?.peek('response'); if (responseFactory != null) { - final func = responseFactory.objectValue.toFunctionValue(); + final ExecutableElement? func = + responseFactory.objectValue.toFunctionValue(); namedArguments['responseConverter'] = refer(_factoryForFunction(func!)); } - final typeArguments = []; + final List typeArguments = []; if (responseType != null) { typeArguments .add(refer(responseType.getDisplayString(withNullability: false))); typeArguments.add( - refer(responseInnerType!.getDisplayString(withNullability: false))); + refer(responseInnerType!.getDisplayString(withNullability: false)), + ); } blocks.add(refer('$_clientVar.send') @@ -326,39 +339,42 @@ class ChopperGenerator extends GeneratorForAnnotation { }); } - String _factoryForFunction(FunctionTypedElement function) { - if (function.enclosingElement is ClassElement) { - return '${function.enclosingElement!.name}.${function.name}'; - } - return function.name!; - } + String _factoryForFunction(FunctionTypedElement function) => + function.enclosingElement is ClassElement + ? '${function.enclosingElement!.name}.${function.name}' + : function.name!; Map _getAnnotation(MethodElement method, Type type) { - var annotation; - var name = ''; - for (final p in method.parameters) { - dynamic a = _typeChecker(type).firstAnnotationOf(p); + DartObject? annotation; + String name = ''; + + for (final ParameterElement p in method.parameters) { + DartObject? a = _typeChecker(type).firstAnnotationOf(p); if (annotation != null && a != null) { throw Exception( - 'Too many $type annotation for \'${method.displayName}\''); + 'Too many $type annotation for \'${method.displayName}\'', + ); } else if (annotation == null && a != null) { annotation = a; name = p.displayName; } } - if (annotation == null) return {}; - return {name: ConstantReader(annotation)}; + + return annotation == null ? {} : {name: ConstantReader(annotation)}; } Map _getAnnotations( - MethodElement m, Type type) { - var annotation = {}; - for (final p in m.parameters) { - final a = _typeChecker(type).firstAnnotationOf(p); + MethodElement m, + Type type, + ) { + Map annotation = {}; + for (final ParameterElement p in m.parameters) { + final DartObject? a = _typeChecker(type).firstAnnotationOf(p); if (a != null) { annotation[p] = ConstantReader(a); } } + return annotation; } @@ -366,28 +382,28 @@ class ChopperGenerator extends GeneratorForAnnotation { ConstantReader? _getMethodAnnotation(MethodElement method) { for (final type in _methodsAnnotations) { - final annotation = _typeChecker(type) + final DartObject? annotation = _typeChecker(type) .firstAnnotationOf(method, throwOnUnresolved: false); - if (annotation != null) return ConstantReader(annotation); + if (annotation != null) { + return ConstantReader(annotation); + } } + return null; } ConstantReader? _getFactoryConverterAnnotation(MethodElement method) { - final annotation = _typeChecker(chopper.FactoryConverter) + final DartObject? annotation = _typeChecker(chopper.FactoryConverter) .firstAnnotationOf(method, throwOnUnresolved: false); - if (annotation != null) return ConstantReader(annotation); - return null; - } - bool _hasAnnotation(MethodElement method, Type type) { - final annotation = - _typeChecker(type).firstAnnotationOf(method, throwOnUnresolved: false); - - return annotation != null; + return annotation != null ? ConstantReader(annotation) : null; } - final _methodsAnnotations = const [ + bool _hasAnnotation(MethodElement method, Type type) => + _typeChecker(type).firstAnnotationOf(method, throwOnUnresolved: false) != + null; + + final List _methodsAnnotations = const [ chopper.Get, chopper.Post, chopper.Delete, @@ -398,18 +414,15 @@ class ChopperGenerator extends GeneratorForAnnotation { chopper.Options, ]; - DartType? _genericOf(DartType? type) { - return type is InterfaceType && type.typeArguments.isNotEmpty - ? type.typeArguments.first - : null; - } + DartType? _genericOf(DartType? type) => + type is InterfaceType && type.typeArguments.isNotEmpty + ? type.typeArguments.first + : null; - DartType? _getResponseType(DartType type) { - return _genericOf(_genericOf(type)); - } + DartType? _getResponseType(DartType type) => _genericOf(_genericOf(type)); DartType? _getResponseInnerType(DartType type) { - final generic = _genericOf(type); + final DartType? generic = _genericOf(type); if (generic == null || _typeChecker(Map).isExactlyType(type) || @@ -428,9 +441,9 @@ class ChopperGenerator extends GeneratorForAnnotation { Map paths, String baseUrl, ) { - var path = getMethodPath(method); + String path = getMethodPath(method); paths.forEach((p, ConstantReader r) { - final name = r.peek('name')?.stringValue ?? p.displayName; + final String name = r.peek('name')?.stringValue ?? p.displayName; path = path.replaceFirst('{$name}', '\${${p.displayName}}'); }); @@ -459,13 +472,13 @@ class ChopperGenerator extends GeneratorForAnnotation { bool useQueries = false, bool useHeaders = false, }) { - final params = [ + final List params = [ literal(getMethodName(method)), refer(_urlVar), refer('$_clientVar.$_baseUrlVar'), ]; - final namedParams = {}; + final Map namedParams = {}; if (hasBody) { namedParams['body'] = refer(_bodyVar); @@ -488,9 +501,9 @@ class ChopperGenerator extends GeneratorForAnnotation { } Expression _generateMap(Map queries) { - final map = {}; - queries.forEach((p, ConstantReader r) { - final name = r.peek('name')?.stringValue ?? p.displayName; + final Map map = {}; + queries.forEach((ParameterElement p, ConstantReader r) { + final String name = r.peek('name')?.stringValue ?? p.displayName; map[literal(name)] = refer(p.displayName); }); @@ -501,21 +514,21 @@ class ChopperGenerator extends GeneratorForAnnotation { Map parts, Map fileFields, ) { - final list = []; + final List list = []; parts.forEach((p, ConstantReader r) { - final name = r.peek('name')?.stringValue ?? p.displayName; - final params = [ + final String name = r.peek('name')?.stringValue ?? p.displayName; + final List params = [ literal(name), refer(p.displayName), ]; list.add(refer( - 'PartValue<${p.type.getDisplayString(withNullability: p.type.isNullable)}>') - .newInstance(params)); + 'PartValue<${p.type.getDisplayString(withNullability: p.type.isNullable)}>', + ).newInstance(params)); }); fileFields.forEach((p, ConstantReader r) { - final name = r.peek('name')?.stringValue ?? p.displayName; - final params = [ + final String name = r.peek('name')?.stringValue ?? p.displayName; + final List params = [ literal(name), refer(p.displayName), ]; @@ -525,18 +538,20 @@ class ChopperGenerator extends GeneratorForAnnotation { .newInstance(params), ); }); + return literalList(list, refer('PartValue')); } Code? _generateHeaders(MethodElement methodElement, ConstantReader method) { - final codeBuffer = StringBuffer('')..writeln('{'); + final StringBuffer codeBuffer = StringBuffer('')..writeln('{'); // Search for @Header anotation in method parameters - final annotations = _getAnnotations(methodElement, chopper.Header); + final Map annotations = + _getAnnotations(methodElement, chopper.Header); annotations.forEach((parameter, ConstantReader annotation) { - final paramName = parameter.displayName; - final name = annotation.peek('name')?.stringValue ?? paramName; + final String paramName = parameter.displayName; + final String name = annotation.peek('name')?.stringValue ?? paramName; if (parameter.type.isNullable) { codeBuffer.writeln('if ($paramName != null) \'$name\': $paramName,'); @@ -545,10 +560,11 @@ class ChopperGenerator extends GeneratorForAnnotation { } }); - final headersReader = method.peek('headers'); + final ConstantReader? headersReader = method.peek('headers'); if (headersReader == null) return null; - final methodAnnotations = headersReader.mapValue; + final Map methodAnnotations = + headersReader.mapValue; methodAnnotations.forEach((headerName, headerValue) { if (headerName != null && headerValue != null) { @@ -559,12 +575,9 @@ class ChopperGenerator extends GeneratorForAnnotation { }); codeBuffer.writeln('};'); - final code = codeBuffer.toString(); - if (code == '{\n};\n') { - return null; - } + final String code = codeBuffer.toString(); - return Code('final $_headersVar = $code'); + return code == '{\n};\n' ? null : Code('final $_headersVar = $code'); } } @@ -587,45 +600,41 @@ extension DartTypeExtension on DartType { } // All positional required params must support nullability -Parameter buildRequiredPositionalParam(ParameterElement p) { - return Parameter( - (pb) => pb - ..name = p.name - ..type = Reference( - p.type.getDisplayString(withNullability: p.type.isNullable), - ), - ); -} +Parameter buildRequiredPositionalParam(ParameterElement p) => Parameter( + (ParameterBuilder pb) => pb + ..name = p.name + ..type = Reference( + p.type.getDisplayString(withNullability: p.type.isNullable), + ), + ); // All optional positional params must support nullability -Parameter buildOptionalPositionalParam(ParameterElement p) { - return Parameter((pb) { - pb - ..name = p.name - ..type = Reference( - p.type.getDisplayString(withNullability: p.type.isNullable), - ); +Parameter buildOptionalPositionalParam(ParameterElement p) => + Parameter((ParameterBuilder pb) { + pb + ..name = p.name + ..type = Reference( + p.type.getDisplayString(withNullability: p.type.isNullable), + ); - if (p.defaultValueCode != null) { - pb.defaultTo = Code(p.defaultValueCode!); - } - }); -} + if (p.defaultValueCode != null) { + pb.defaultTo = Code(p.defaultValueCode!); + } + }); // Named params can be optional or required, they also need to support // nullability -Parameter buildNamedParam(ParameterElement p) { - return Parameter((pb) { - pb - ..named = true - ..name = p.name - ..required = p.isRequiredNamed - ..type = Reference( - p.type.getDisplayString(withNullability: p.type.isNullable), - ); +Parameter buildNamedParam(ParameterElement p) => + Parameter((ParameterBuilder pb) { + pb + ..named = true + ..name = p.name + ..required = p.isRequiredNamed + ..type = Reference( + p.type.getDisplayString(withNullability: p.type.isNullable), + ); - if (p.defaultValueCode != null) { - pb.defaultTo = Code(p.defaultValueCode!); - } - }); -} + if (p.defaultValueCode != null) { + pb.defaultTo = Code(p.defaultValueCode!); + } + }); diff --git a/chopper_generator/pubspec.yaml b/chopper_generator/pubspec.yaml index ef2adb98..b868926a 100644 --- a/chopper_generator/pubspec.yaml +++ b/chopper_generator/pubspec.yaml @@ -5,22 +5,23 @@ documentation: https://hadrien-lejard.gitbook.io/chopper repository: https://github.com/lejard-h/chopper environment: - sdk: ">=2.12.0 <3.0.0" + sdk: ">=2.17.0 <3.0.0" dependencies: analyzer: ">=4.1.0 <4.3.0" build: ^2.0.0 built_collection: ^5.0.0 chopper: ^4.0.0 - code_builder: ^4.0.0 + code_builder: ^4.1.0 dart_style: ^2.0.0 logging: ^1.0.0 meta: ^1.3.0 source_gen: ^1.0.0 dev_dependencies: - pedantic: ^1.11.0 - test: ^1.15.4 + test: ^1.16.4 + dart_code_metrics: ^4.8.1 + lints: ^2.0.0 dependency_overrides: # Comment before publish diff --git a/example/analysis_options.yaml b/example/analysis_options.yaml new file mode 100644 index 00000000..7061686f --- /dev/null +++ b/example/analysis_options.yaml @@ -0,0 +1,32 @@ +include: package:lints/recommended.yaml + +analyzer: + exclude: + - "**.g.dart" + - "**.chopper.dart" + - "**.mocks.dart" + plugins: + - dart_code_metrics + +dart_code_metrics: + metrics: + cyclomatic-complexity: 20 + number-of-arguments: 4 + maximum-nesting-level: 5 + metrics-exclude: + - test/** + rules: + - newline-before-return + - no-boolean-literal-compare + - no-empty-block + - prefer-trailing-comma + - prefer-conditional-expressions + - no-equal-then-else + anti-patterns: + - long-method + - long-parameter-list + +linter: + rules: + avoid_print: false + prefer_single_quotes: true diff --git a/example/bin/main_built_value.dart b/example/bin/main_built_value.dart index a92630ea..9f87becc 100644 --- a/example/bin/main_built_value.dart +++ b/example/bin/main_built_value.dart @@ -2,29 +2,32 @@ import 'dart:async'; import 'package:built_collection/built_collection.dart'; import 'package:built_value/serializer.dart'; +import 'package:built_value/standard_json_plugin.dart'; import 'package:chopper/chopper.dart'; import 'package:chopper_example/built_value_resource.dart'; import 'package:chopper_example/built_value_serializers.dart'; -import 'package:built_value/standard_json_plugin.dart'; import 'package:http/http.dart' as http; import 'package:http/testing.dart'; final jsonSerializers = - (serializers.toBuilder()..addPlugin(new StandardJsonPlugin())).build(); + (serializers.toBuilder()..addPlugin(StandardJsonPlugin())).build(); /// Simple client to have working example without remote server final client = MockClient((req) async { - if (req.method == 'POST') + if (req.method == 'POST') { return http.Response('{"type":"Fatal","message":"fatal erorr"}', 500); - if (req.url.path == '/resources/list') + } + if (req.url.path == '/resources/list') { return http.Response('[{"id":"1","name":"Foo"}]', 200); + } + return http.Response('{"id":"1","name":"Foo"}', 200); }); main() async { - final chopper = new ChopperClient( + final chopper = ChopperClient( client: client, - baseUrl: "http://localhost:8000", + baseUrl: 'http://localhost:8000', converter: BuiltValueConverter(), errorConverter: BuiltValueConverter(), services: [ @@ -35,7 +38,7 @@ main() async { final myService = chopper.getService(); - final response1 = await myService.getResource("1"); + final response1 = await myService.getResource('1'); print('response 1: ${response1.body}'); // undecoded String final response2 = await myService.getTypedResource(); @@ -46,8 +49,8 @@ main() async { try { final builder = ResourceBuilder() - ..id = "3" - ..name = "Super Name"; + ..id = '3' + ..name = 'Super Name'; await myService.newResource(builder.build()); } on Response catch (error) { print(error.body); @@ -58,12 +61,10 @@ class BuiltValueConverter extends JsonConverter { T? _deserialize(dynamic value) { final serializer = jsonSerializers.serializerForType(T) as Serializer?; if (serializer == null) { - throw Exception('No serializer for type ${T}'); + throw Exception('No serializer for type $T'); } - return jsonSerializers.deserializeWith( - serializer, - value, - ); + + return jsonSerializers.deserializeWith(serializer, value); } BuiltList _deserializeListOf(Iterable value) => BuiltList( @@ -77,20 +78,24 @@ class BuiltValueConverter extends JsonConverter { if (entity is T) return entity; try { - if (entity is List) return _deserializeListOf(entity); - return _deserialize(entity); + return entity is List + ? _deserializeListOf(entity) + : _deserialize(entity); } catch (e) { print(e); + return null; } } @override FutureOr> convertResponse( - Response response) async { + Response response, + ) async { // use [JsonConverter] to decode json - final jsonRes = await super.convertResponse(response); + final Response jsonRes = await super.convertResponse(response); final body = _decode(jsonRes.body); + return jsonRes.copyWith(body: body); } diff --git a/example/bin/main_json_serializable.dart b/example/bin/main_json_serializable.dart index 75851acd..956014f5 100644 --- a/example/bin/main_json_serializable.dart +++ b/example/bin/main_json_serializable.dart @@ -1,16 +1,19 @@ -import "dart:async"; +import 'dart:async'; + import 'package:chopper/chopper.dart'; import 'package:chopper_example/json_serializable.dart'; - import 'package:http/http.dart' as http; import 'package:http/testing.dart'; /// Simple client to have working example without remote server final client = MockClient((req) async { - if (req.method == 'POST') + if (req.method == 'POST') { return http.Response('{"type":"Fatal","message":"fatal erorr"}', 500); - if (req.method == 'GET' && req.headers['test'] == 'list') + } + if (req.method == 'GET' && req.headers['test'] == 'list') { return http.Response('[{"id":"1","name":"Foo"}]', 200); + } + return http.Response('{"id":"1","name":"Foo"}', 200); }); @@ -21,7 +24,7 @@ main() async { final chopper = ChopperClient( client: client, - baseUrl: "http://localhost:8000", + baseUrl: 'http://localhost:8000', // bind your object factories here converter: converter, errorConverter: converter, @@ -35,7 +38,7 @@ main() async { final myService = chopper.getService(); - final response1 = await myService.getResource("1"); + final response1 = await myService.getResource('1'); print('response 1: ${response1.body}'); // undecoded String final response2 = await myService.getResources(); @@ -44,11 +47,11 @@ main() async { final response3 = await myService.getTypedResource(); print('response 3: ${response3.body}'); // decoded Resource - final response4 = await myService.getMapResource("1"); + final response4 = await myService.getMapResource('1'); print('response 4: ${response4.body}'); // undecoded Resource try { - await myService.newResource(Resource("3", "Super Name")); + await myService.newResource(Resource('3', 'Super Name')); } on Response catch (error) { print(error.body); } @@ -56,8 +59,8 @@ main() async { Future authHeader(Request request) async => applyHeader( request, - "Authorization", - "42", + 'Authorization', + '42', ); typedef JsonFactory = T Function(Map json); @@ -92,7 +95,8 @@ class JsonSerializableConverter extends JsonConverter { @override FutureOr> convertResponse( - Response response) async { + Response response, + ) async { // use [JsonConverter] to decode json final jsonRes = await super.convertResponse(response); @@ -101,8 +105,10 @@ class JsonSerializableConverter extends JsonConverter { @override // all objects should implements toJson method + // ignore: unnecessary_overrides Request convertRequest(Request request) => super.convertRequest(request); + @override FutureOr convertError(Response response) async { // use [JsonConverter] to decode json final jsonRes = await super.convertError(response); diff --git a/example/lib/built_value_resource.dart b/example/lib/built_value_resource.dart index 6ea4e35b..9e30c3ed 100644 --- a/example/lib/built_value_resource.dart +++ b/example/lib/built_value_resource.dart @@ -7,44 +7,51 @@ import 'package:built_value/built_value.dart'; import 'package:built_value/serializer.dart'; import 'package:chopper/chopper.dart'; -part 'built_value_resource.g.dart'; part 'built_value_resource.chopper.dart'; +part 'built_value_resource.g.dart'; abstract class Resource implements Built { String get id; + String get name; static Serializer get serializer => _$resourceSerializer; - factory Resource([updates(ResourceBuilder b)]) = _$Resource; + factory Resource([Function(ResourceBuilder b) updates]) = _$Resource; + Resource._(); } abstract class ResourceError implements Built { String get type; + String get message; static Serializer get serializer => _$resourceErrorSerializer; - factory ResourceError([updates(ResourceErrorBuilder b)]) = _$ResourceError; + factory ResourceError([Function(ResourceErrorBuilder b) updates]) = + _$ResourceError; + ResourceError._(); } -@ChopperApi(baseUrl: "/resources") +@ChopperApi(baseUrl: '/resources') abstract class MyService extends ChopperService { static MyService create([ChopperClient? client]) => _$MyService(client); - @Get(path: "/{id}/") + @Get(path: '/{id}/') Future getResource(@Path() String id); - @Get(path: "/list") + @Get(path: '/list') Future>> getBuiltListResources(); - @Get(path: "/", headers: const {"foo": "bar"}) + @Get(path: '/', headers: {'foo': 'bar'}) Future> getTypedResource(); @Post() - Future> newResource(@Body() Resource resource, - {@Header() String? name}); + Future> newResource( + @Body() Resource resource, { + @Header() String? name, + }); } diff --git a/example/lib/built_value_resource.g.dart b/example/lib/built_value_resource.g.dart index 8030092d..bc969e05 100644 --- a/example/lib/built_value_resource.g.dart +++ b/example/lib/built_value_resource.g.dart @@ -36,17 +36,17 @@ class _$ResourceSerializer implements StructuredSerializer { final iterator = serialized.iterator; while (iterator.moveNext()) { - final key = iterator.current as String; + final key = iterator.current! as String; iterator.moveNext(); final Object? value = iterator.current; switch (key) { case 'id': result.id = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String))! as String; break; case 'name': result.name = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String))! as String; break; } } @@ -83,17 +83,17 @@ class _$ResourceErrorSerializer implements StructuredSerializer { final iterator = serialized.iterator; while (iterator.moveNext()) { - final key = iterator.current as String; + final key = iterator.current! as String; iterator.moveNext(); final Object? value = iterator.current; switch (key) { case 'type': result.type = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String))! as String; break; case 'message': result.message = serializers.deserialize(value, - specifiedType: const FullType(String)) as String; + specifiedType: const FullType(String))! as String; break; } } @@ -109,11 +109,11 @@ class _$Resource extends Resource { final String name; factory _$Resource([void Function(ResourceBuilder)? updates]) => - (new ResourceBuilder()..update(updates)).build(); + (new ResourceBuilder()..update(updates))._build(); _$Resource._({required this.id, required this.name}) : super._() { - BuiltValueNullFieldError.checkNotNull(id, 'Resource', 'id'); - BuiltValueNullFieldError.checkNotNull(name, 'Resource', 'name'); + BuiltValueNullFieldError.checkNotNull(id, r'Resource', 'id'); + BuiltValueNullFieldError.checkNotNull(name, r'Resource', 'name'); } @override @@ -136,7 +136,7 @@ class _$Resource extends Resource { @override String toString() { - return (newBuiltValueToStringHelper('Resource') + return (newBuiltValueToStringHelper(r'Resource') ..add('id', id) ..add('name', name)) .toString(); @@ -178,12 +178,14 @@ class ResourceBuilder implements Builder { } @override - _$Resource build() { + Resource build() => _build(); + + _$Resource _build() { final _$result = _$v ?? new _$Resource._( - id: BuiltValueNullFieldError.checkNotNull(id, 'Resource', 'id'), + id: BuiltValueNullFieldError.checkNotNull(id, r'Resource', 'id'), name: BuiltValueNullFieldError.checkNotNull( - name, 'Resource', 'name')); + name, r'Resource', 'name')); replace(_$result); return _$result; } @@ -196,11 +198,11 @@ class _$ResourceError extends ResourceError { final String message; factory _$ResourceError([void Function(ResourceErrorBuilder)? updates]) => - (new ResourceErrorBuilder()..update(updates)).build(); + (new ResourceErrorBuilder()..update(updates))._build(); _$ResourceError._({required this.type, required this.message}) : super._() { - BuiltValueNullFieldError.checkNotNull(type, 'ResourceError', 'type'); - BuiltValueNullFieldError.checkNotNull(message, 'ResourceError', 'message'); + BuiltValueNullFieldError.checkNotNull(type, r'ResourceError', 'type'); + BuiltValueNullFieldError.checkNotNull(message, r'ResourceError', 'message'); } @override @@ -225,7 +227,7 @@ class _$ResourceError extends ResourceError { @override String toString() { - return (newBuiltValueToStringHelper('ResourceError') + return (newBuiltValueToStringHelper(r'ResourceError') ..add('type', type) ..add('message', message)) .toString(); @@ -268,16 +270,18 @@ class ResourceErrorBuilder } @override - _$ResourceError build() { + ResourceError build() => _build(); + + _$ResourceError _build() { final _$result = _$v ?? new _$ResourceError._( type: BuiltValueNullFieldError.checkNotNull( - type, 'ResourceError', 'type'), + type, r'ResourceError', 'type'), message: BuiltValueNullFieldError.checkNotNull( - message, 'ResourceError', 'message')); + message, r'ResourceError', 'message')); replace(_$result); return _$result; } } -// ignore_for_file: always_put_control_body_on_new_line,always_specify_types,annotate_overrides,avoid_annotating_with_dynamic,avoid_as,avoid_catches_without_on_clauses,avoid_returning_this,deprecated_member_use_from_same_package,lines_longer_than_80_chars,omit_local_variable_types,prefer_expression_function_bodies,sort_constructors_first,test_types_in_equals,unnecessary_const,unnecessary_new +// ignore_for_file: always_put_control_body_on_new_line,always_specify_types,annotate_overrides,avoid_annotating_with_dynamic,avoid_as,avoid_catches_without_on_clauses,avoid_returning_this,deprecated_member_use_from_same_package,lines_longer_than_80_chars,no_leading_underscores_for_local_identifiers,omit_local_variable_types,prefer_expression_function_bodies,sort_constructors_first,test_types_in_equals,unnecessary_const,unnecessary_new,unnecessary_lambdas diff --git a/example/lib/built_value_serializers.dart b/example/lib/built_value_serializers.dart index cc8ef450..256983bf 100644 --- a/example/lib/built_value_serializers.dart +++ b/example/lib/built_value_serializers.dart @@ -1,12 +1,13 @@ library serializers; import 'package:built_value/serializer.dart'; + import 'built_value_resource.dart'; part 'built_value_serializers.g.dart'; /// Collection of generated serializers for the built_value chat example. -@SerializersFor(const [ +@SerializersFor([ Resource, ResourceError, ]) diff --git a/example/lib/built_value_serializers.g.dart b/example/lib/built_value_serializers.g.dart index dbad2232..699b3f08 100644 --- a/example/lib/built_value_serializers.g.dart +++ b/example/lib/built_value_serializers.g.dart @@ -11,4 +11,4 @@ Serializers _$serializers = (new Serializers().toBuilder() ..add(ResourceError.serializer)) .build(); -// ignore_for_file: always_put_control_body_on_new_line,always_specify_types,annotate_overrides,avoid_annotating_with_dynamic,avoid_as,avoid_catches_without_on_clauses,avoid_returning_this,deprecated_member_use_from_same_package,lines_longer_than_80_chars,omit_local_variable_types,prefer_expression_function_bodies,sort_constructors_first,test_types_in_equals,unnecessary_const,unnecessary_new +// ignore_for_file: always_put_control_body_on_new_line,always_specify_types,annotate_overrides,avoid_annotating_with_dynamic,avoid_as,avoid_catches_without_on_clauses,avoid_returning_this,deprecated_member_use_from_same_package,lines_longer_than_80_chars,no_leading_underscores_for_local_identifiers,omit_local_variable_types,prefer_expression_function_bodies,sort_constructors_first,test_types_in_equals,unnecessary_const,unnecessary_new,unnecessary_lambdas diff --git a/example/lib/json_serializable.dart b/example/lib/json_serializable.dart index 361f166a..4676bcab 100644 --- a/example/lib/json_serializable.dart +++ b/example/lib/json_serializable.dart @@ -3,8 +3,8 @@ import 'dart:async'; import 'package:chopper/chopper.dart'; import 'package:json_annotation/json_annotation.dart'; -part 'json_serializable.g.dart'; part 'json_serializable.chopper.dart'; +part 'json_serializable.g.dart'; @JsonSerializable() class Resource { @@ -33,23 +33,25 @@ class ResourceError { Map toJson() => _$ResourceErrorToJson(this); } -@ChopperApi(baseUrl: "/resources") +@ChopperApi(baseUrl: '/resources') abstract class MyService extends ChopperService { static MyService create([ChopperClient? client]) => _$MyService(client); - @Get(path: "/{id}/") + @Get(path: '/{id}/') Future getResource(@Path() String id); - @Get(path: "/all", headers: const {"test": "list"}) + @Get(path: '/all', headers: {'test': 'list'}) Future>> getResources(); - @Get(path: "/") + @Get(path: '/') Future> getMapResource(@Query() String id); - @Get(path: "/", headers: const {"foo": "bar"}) + @Get(path: '/', headers: {'foo': 'bar'}) Future> getTypedResource(); @Post() - Future> newResource(@Body() Resource resource, - {@Header() String? name}); + Future> newResource( + @Body() Resource resource, { + @Header() String? name, + }); } diff --git a/example/pubspec.lock b/example/pubspec.lock index fff2f649..572b2c64 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -7,14 +7,28 @@ packages: name: _fe_analyzer_shared url: "https://pub.dartlang.org" source: hosted - version: "34.0.0" + version: "40.0.0" analyzer: dependency: "direct main" description: name: analyzer url: "https://pub.dartlang.org" source: hosted - version: "3.2.0" + version: "4.1.0" + analyzer_plugin: + dependency: transitive + description: + name: analyzer_plugin + url: "https://pub.dartlang.org" + source: hosted + version: "0.10.0" + ansicolor: + dependency: transitive + description: + name: ansicolor + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.1" args: dependency: transitive description: @@ -35,7 +49,7 @@ packages: name: build url: "https://pub.dartlang.org" source: hosted - version: "2.2.1" + version: "2.3.0" build_config: dependency: transitive description: @@ -49,21 +63,21 @@ packages: name: build_daemon url: "https://pub.dartlang.org" source: hosted - version: "3.0.1" + version: "3.1.0" build_resolvers: dependency: transitive description: name: build_resolvers url: "https://pub.dartlang.org" source: hosted - version: "2.0.6" + version: "2.0.9" build_runner: dependency: "direct dev" description: name: build_runner url: "https://pub.dartlang.org" source: hosted - version: "2.1.7" + version: "2.1.11" build_runner_core: dependency: transitive description: @@ -72,7 +86,7 @@ packages: source: hosted version: "7.2.3" built_collection: - dependency: transitive + dependency: "direct main" description: name: built_collection url: "https://pub.dartlang.org" @@ -91,7 +105,7 @@ packages: name: built_value_generator url: "https://pub.dartlang.org" source: hosted - version: "8.1.4" + version: "8.4.0" charcode: dependency: transitive description: @@ -119,14 +133,7 @@ packages: path: "../chopper_generator" relative: true source: path - version: "4.0.2" - cli_util: - dependency: transitive - description: - name: cli_util - url: "https://pub.dartlang.org" - source: hosted - version: "0.3.5" + version: "4.0.3" code_builder: dependency: transitive description: @@ -155,13 +162,27 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "3.0.1" + csslib: + dependency: transitive + description: + name: csslib + url: "https://pub.dartlang.org" + source: hosted + version: "0.17.2" + dart_code_metrics: + dependency: "direct dev" + description: + name: dart_code_metrics + url: "https://pub.dartlang.org" + source: hosted + version: "4.16.0" dart_style: dependency: transitive description: name: dart_style url: "https://pub.dartlang.org" source: hosted - version: "2.2.1" + version: "2.2.3" file: dependency: transitive description: @@ -197,8 +218,15 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.0" - http: + html: dependency: transitive + description: + name: html + url: "https://pub.dartlang.org" + source: hosted + version: "0.15.0" + http: + dependency: "direct main" description: name: http url: "https://pub.dartlang.org" @@ -245,7 +273,14 @@ packages: name: json_serializable url: "https://pub.dartlang.org" source: hosted - version: "6.1.4" + version: "6.1.6" + lints: + dependency: "direct dev" + description: + name: lints + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" logging: dependency: transitive description: @@ -288,6 +323,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.8.1" + petitparser: + dependency: transitive + description: + name: petitparser + url: "https://pub.dartlang.org" + source: hosted + version: "4.4.0" pool: dependency: transitive description: @@ -336,14 +378,14 @@ packages: name: source_gen url: "https://pub.dartlang.org" source: hosted - version: "1.2.1" + version: "1.2.2" source_helper: dependency: transitive description: name: source_helper url: "https://pub.dartlang.org" source: hosted - version: "1.3.1" + version: "1.3.2" source_span: dependency: transitive description: @@ -414,6 +456,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.0" + xml: + dependency: transitive + description: + name: xml + url: "https://pub.dartlang.org" + source: hosted + version: "5.3.1" yaml: dependency: transitive description: @@ -422,4 +471,4 @@ packages: source: hosted version: "3.1.0" sdks: - dart: ">=2.16.0-100.0.dev <3.0.0" + dart: ">=2.17.0 <3.0.0" diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 9a88feed..9975770b 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -5,19 +5,23 @@ documentation: https://hadrien-lejard.gitbook.io/chopper/ #author: Hadrien Lejard environment: - sdk: '>=2.12.0 <3.0.0' + sdk: '>=2.17.0 <3.0.0' dependencies: chopper: json_annotation: built_value: analyzer: + http: + built_collection: dev_dependencies: build_runner: chopper_generator: json_serializable: built_value_generator: + dart_code_metrics: ^4.8.1 + lints: ^2.0.0 dependency_overrides: chopper: From 02cf2e003317e593176f40bed84cea8b772e4942 Mon Sep 17 00:00:00 2001 From: Ivan Terekhin Date: Tue, 13 Sep 2022 14:53:55 +0300 Subject: [PATCH 024/171] Version bumped for release (#352) --- chopper/CHANGELOG.md | 4 ++++ chopper/pubspec.yaml | 2 +- chopper_built_value/CHANGELOG.md | 4 ++++ chopper_built_value/pubspec.yaml | 4 ++-- chopper_generator/CHANGELOG.md | 4 ++++ chopper_generator/pubspec.yaml | 4 ++-- example/pubspec.lock | 2 +- 7 files changed, 18 insertions(+), 6 deletions(-) diff --git a/chopper/CHANGELOG.md b/chopper/CHANGELOG.md index 6af68839..e78e1909 100644 --- a/chopper/CHANGELOG.md +++ b/chopper/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 5.0.0 + +- API breaking changes (FutureOr) + ## 4.0.1 - Fix for the null safety support diff --git a/chopper/pubspec.yaml b/chopper/pubspec.yaml index cff2ec03..96817bb9 100644 --- a/chopper/pubspec.yaml +++ b/chopper/pubspec.yaml @@ -1,6 +1,6 @@ name: chopper description: Chopper is an http client generator using source_gen, inspired by Retrofit -version: 4.0.1 +version: 5.0.0 documentation: https://hadrien-lejard.gitbook.io/chopper repository: https://github.com/lejard-h/chopper diff --git a/chopper_built_value/CHANGELOG.md b/chopper_built_value/CHANGELOG.md index df5a3b34..e3a476bc 100644 --- a/chopper_built_value/CHANGELOG.md +++ b/chopper_built_value/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 1.1.0 + +- Chopper upgraded + ## 1.0.0 - Null safety support diff --git a/chopper_built_value/pubspec.yaml b/chopper_built_value/pubspec.yaml index b96ad512..0bd63c23 100644 --- a/chopper_built_value/pubspec.yaml +++ b/chopper_built_value/pubspec.yaml @@ -1,6 +1,6 @@ name: chopper_built_value description: A built_value based Converter for Chopper. -version: 1.0.0 +version: 1.1.0 documentation: https://hadrien-lejard.gitbook.io/chopper/converters/built-value-converter repository: https://github.com/lejard-h/chopper @@ -10,7 +10,7 @@ environment: dependencies: built_value: ^8.0.0 built_collection: ^5.0.0 - chopper: ^4.0.0 + chopper: ^5.0.0 http: ^0.13.0 dev_dependencies: diff --git a/chopper_generator/CHANGELOG.md b/chopper_generator/CHANGELOG.md index cec3211d..22074e42 100644 --- a/chopper_generator/CHANGELOG.md +++ b/chopper_generator/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 5.0.0 + +- API breaking changes (FutureOr usage) + ## 4.0.3 - Analyzer dependency upgrade diff --git a/chopper_generator/pubspec.yaml b/chopper_generator/pubspec.yaml index b868926a..a5804be1 100644 --- a/chopper_generator/pubspec.yaml +++ b/chopper_generator/pubspec.yaml @@ -1,6 +1,6 @@ name: chopper_generator description: Chopper is an http client generator using source_gen, inspired by Retrofit -version: 4.0.3 +version: 5.0.0 documentation: https://hadrien-lejard.gitbook.io/chopper repository: https://github.com/lejard-h/chopper @@ -11,7 +11,7 @@ dependencies: analyzer: ">=4.1.0 <4.3.0" build: ^2.0.0 built_collection: ^5.0.0 - chopper: ^4.0.0 + chopper: ^5.0.0 code_builder: ^4.1.0 dart_style: ^2.0.0 logging: ^1.0.0 diff --git a/example/pubspec.lock b/example/pubspec.lock index 572b2c64..f9411798 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -126,7 +126,7 @@ packages: path: "../chopper" relative: true source: path - version: "4.0.1" + version: "4.1.0" chopper_generator: dependency: "direct dev" description: From 34b3bda917fd187f3c40b21a76d1eca887de5910 Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Tue, 13 Sep 2022 15:17:28 +0100 Subject: [PATCH 025/171] Revert analyzer to ^4.1.0 and silence linters for Element.enclosingElement (#354) --- chopper_generator/lib/src/generator.dart | 4 ++++ chopper_generator/pubspec.yaml | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/chopper_generator/lib/src/generator.dart b/chopper_generator/lib/src/generator.dart index 13b3c606..4afa6fd3 100644 --- a/chopper_generator/lib/src/generator.dart +++ b/chopper_generator/lib/src/generator.dart @@ -339,8 +339,12 @@ class ChopperGenerator extends GeneratorForAnnotation { }); } + /// TODO: upgrade analyzer to ^4.4.0 to replace enclosingElement with enclosingElement3 + /// https://github.com/dart-lang/sdk/blob/main/pkg/analyzer/CHANGELOG.md#440 String _factoryForFunction(FunctionTypedElement function) => + // ignore: deprecated_member_use function.enclosingElement is ClassElement + // ignore: deprecated_member_use ? '${function.enclosingElement!.name}.${function.name}' : function.name!; diff --git a/chopper_generator/pubspec.yaml b/chopper_generator/pubspec.yaml index a5804be1..588ed462 100644 --- a/chopper_generator/pubspec.yaml +++ b/chopper_generator/pubspec.yaml @@ -1,6 +1,6 @@ name: chopper_generator description: Chopper is an http client generator using source_gen, inspired by Retrofit -version: 5.0.0 +version: 5.0.0+1 documentation: https://hadrien-lejard.gitbook.io/chopper repository: https://github.com/lejard-h/chopper @@ -8,7 +8,7 @@ environment: sdk: ">=2.17.0 <3.0.0" dependencies: - analyzer: ">=4.1.0 <4.3.0" + analyzer: ^4.1.0 build: ^2.0.0 built_collection: ^5.0.0 chopper: ^5.0.0 From 426a16e9580f07063b631e9179603805d198422e Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Sat, 17 Sep 2022 06:58:15 +0100 Subject: [PATCH 026/171] [chopper_generator] Update analyzer to ^4.4.0 and code_builde to ^4.3.0 and migrate deprecated code (#358) --- chopper/example/definition.chopper.dart | 83 ++- chopper/test/test_service.chopper.dart | 344 ++++++++++--- chopper_generator/analysis_options.yaml | 2 +- chopper_generator/lib/src/generator.dart | 85 ++-- chopper_generator/pubspec.yaml | 4 +- example/lib/built_value_resource.chopper.dart | 33 +- example/lib/json_serializable.chopper.dart | 41 +- example/pubspec.lock | 474 ------------------ 8 files changed, 452 insertions(+), 614 deletions(-) delete mode 100644 example/pubspec.lock diff --git a/chopper/example/definition.chopper.dart b/chopper/example/definition.chopper.dart index f43a754d..bae3d084 100644 --- a/chopper/example/definition.chopper.dart +++ b/chopper/example/definition.chopper.dart @@ -19,7 +19,11 @@ class _$MyService extends MyService { @override Future> getResource(String id) { final $url = '/resources/${id}'; - final $request = Request('GET', $url, client.baseUrl); + final $request = Request( + 'GET', + $url, + client.baseUrl, + ); return client.send($request); } @@ -31,47 +35,94 @@ class _$MyService extends MyService { 'foo': 'bar', }; - final $request = Request('GET', $url, client.baseUrl, - parameters: $params, headers: $headers); + final $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + headers: $headers, + ); return client.send, Map>($request); } @override Future>>> getListResources() { final $url = '/resources/resources'; - final $request = Request('GET', $url, client.baseUrl); + final $request = Request( + 'GET', + $url, + client.baseUrl, + ); return client .send>, Map>($request); } @override - Future> postResourceUrlEncoded(String toto, String b) { + Future> postResourceUrlEncoded( + String toto, + String b, + ) { final $url = '/resources/'; - final $body = {'a': toto, 'b': b}; - final $request = Request('POST', $url, client.baseUrl, body: $body); + final $body = { + 'a': toto, + 'b': b, + }; + final $request = Request( + 'POST', + $url, + client.baseUrl, + body: $body, + ); return client.send($request); } @override Future> postResources( - Map a, Map b, String c) { + Map a, + Map b, + String c, + ) { final $url = '/resources/multi'; final $parts = [ - PartValue>('1', a), - PartValue>('2', b), - PartValue('3', c) + PartValue>( + '1', + a, + ), + PartValue>( + '2', + b, + ), + PartValue( + '3', + c, + ), ]; - final $request = - Request('POST', $url, client.baseUrl, parts: $parts, multipart: true); + final $request = Request( + 'POST', + $url, + client.baseUrl, + parts: $parts, + multipart: true, + ); return client.send($request); } @override Future> postFile(List bytes) { final $url = '/resources/file'; - final $parts = [PartValue>('file', bytes)]; - final $request = - Request('POST', $url, client.baseUrl, parts: $parts, multipart: true); + final $parts = [ + PartValue>( + 'file', + bytes, + ) + ]; + final $request = Request( + 'POST', + $url, + client.baseUrl, + parts: $parts, + multipart: true, + ); return client.send($request); } } diff --git a/chopper/test/test_service.chopper.dart b/chopper/test/test_service.chopper.dart index f1fd6769..b49ddc66 100644 --- a/chopper/test/test_service.chopper.dart +++ b/chopper/test/test_service.chopper.dart @@ -17,61 +17,97 @@ class _$HttpTestService extends HttpTestService { final definitionType = HttpTestService; @override - Future> getTest(String id, {required String dynamicHeader}) { + Future> getTest( + String id, { + required String dynamicHeader, + }) { final $url = '/test/get/${id}'; final $headers = { 'test': dynamicHeader, }; - final $request = Request('GET', $url, client.baseUrl, headers: $headers); + final $request = Request( + 'GET', + $url, + client.baseUrl, + headers: $headers, + ); return client.send($request); } @override Future> headTest() { final $url = '/test/head'; - final $request = Request('HEAD', $url, client.baseUrl); + final $request = Request( + 'HEAD', + $url, + client.baseUrl, + ); return client.send($request); } @override Future> optionsTest() { final $url = '/test/options'; - final $request = Request('OPTIONS', $url, client.baseUrl); + final $request = Request( + 'OPTIONS', + $url, + client.baseUrl, + ); return client.send($request); } @override Future>>> getStreamTest() { final $url = '/test/get'; - final $request = Request('GET', $url, client.baseUrl); + final $request = Request( + 'GET', + $url, + client.baseUrl, + ); return client.send>, int>($request); } @override Future> getAll() { final $url = '/test'; - final $request = Request('GET', $url, client.baseUrl); + final $request = Request( + 'GET', + $url, + client.baseUrl, + ); return client.send($request); } @override Future> getAllWithTrailingSlash() { final $url = '/test/'; - final $request = Request('GET', $url, client.baseUrl); + final $request = Request( + 'GET', + $url, + client.baseUrl, + ); return client.send($request); } @override - Future> getQueryTest( - {String name = '', int? number, int? def = 42}) { + Future> getQueryTest({ + String name = '', + int? number, + int? def = 42, + }) { final $url = '/test/query'; final $params = { 'name': name, 'int': number, - 'default_value': def + 'default_value': def, }; - final $request = Request('GET', $url, client.baseUrl, parameters: $params); + final $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + ); return client.send($request); } @@ -79,47 +115,84 @@ class _$HttpTestService extends HttpTestService { Future> getQueryMapTest(Map query) { final $url = '/test/query_map'; final $params = query; - final $request = Request('GET', $url, client.baseUrl, parameters: $params); + final $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + ); return client.send($request); } @override - Future> getQueryMapTest2(Map query, - {bool? test}) { + Future> getQueryMapTest2( + Map query, { + bool? test, + }) { final $url = '/test/query_map'; final $params = {'test': test}; $params.addAll(query); - final $request = Request('GET', $url, client.baseUrl, parameters: $params); + final $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + ); return client.send($request); } @override - Future> getQueryMapTest3( - {String name = '', - int? number, - Map filters = const {}}) { + Future> getQueryMapTest3({ + String name = '', + int? number, + Map filters = const {}, + }) { final $url = '/test/query_map'; - final $params = {'name': name, 'number': number}; + final $params = { + 'name': name, + 'number': number, + }; $params.addAll(filters); - final $request = Request('GET', $url, client.baseUrl, parameters: $params); + final $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + ); return client.send($request); } @override - Future> getQueryMapTest4( - {String name = '', int? number, Map? filters}) { + Future> getQueryMapTest4({ + String name = '', + int? number, + Map? filters, + }) { final $url = '/test/query_map'; - final $params = {'name': name, 'number': number}; - $params.addAll(filters ?? {}); - final $request = Request('GET', $url, client.baseUrl, parameters: $params); + final $params = { + 'name': name, + 'number': number, + }; + $params.addAll(filters ?? const {}); + final $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + ); return client.send($request); } @override Future> getQueryMapTest5({Map? filters}) { final $url = '/test/query_map'; - final $params = filters ?? {}; - final $request = Request('GET', $url, client.baseUrl, parameters: $params); + final $params = filters ?? const {}; + final $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + ); return client.send($request); } @@ -127,7 +200,12 @@ class _$HttpTestService extends HttpTestService { Future> getBody(dynamic body) { final $url = '/test/get_body'; final $body = body; - final $request = Request('GET', $url, client.baseUrl, body: $body); + final $request = Request( + 'GET', + $url, + client.baseUrl, + body: $body, + ); return client.send($request); } @@ -135,7 +213,12 @@ class _$HttpTestService extends HttpTestService { Future> postTest(String data) { final $url = '/test/post'; final $body = data; - final $request = Request('POST', $url, client.baseUrl, body: $body); + final $request = Request( + 'POST', + $url, + client.baseUrl, + body: $body, + ); return client.send($request); } @@ -143,15 +226,28 @@ class _$HttpTestService extends HttpTestService { Future> postStreamTest(Stream> byteStream) { final $url = '/test/post'; final $body = byteStream; - final $request = Request('POST', $url, client.baseUrl, body: $body); + final $request = Request( + 'POST', + $url, + client.baseUrl, + body: $body, + ); return client.send($request); } @override - Future> putTest(String test, String data) { + Future> putTest( + String test, + String data, + ) { final $url = '/test/put/${test}'; final $body = data; - final $request = Request('PUT', $url, client.baseUrl, body: $body); + final $request = Request( + 'PUT', + $url, + client.baseUrl, + body: $body, + ); return client.send($request); } @@ -162,15 +258,28 @@ class _$HttpTestService extends HttpTestService { 'foo': 'bar', }; - final $request = Request('DELETE', $url, client.baseUrl, headers: $headers); + final $request = Request( + 'DELETE', + $url, + client.baseUrl, + headers: $headers, + ); return client.send($request); } @override - Future> patchTest(String id, String data) { + Future> patchTest( + String id, + String data, + ) { final $url = '/test/patch/${id}'; final $body = data; - final $request = Request('PATCH', $url, client.baseUrl, body: $body); + final $request = Request( + 'PATCH', + $url, + client.baseUrl, + body: $body, + ); return client.send($request); } @@ -178,7 +287,12 @@ class _$HttpTestService extends HttpTestService { Future> mapTest(Map map) { final $url = '/test/map'; final $body = map; - final $request = Request('POST', $url, client.baseUrl, body: $body); + final $request = Request( + 'POST', + $url, + client.baseUrl, + body: $body, + ); return client.send($request); } @@ -186,9 +300,16 @@ class _$HttpTestService extends HttpTestService { Future> postForm(Map fields) { final $url = '/test/form/body'; final $body = fields; - final $request = Request('POST', $url, client.baseUrl, body: $body); - return client.send($request, - requestConverter: convertForm); + final $request = Request( + 'POST', + $url, + client.baseUrl, + body: $body, + ); + return client.send( + $request, + requestConverter: convertForm, + ); } @override @@ -199,62 +320,123 @@ class _$HttpTestService extends HttpTestService { }; final $body = fields; - final $request = - Request('POST', $url, client.baseUrl, body: $body, headers: $headers); + final $request = Request( + 'POST', + $url, + client.baseUrl, + body: $body, + headers: $headers, + ); return client.send($request); } @override - Future> postFormFields(String foo, int bar) { + Future> postFormFields( + String foo, + int bar, + ) { final $url = '/test/form/body/fields'; - final $body = {'foo': foo, 'bar': bar}; - final $request = Request('POST', $url, client.baseUrl, body: $body); - return client.send($request, - requestConverter: convertForm); + final $body = { + 'foo': foo, + 'bar': bar, + }; + final $request = Request( + 'POST', + $url, + client.baseUrl, + body: $body, + ); + return client.send( + $request, + requestConverter: convertForm, + ); } @override Future> forceJsonTest(Map map) { final $url = '/test/map/json'; final $body = map; - final $request = Request('POST', $url, client.baseUrl, body: $body); - return client.send($request, - requestConverter: customConvertRequest, - responseConverter: customConvertResponse); + final $request = Request( + 'POST', + $url, + client.baseUrl, + body: $body, + ); + return client.send( + $request, + requestConverter: customConvertRequest, + responseConverter: customConvertResponse, + ); } @override Future> postResources( - Map a, Map b) { + Map a, + Map b, + ) { final $url = '/test/multi'; final $parts = [ - PartValue>('1', a), - PartValue>('2', b) + PartValue>( + '1', + a, + ), + PartValue>( + '2', + b, + ), ]; - final $request = - Request('POST', $url, client.baseUrl, parts: $parts, multipart: true); + final $request = Request( + 'POST', + $url, + client.baseUrl, + parts: $parts, + multipart: true, + ); return client.send($request); } @override Future> postFile(List bytes) { final $url = '/test/file'; - final $parts = [PartValueFile>('file', bytes)]; - final $request = - Request('POST', $url, client.baseUrl, parts: $parts, multipart: true); + final $parts = [ + PartValueFile>( + 'file', + bytes, + ) + ]; + final $request = Request( + 'POST', + $url, + client.baseUrl, + parts: $parts, + multipart: true, + ); return client.send($request); } @override - Future> postMultipartFile(MultipartFile file, - {String? id}) { + Future> postMultipartFile( + MultipartFile file, { + String? id, + }) { final $url = '/test/file'; final $parts = [ - PartValue('id', id), - PartValueFile('file', file) + PartValue( + 'id', + id, + ), + PartValueFile( + 'file', + file, + ), ]; - final $request = - Request('POST', $url, client.baseUrl, parts: $parts, multipart: true); + final $request = Request( + 'POST', + $url, + client.baseUrl, + parts: $parts, + multipart: true, + ); return client.send($request); } @@ -262,31 +444,51 @@ class _$HttpTestService extends HttpTestService { Future> postListFiles(List files) { final $url = '/test/files'; final $parts = [ - PartValueFile>('files', files) + PartValueFile>( + 'files', + files, + ) ]; - final $request = - Request('POST', $url, client.baseUrl, parts: $parts, multipart: true); + final $request = Request( + 'POST', + $url, + client.baseUrl, + parts: $parts, + multipart: true, + ); return client.send($request); } @override Future fullUrl() { final $url = 'https://test.com'; - final $request = Request('GET', $url, client.baseUrl); + final $request = Request( + 'GET', + $url, + client.baseUrl, + ); return client.send($request); } @override Future>> listString() { final $url = '/test/list/string'; - final $request = Request('GET', $url, client.baseUrl); + final $request = Request( + 'GET', + $url, + client.baseUrl, + ); return client.send, String>($request); } @override Future> noBody() { final $url = '/test/no-body'; - final $request = Request('POST', $url, client.baseUrl); + final $request = Request( + 'POST', + $url, + client.baseUrl, + ); return client.send($request); } } diff --git a/chopper_generator/analysis_options.yaml b/chopper_generator/analysis_options.yaml index 57256269..ead6c70c 100644 --- a/chopper_generator/analysis_options.yaml +++ b/chopper_generator/analysis_options.yaml @@ -15,7 +15,7 @@ dart_code_metrics: number-of-arguments: 4 maximum-nesting-level: 5 number-of-parameters: 5 - source-lines-of-code: 200 + source-lines-of-code: 250 metrics-exclude: - test/** rules: diff --git a/chopper_generator/lib/src/generator.dart b/chopper_generator/lib/src/generator.dart index 4afa6fd3..c5388f33 100644 --- a/chopper_generator/lib/src/generator.dart +++ b/chopper_generator/lib/src/generator.dart @@ -174,11 +174,13 @@ class ChopperGenerator extends GeneratorForAnnotation { ); final List blocks = [ - url.assignFinal(_urlVar).statement, + declareFinal(_urlVar).assign(url).statement, ]; if (queries.isNotEmpty) { - blocks.add(_generateMap(queries).assignFinal(_parametersVar).statement); + blocks.add( + declareFinal(_parametersVar).assign(_generateMap(queries)).statement, + ); } // Build an iterable of all the parameters that are nullable @@ -194,21 +196,20 @@ class ChopperGenerator extends GeneratorForAnnotation { [ // Check if the parameter is nullable optionalNullableParameters.contains(queryMap.keys.first) - ? refer(queryMap.keys.first).ifNullThen(refer('{}')) + ? refer(queryMap.keys.first).ifNullThen(refer('const {}')) : refer(queryMap.keys.first), ], ).statement); } else { blocks.add( - // Check if the parameter is nullable - optionalNullableParameters.contains(queryMap.keys.first) - ? refer(queryMap.keys.first) - .ifNullThen(refer('{}')) - .assignFinal(_parametersVar) - .statement - : refer(queryMap.keys.first) - .assignFinal(_parametersVar) - .statement, + declareFinal(_parametersVar) + .assign( + // Check if the parameter is nullable + optionalNullableParameters.contains(queryMap.keys.first) + ? refer(queryMap.keys.first).ifNullThen(refer('const {}')) + : refer(queryMap.keys.first), + ) + .statement, ); } } @@ -226,11 +227,11 @@ class ChopperGenerator extends GeneratorForAnnotation { if (hasBody) { if (body.isNotEmpty) { blocks.add( - refer(body.keys.first).assignFinal(_bodyVar).statement, + declareFinal(_bodyVar).assign(refer(body.keys.first)).statement, ); } else { blocks.add( - _generateMap(fields).assignFinal(_bodyVar).statement, + declareFinal(_bodyVar).assign(_generateMap(fields)).statement, ); } } @@ -243,7 +244,7 @@ class ChopperGenerator extends GeneratorForAnnotation { ).statement); } else { blocks.add( - refer(fieldMap.keys.first).assignFinal(_bodyVar).statement, + declareFinal(_bodyVar).assign(refer(fieldMap.keys.first)).statement, ); } } @@ -253,19 +254,23 @@ class ChopperGenerator extends GeneratorForAnnotation { bool hasParts = multipart && (parts.isNotEmpty || fileFields.isNotEmpty); if (hasParts) { blocks.add( - _generateList(parts, fileFields).assignFinal(_partsVar).statement, + declareFinal(_partsVar) + .assign(_generateList(parts, fileFields)) + .statement, ); } final bool hasPartMap = multipart && partMap.isNotEmpty; if (hasPartMap) { if (hasParts) { - blocks.add(refer('$_partsVar.addAll').call( - [refer(partMap.keys.first)], - ).statement); + blocks.add( + refer('$_partsVar.addAll').call( + [refer(partMap.keys.first)], + ).statement, + ); } else { blocks.add( - refer(partMap.keys.first).assignFinal(_partsVar).statement, + declareFinal(_partsVar).assign(refer(partMap.keys.first)).statement, ); } } @@ -273,12 +278,16 @@ class ChopperGenerator extends GeneratorForAnnotation { final bool hasFileFilesMap = multipart && fileFieldMap.isNotEmpty; if (hasFileFilesMap) { if (hasParts || hasPartMap) { - blocks.add(refer('$_partsVar.addAll').call( - [refer(fileFieldMap.keys.first)], - ).statement); + blocks.add( + refer('$_partsVar.addAll').call( + [refer(fileFieldMap.keys.first)], + ).statement, + ); } else { blocks.add( - refer(fileFieldMap.keys.first).assignFinal(_partsVar).statement, + declareFinal(_partsVar) + .assign(refer(fileFieldMap.keys.first)) + .statement, ); } } @@ -296,13 +305,19 @@ class ChopperGenerator extends GeneratorForAnnotation { ); } - blocks.add(_generateRequest( - method, - hasBody: hasBody, - useQueries: hasQuery, - useHeaders: headers != null, - hasParts: hasParts, - ).assignFinal(_requestVar).statement); + blocks.add( + declareFinal(_requestVar) + .assign( + _generateRequest( + method, + hasBody: hasBody, + useQueries: hasQuery, + useHeaders: headers != null, + hasParts: hasParts, + ), + ) + .statement, + ); final Map namedArguments = {}; @@ -339,13 +354,9 @@ class ChopperGenerator extends GeneratorForAnnotation { }); } - /// TODO: upgrade analyzer to ^4.4.0 to replace enclosingElement with enclosingElement3 - /// https://github.com/dart-lang/sdk/blob/main/pkg/analyzer/CHANGELOG.md#440 String _factoryForFunction(FunctionTypedElement function) => - // ignore: deprecated_member_use - function.enclosingElement is ClassElement - // ignore: deprecated_member_use - ? '${function.enclosingElement!.name}.${function.name}' + function.enclosingElement3 is ClassElement + ? '${function.enclosingElement3!.name}.${function.name}' : function.name!; Map _getAnnotation(MethodElement method, Type type) { diff --git a/chopper_generator/pubspec.yaml b/chopper_generator/pubspec.yaml index 588ed462..93386abb 100644 --- a/chopper_generator/pubspec.yaml +++ b/chopper_generator/pubspec.yaml @@ -8,11 +8,11 @@ environment: sdk: ">=2.17.0 <3.0.0" dependencies: - analyzer: ^4.1.0 + analyzer: ^4.4.0 build: ^2.0.0 built_collection: ^5.0.0 chopper: ^5.0.0 - code_builder: ^4.1.0 + code_builder: ^4.3.0 dart_style: ^2.0.0 logging: ^1.0.0 meta: ^1.3.0 diff --git a/example/lib/built_value_resource.chopper.dart b/example/lib/built_value_resource.chopper.dart index c092d4b0..24e1b894 100644 --- a/example/lib/built_value_resource.chopper.dart +++ b/example/lib/built_value_resource.chopper.dart @@ -19,14 +19,22 @@ class _$MyService extends MyService { @override Future> getResource(String id) { final $url = '/resources/${id}/'; - final $request = Request('GET', $url, client.baseUrl); + final $request = Request( + 'GET', + $url, + client.baseUrl, + ); return client.send($request); } @override Future>> getBuiltListResources() { final $url = '/resources/list'; - final $request = Request('GET', $url, client.baseUrl); + final $request = Request( + 'GET', + $url, + client.baseUrl, + ); return client.send, Resource>($request); } @@ -37,20 +45,33 @@ class _$MyService extends MyService { 'foo': 'bar', }; - final $request = Request('GET', $url, client.baseUrl, headers: $headers); + final $request = Request( + 'GET', + $url, + client.baseUrl, + headers: $headers, + ); return client.send($request); } @override - Future> newResource(Resource resource, {String? name}) { + Future> newResource( + Resource resource, { + String? name, + }) { final $url = '/resources'; final $headers = { if (name != null) 'name': name, }; final $body = resource; - final $request = - Request('POST', $url, client.baseUrl, body: $body, headers: $headers); + final $request = Request( + 'POST', + $url, + client.baseUrl, + body: $body, + headers: $headers, + ); return client.send($request); } } diff --git a/example/lib/json_serializable.chopper.dart b/example/lib/json_serializable.chopper.dart index bd44d2f3..8c6e6fbf 100644 --- a/example/lib/json_serializable.chopper.dart +++ b/example/lib/json_serializable.chopper.dart @@ -19,7 +19,11 @@ class _$MyService extends MyService { @override Future> getResource(String id) { final $url = '/resources/${id}/'; - final $request = Request('GET', $url, client.baseUrl); + final $request = Request( + 'GET', + $url, + client.baseUrl, + ); return client.send($request); } @@ -30,7 +34,12 @@ class _$MyService extends MyService { 'test': 'list', }; - final $request = Request('GET', $url, client.baseUrl, headers: $headers); + final $request = Request( + 'GET', + $url, + client.baseUrl, + headers: $headers, + ); return client.send, Resource>($request); } @@ -38,7 +47,12 @@ class _$MyService extends MyService { Future>> getMapResource(String id) { final $url = '/resources/'; final $params = {'id': id}; - final $request = Request('GET', $url, client.baseUrl, parameters: $params); + final $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + ); return client.send, Map>($request); } @@ -49,20 +63,33 @@ class _$MyService extends MyService { 'foo': 'bar', }; - final $request = Request('GET', $url, client.baseUrl, headers: $headers); + final $request = Request( + 'GET', + $url, + client.baseUrl, + headers: $headers, + ); return client.send($request); } @override - Future> newResource(Resource resource, {String? name}) { + Future> newResource( + Resource resource, { + String? name, + }) { final $url = '/resources'; final $headers = { if (name != null) 'name': name, }; final $body = resource; - final $request = - Request('POST', $url, client.baseUrl, body: $body, headers: $headers); + final $request = Request( + 'POST', + $url, + client.baseUrl, + body: $body, + headers: $headers, + ); return client.send($request); } } diff --git a/example/pubspec.lock b/example/pubspec.lock deleted file mode 100644 index f9411798..00000000 --- a/example/pubspec.lock +++ /dev/null @@ -1,474 +0,0 @@ -# Generated by pub -# See https://dart.dev/tools/pub/glossary#lockfile -packages: - _fe_analyzer_shared: - dependency: transitive - description: - name: _fe_analyzer_shared - url: "https://pub.dartlang.org" - source: hosted - version: "40.0.0" - analyzer: - dependency: "direct main" - description: - name: analyzer - url: "https://pub.dartlang.org" - source: hosted - version: "4.1.0" - analyzer_plugin: - dependency: transitive - description: - name: analyzer_plugin - url: "https://pub.dartlang.org" - source: hosted - version: "0.10.0" - ansicolor: - dependency: transitive - description: - name: ansicolor - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.1" - args: - dependency: transitive - description: - name: args - url: "https://pub.dartlang.org" - source: hosted - version: "2.3.0" - async: - dependency: transitive - description: - name: async - url: "https://pub.dartlang.org" - source: hosted - version: "2.8.2" - build: - dependency: transitive - description: - name: build - url: "https://pub.dartlang.org" - source: hosted - version: "2.3.0" - build_config: - dependency: transitive - description: - name: build_config - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.0" - build_daemon: - dependency: transitive - description: - name: build_daemon - url: "https://pub.dartlang.org" - source: hosted - version: "3.1.0" - build_resolvers: - dependency: transitive - description: - name: build_resolvers - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.9" - build_runner: - dependency: "direct dev" - description: - name: build_runner - url: "https://pub.dartlang.org" - source: hosted - version: "2.1.11" - build_runner_core: - dependency: transitive - description: - name: build_runner_core - url: "https://pub.dartlang.org" - source: hosted - version: "7.2.3" - built_collection: - dependency: "direct main" - description: - name: built_collection - url: "https://pub.dartlang.org" - source: hosted - version: "5.1.1" - built_value: - dependency: "direct main" - description: - name: built_value - url: "https://pub.dartlang.org" - source: hosted - version: "8.1.4" - built_value_generator: - dependency: "direct dev" - description: - name: built_value_generator - url: "https://pub.dartlang.org" - source: hosted - version: "8.4.0" - charcode: - dependency: transitive - description: - name: charcode - url: "https://pub.dartlang.org" - source: hosted - version: "1.3.1" - checked_yaml: - dependency: transitive - description: - name: checked_yaml - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.1" - chopper: - dependency: "direct main" - description: - path: "../chopper" - relative: true - source: path - version: "4.1.0" - chopper_generator: - dependency: "direct dev" - description: - path: "../chopper_generator" - relative: true - source: path - version: "4.0.3" - code_builder: - dependency: transitive - description: - name: code_builder - url: "https://pub.dartlang.org" - source: hosted - version: "4.1.0" - collection: - dependency: transitive - description: - name: collection - url: "https://pub.dartlang.org" - source: hosted - version: "1.15.0" - convert: - dependency: transitive - description: - name: convert - url: "https://pub.dartlang.org" - source: hosted - version: "3.0.1" - crypto: - dependency: transitive - description: - name: crypto - url: "https://pub.dartlang.org" - source: hosted - version: "3.0.1" - csslib: - dependency: transitive - description: - name: csslib - url: "https://pub.dartlang.org" - source: hosted - version: "0.17.2" - dart_code_metrics: - dependency: "direct dev" - description: - name: dart_code_metrics - url: "https://pub.dartlang.org" - source: hosted - version: "4.16.0" - dart_style: - dependency: transitive - description: - name: dart_style - url: "https://pub.dartlang.org" - source: hosted - version: "2.2.3" - file: - dependency: transitive - description: - name: file - url: "https://pub.dartlang.org" - source: hosted - version: "6.1.2" - fixnum: - dependency: transitive - description: - name: fixnum - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.0" - frontend_server_client: - dependency: transitive - description: - name: frontend_server_client - url: "https://pub.dartlang.org" - source: hosted - version: "2.1.2" - glob: - dependency: transitive - description: - name: glob - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.2" - graphs: - dependency: transitive - description: - name: graphs - url: "https://pub.dartlang.org" - source: hosted - version: "2.1.0" - html: - dependency: transitive - description: - name: html - url: "https://pub.dartlang.org" - source: hosted - version: "0.15.0" - http: - dependency: "direct main" - description: - name: http - url: "https://pub.dartlang.org" - source: hosted - version: "0.13.4" - http_multi_server: - dependency: transitive - description: - name: http_multi_server - url: "https://pub.dartlang.org" - source: hosted - version: "3.2.0" - http_parser: - dependency: transitive - description: - name: http_parser - url: "https://pub.dartlang.org" - source: hosted - version: "4.0.0" - io: - dependency: transitive - description: - name: io - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.3" - js: - dependency: transitive - description: - name: js - url: "https://pub.dartlang.org" - source: hosted - version: "0.6.4" - json_annotation: - dependency: "direct main" - description: - name: json_annotation - url: "https://pub.dartlang.org" - source: hosted - version: "4.4.0" - json_serializable: - dependency: "direct dev" - description: - name: json_serializable - url: "https://pub.dartlang.org" - source: hosted - version: "6.1.6" - lints: - dependency: "direct dev" - description: - name: lints - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.0" - logging: - dependency: transitive - description: - name: logging - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.2" - matcher: - dependency: transitive - description: - name: matcher - url: "https://pub.dartlang.org" - source: hosted - version: "0.12.11" - meta: - dependency: transitive - description: - name: meta - url: "https://pub.dartlang.org" - source: hosted - version: "1.7.0" - mime: - dependency: transitive - description: - name: mime - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.1" - package_config: - dependency: transitive - description: - name: package_config - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.2" - path: - dependency: transitive - description: - name: path - url: "https://pub.dartlang.org" - source: hosted - version: "1.8.1" - petitparser: - dependency: transitive - description: - name: petitparser - url: "https://pub.dartlang.org" - source: hosted - version: "4.4.0" - pool: - dependency: transitive - description: - name: pool - url: "https://pub.dartlang.org" - source: hosted - version: "1.5.0" - pub_semver: - dependency: transitive - description: - name: pub_semver - url: "https://pub.dartlang.org" - source: hosted - version: "2.1.0" - pubspec_parse: - dependency: transitive - description: - name: pubspec_parse - url: "https://pub.dartlang.org" - source: hosted - version: "1.2.0" - quiver: - dependency: transitive - description: - name: quiver - url: "https://pub.dartlang.org" - source: hosted - version: "3.0.1+1" - shelf: - dependency: transitive - description: - name: shelf - url: "https://pub.dartlang.org" - source: hosted - version: "1.2.0" - shelf_web_socket: - dependency: transitive - description: - name: shelf_web_socket - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.1" - source_gen: - dependency: transitive - description: - name: source_gen - url: "https://pub.dartlang.org" - source: hosted - version: "1.2.2" - source_helper: - dependency: transitive - description: - name: source_helper - url: "https://pub.dartlang.org" - source: hosted - version: "1.3.2" - source_span: - dependency: transitive - description: - name: source_span - url: "https://pub.dartlang.org" - source: hosted - version: "1.8.2" - stack_trace: - dependency: transitive - description: - name: stack_trace - url: "https://pub.dartlang.org" - source: hosted - version: "1.10.0" - stream_channel: - dependency: transitive - description: - name: stream_channel - url: "https://pub.dartlang.org" - source: hosted - version: "2.1.0" - stream_transform: - dependency: transitive - description: - name: stream_transform - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.0" - string_scanner: - dependency: transitive - description: - name: string_scanner - url: "https://pub.dartlang.org" - source: hosted - version: "1.1.0" - term_glyph: - dependency: transitive - description: - name: term_glyph - url: "https://pub.dartlang.org" - source: hosted - version: "1.2.0" - timing: - dependency: transitive - description: - name: timing - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.0" - typed_data: - dependency: transitive - description: - name: typed_data - url: "https://pub.dartlang.org" - source: hosted - version: "1.3.0" - watcher: - dependency: transitive - description: - name: watcher - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.1" - web_socket_channel: - dependency: transitive - description: - name: web_socket_channel - url: "https://pub.dartlang.org" - source: hosted - version: "2.1.0" - xml: - dependency: transitive - description: - name: xml - url: "https://pub.dartlang.org" - source: hosted - version: "5.3.1" - yaml: - dependency: transitive - description: - name: yaml - url: "https://pub.dartlang.org" - source: hosted - version: "3.1.0" -sdks: - dart: ">=2.17.0 <3.0.0" From 35fdf8f0534381b187f870b7ee3280bf6874d09d Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Sat, 17 Sep 2022 09:05:44 +0100 Subject: [PATCH 027/171] Add Makefiles to streamline development (#357) --- chopper/Makefile | 61 ++++++++++++++++++++++++++++++++++++ chopper_built_value/Makefile | 61 ++++++++++++++++++++++++++++++++++++ chopper_generator/Makefile | 46 +++++++++++++++++++++++++++ example/Makefile | 46 +++++++++++++++++++++++++++ tool/makefile_helpers.sh | 27 ++++++++++++++++ 5 files changed, 241 insertions(+) create mode 100644 chopper/Makefile create mode 100644 chopper_built_value/Makefile create mode 100644 chopper_generator/Makefile create mode 100644 example/Makefile create mode 100644 tool/makefile_helpers.sh diff --git a/chopper/Makefile b/chopper/Makefile new file mode 100644 index 00000000..d52b82d2 --- /dev/null +++ b/chopper/Makefile @@ -0,0 +1,61 @@ +# Makefile + +help: + @printf "%-20s %s\n" "Target" "Description" + @printf "%-20s %s\n" "------" "-----------" + @make -pqR : 2>/dev/null \ + | awk -v RS= -F: '/^# File/,/^# Finished Make data base/ {if ($$1 !~ "^[#.]") {print $$1}}' \ + | sort \ + | egrep -v -e '^[^[:alnum:]]' -e '^$@$$' \ + | xargs -I _ sh -c 'printf "%-20s " _; make _ -nB | (grep -i "^# Help:" || echo "") | tail -1 | sed "s/^# Help: //g"' + +analyze: + @# Help: Analyze the project's Dart code. + dart analyze --fatal-infos + +check_format: + @# Help: Check the formatting of one or more Dart files. + dart format --output=none --set-exit-if-changed . + +check_outdated: + @# Help: Check which of the project's packages are outdated. + dart pub outdated + +check_style: + @# Help: Analyze the project's Dart code and check the formatting one or more Dart files. + make analyze && make check_format + +code_gen: + @# Help: Run the build system for Dart code generation and modular compilation. + dart run build_runner build --delete-conflicting-outputs + +code_gen_watcher: + @# Help: Run the build system for Dart code generation and modular compilation as a watcher. + dart run build_runner watch --delete-conflicting-outputs + +format: + @# Help: Format one or more Dart files. + dart format . + +install: + @# Help: Install all the project's packages + dart pub get + +sure: + @# Help: Analyze the project's Dart code, check the formatting one or more Dart files and run unit tests for the current project. + make check_style && make tests + +show_test_coverage: + @# Help: Run Dart unit tests for the current project and show the coverage. + dart pub global activate coverage && dart pub global run coverage:test_with_coverage + lcov --remove coverage/lcov.info '**.g.dart' '**.mock.dart' '**.chopper.dart' -o coverage/lcov_without_generated_code.info + genhtml coverage/lcov_without_generated_code.info -o coverage/html + source ../tool/makefile_helpers.sh && open_link "coverage/html/index.html" + +tests: + @# Help: Run Dart unit and widget tests for the current project. + dart test + +upgrade: + @# Help: Upgrade all the project's packages. + dart pub upgrade \ No newline at end of file diff --git a/chopper_built_value/Makefile b/chopper_built_value/Makefile new file mode 100644 index 00000000..d52b82d2 --- /dev/null +++ b/chopper_built_value/Makefile @@ -0,0 +1,61 @@ +# Makefile + +help: + @printf "%-20s %s\n" "Target" "Description" + @printf "%-20s %s\n" "------" "-----------" + @make -pqR : 2>/dev/null \ + | awk -v RS= -F: '/^# File/,/^# Finished Make data base/ {if ($$1 !~ "^[#.]") {print $$1}}' \ + | sort \ + | egrep -v -e '^[^[:alnum:]]' -e '^$@$$' \ + | xargs -I _ sh -c 'printf "%-20s " _; make _ -nB | (grep -i "^# Help:" || echo "") | tail -1 | sed "s/^# Help: //g"' + +analyze: + @# Help: Analyze the project's Dart code. + dart analyze --fatal-infos + +check_format: + @# Help: Check the formatting of one or more Dart files. + dart format --output=none --set-exit-if-changed . + +check_outdated: + @# Help: Check which of the project's packages are outdated. + dart pub outdated + +check_style: + @# Help: Analyze the project's Dart code and check the formatting one or more Dart files. + make analyze && make check_format + +code_gen: + @# Help: Run the build system for Dart code generation and modular compilation. + dart run build_runner build --delete-conflicting-outputs + +code_gen_watcher: + @# Help: Run the build system for Dart code generation and modular compilation as a watcher. + dart run build_runner watch --delete-conflicting-outputs + +format: + @# Help: Format one or more Dart files. + dart format . + +install: + @# Help: Install all the project's packages + dart pub get + +sure: + @# Help: Analyze the project's Dart code, check the formatting one or more Dart files and run unit tests for the current project. + make check_style && make tests + +show_test_coverage: + @# Help: Run Dart unit tests for the current project and show the coverage. + dart pub global activate coverage && dart pub global run coverage:test_with_coverage + lcov --remove coverage/lcov.info '**.g.dart' '**.mock.dart' '**.chopper.dart' -o coverage/lcov_without_generated_code.info + genhtml coverage/lcov_without_generated_code.info -o coverage/html + source ../tool/makefile_helpers.sh && open_link "coverage/html/index.html" + +tests: + @# Help: Run Dart unit and widget tests for the current project. + dart test + +upgrade: + @# Help: Upgrade all the project's packages. + dart pub upgrade \ No newline at end of file diff --git a/chopper_generator/Makefile b/chopper_generator/Makefile new file mode 100644 index 00000000..dee4bcf7 --- /dev/null +++ b/chopper_generator/Makefile @@ -0,0 +1,46 @@ +# Makefile + +help: + @printf "%-20s %s\n" "Target" "Description" + @printf "%-20s %s\n" "------" "-----------" + @make -pqR : 2>/dev/null \ + | awk -v RS= -F: '/^# File/,/^# Finished Make data base/ {if ($$1 !~ "^[#.]") {print $$1}}' \ + | sort \ + | egrep -v -e '^[^[:alnum:]]' -e '^$@$$' \ + | xargs -I _ sh -c 'printf "%-20s " _; make _ -nB | (grep -i "^# Help:" || echo "") | tail -1 | sed "s/^# Help: //g"' + +analyze: + @# Help: Analyze the project's Dart code. + dart analyze --fatal-infos + +check_format: + @# Help: Check the formatting of one or more Dart files. + dart format --output=none --set-exit-if-changed . + +check_outdated: + @# Help: Check which of the project's packages are outdated. + dart pub outdated + +check_style: + @# Help: Analyze the project's Dart code and check the formatting one or more Dart files. + make analyze && make check_format + +code_gen: + @# Help: Run the build system for Dart code generation and modular compilation. + dart run build_runner build --delete-conflicting-outputs + +code_gen_watcher: + @# Help: Run the build system for Dart code generation and modular compilation as a watcher. + dart run build_runner watch --delete-conflicting-outputs + +format: + @# Help: Format one or more Dart files. + dart format . + +install: + @# Help: Install all the project's packages + dart pub get + +upgrade: + @# Help: Upgrade all the project's packages. + dart pub upgrade \ No newline at end of file diff --git a/example/Makefile b/example/Makefile new file mode 100644 index 00000000..dee4bcf7 --- /dev/null +++ b/example/Makefile @@ -0,0 +1,46 @@ +# Makefile + +help: + @printf "%-20s %s\n" "Target" "Description" + @printf "%-20s %s\n" "------" "-----------" + @make -pqR : 2>/dev/null \ + | awk -v RS= -F: '/^# File/,/^# Finished Make data base/ {if ($$1 !~ "^[#.]") {print $$1}}' \ + | sort \ + | egrep -v -e '^[^[:alnum:]]' -e '^$@$$' \ + | xargs -I _ sh -c 'printf "%-20s " _; make _ -nB | (grep -i "^# Help:" || echo "") | tail -1 | sed "s/^# Help: //g"' + +analyze: + @# Help: Analyze the project's Dart code. + dart analyze --fatal-infos + +check_format: + @# Help: Check the formatting of one or more Dart files. + dart format --output=none --set-exit-if-changed . + +check_outdated: + @# Help: Check which of the project's packages are outdated. + dart pub outdated + +check_style: + @# Help: Analyze the project's Dart code and check the formatting one or more Dart files. + make analyze && make check_format + +code_gen: + @# Help: Run the build system for Dart code generation and modular compilation. + dart run build_runner build --delete-conflicting-outputs + +code_gen_watcher: + @# Help: Run the build system for Dart code generation and modular compilation as a watcher. + dart run build_runner watch --delete-conflicting-outputs + +format: + @# Help: Format one or more Dart files. + dart format . + +install: + @# Help: Install all the project's packages + dart pub get + +upgrade: + @# Help: Upgrade all the project's packages. + dart pub upgrade \ No newline at end of file diff --git a/tool/makefile_helpers.sh b/tool/makefile_helpers.sh new file mode 100644 index 00000000..003ff2e4 --- /dev/null +++ b/tool/makefile_helpers.sh @@ -0,0 +1,27 @@ +#!/bin/bash + +# Opens a link with the default browser of OS (It works cross-platform) +# +## You can call it like `open_link balad.ir` to open balad website on your default browser +open_link() { + case "$(uname -s)" in + Darwin) + # macOS + open "$1" + ;; + + Linux) + # Linux: + xdg-open "$1" + ;; + + CYGWIN* | MINGW32* | MSYS* | MINGW*) + # Windows + start "$1" + ;; + + *) + echo 'Not supported OS' + ;; + esac +} From 5a59ebf19cdeb3405d5d6c034c46f2064ed5541f Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Sun, 18 Sep 2022 06:07:19 +0100 Subject: [PATCH 028/171] Add Bug Report Github issue template (#359) --- .github/ISSUE_TEMPLATE/bug_report.md | 69 ++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000..a67c8b85 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,69 @@ +--- +name: Bug report +about: The application is crashing or throws an exception or something else looks wrong. +title: '' +labels: bug +assignees: '' + +--- + +## Steps to Reproduce + + + +1. Execute `dart run` on the code sample +2. ... +3. ... + +**Expected results:** + +**Actual results:** + +
+Code sample + + + +```dart +``` + +
+ +
+ Logs + + + +``` +``` + + + +``` +``` + + + +``` +``` + +
From fcf347817c6437b4149c67eef53bff8130d6bbd4 Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Mon, 19 Sep 2022 07:12:38 +0100 Subject: [PATCH 029/171] [chopper_generator] Add types to the generated variables (#360) --- chopper/example/definition.chopper.dart | 33 ++-- chopper/test/test_service.chopper.dart | 149 +++++++++--------- chopper_generator/lib/src/generator.dart | 26 +-- example/lib/built_value_resource.chopper.dart | 22 ++- example/lib/json_serializable.chopper.dart | 31 ++-- 5 files changed, 130 insertions(+), 131 deletions(-) diff --git a/chopper/example/definition.chopper.dart b/chopper/example/definition.chopper.dart index bae3d084..884b3470 100644 --- a/chopper/example/definition.chopper.dart +++ b/chopper/example/definition.chopper.dart @@ -18,8 +18,8 @@ class _$MyService extends MyService { @override Future> getResource(String id) { - final $url = '/resources/${id}'; - final $request = Request( + final String $url = '/resources/${id}'; + final Request $request = Request( 'GET', $url, client.baseUrl, @@ -29,13 +29,12 @@ class _$MyService extends MyService { @override Future>> getMapResource(String id) { - final $url = '/resources/'; - final $params = {'id': id}; - final $headers = { + final String $url = '/resources/'; + final Map $params = {'id': id}; + final Map $headers = { 'foo': 'bar', }; - - final $request = Request( + final Request $request = Request( 'GET', $url, client.baseUrl, @@ -47,8 +46,8 @@ class _$MyService extends MyService { @override Future>>> getListResources() { - final $url = '/resources/resources'; - final $request = Request( + final String $url = '/resources/resources'; + final Request $request = Request( 'GET', $url, client.baseUrl, @@ -62,12 +61,12 @@ class _$MyService extends MyService { String toto, String b, ) { - final $url = '/resources/'; + final String $url = '/resources/'; final $body = { 'a': toto, 'b': b, }; - final $request = Request( + final Request $request = Request( 'POST', $url, client.baseUrl, @@ -82,8 +81,8 @@ class _$MyService extends MyService { Map b, String c, ) { - final $url = '/resources/multi'; - final $parts = [ + final String $url = '/resources/multi'; + final List $parts = [ PartValue>( '1', a, @@ -97,7 +96,7 @@ class _$MyService extends MyService { c, ), ]; - final $request = Request( + final Request $request = Request( 'POST', $url, client.baseUrl, @@ -109,14 +108,14 @@ class _$MyService extends MyService { @override Future> postFile(List bytes) { - final $url = '/resources/file'; - final $parts = [ + final String $url = '/resources/file'; + final List $parts = [ PartValue>( 'file', bytes, ) ]; - final $request = Request( + final Request $request = Request( 'POST', $url, client.baseUrl, diff --git a/chopper/test/test_service.chopper.dart b/chopper/test/test_service.chopper.dart index b49ddc66..01d5757e 100644 --- a/chopper/test/test_service.chopper.dart +++ b/chopper/test/test_service.chopper.dart @@ -21,12 +21,11 @@ class _$HttpTestService extends HttpTestService { String id, { required String dynamicHeader, }) { - final $url = '/test/get/${id}'; - final $headers = { + final String $url = '/test/get/${id}'; + final Map $headers = { 'test': dynamicHeader, }; - - final $request = Request( + final Request $request = Request( 'GET', $url, client.baseUrl, @@ -37,8 +36,8 @@ class _$HttpTestService extends HttpTestService { @override Future> headTest() { - final $url = '/test/head'; - final $request = Request( + final String $url = '/test/head'; + final Request $request = Request( 'HEAD', $url, client.baseUrl, @@ -48,8 +47,8 @@ class _$HttpTestService extends HttpTestService { @override Future> optionsTest() { - final $url = '/test/options'; - final $request = Request( + final String $url = '/test/options'; + final Request $request = Request( 'OPTIONS', $url, client.baseUrl, @@ -59,8 +58,8 @@ class _$HttpTestService extends HttpTestService { @override Future>>> getStreamTest() { - final $url = '/test/get'; - final $request = Request( + final String $url = '/test/get'; + final Request $request = Request( 'GET', $url, client.baseUrl, @@ -70,8 +69,8 @@ class _$HttpTestService extends HttpTestService { @override Future> getAll() { - final $url = '/test'; - final $request = Request( + final String $url = '/test'; + final Request $request = Request( 'GET', $url, client.baseUrl, @@ -81,8 +80,8 @@ class _$HttpTestService extends HttpTestService { @override Future> getAllWithTrailingSlash() { - final $url = '/test/'; - final $request = Request( + final String $url = '/test/'; + final Request $request = Request( 'GET', $url, client.baseUrl, @@ -96,13 +95,13 @@ class _$HttpTestService extends HttpTestService { int? number, int? def = 42, }) { - final $url = '/test/query'; - final $params = { + final String $url = '/test/query'; + final Map $params = { 'name': name, 'int': number, 'default_value': def, }; - final $request = Request( + final Request $request = Request( 'GET', $url, client.baseUrl, @@ -113,9 +112,9 @@ class _$HttpTestService extends HttpTestService { @override Future> getQueryMapTest(Map query) { - final $url = '/test/query_map'; - final $params = query; - final $request = Request( + final String $url = '/test/query_map'; + final Map $params = query; + final Request $request = Request( 'GET', $url, client.baseUrl, @@ -129,10 +128,10 @@ class _$HttpTestService extends HttpTestService { Map query, { bool? test, }) { - final $url = '/test/query_map'; - final $params = {'test': test}; + final String $url = '/test/query_map'; + final Map $params = {'test': test}; $params.addAll(query); - final $request = Request( + final Request $request = Request( 'GET', $url, client.baseUrl, @@ -147,13 +146,13 @@ class _$HttpTestService extends HttpTestService { int? number, Map filters = const {}, }) { - final $url = '/test/query_map'; - final $params = { + final String $url = '/test/query_map'; + final Map $params = { 'name': name, 'number': number, }; $params.addAll(filters); - final $request = Request( + final Request $request = Request( 'GET', $url, client.baseUrl, @@ -168,13 +167,13 @@ class _$HttpTestService extends HttpTestService { int? number, Map? filters, }) { - final $url = '/test/query_map'; - final $params = { + final String $url = '/test/query_map'; + final Map $params = { 'name': name, 'number': number, }; $params.addAll(filters ?? const {}); - final $request = Request( + final Request $request = Request( 'GET', $url, client.baseUrl, @@ -185,9 +184,9 @@ class _$HttpTestService extends HttpTestService { @override Future> getQueryMapTest5({Map? filters}) { - final $url = '/test/query_map'; - final $params = filters ?? const {}; - final $request = Request( + final String $url = '/test/query_map'; + final Map $params = filters ?? const {}; + final Request $request = Request( 'GET', $url, client.baseUrl, @@ -198,9 +197,9 @@ class _$HttpTestService extends HttpTestService { @override Future> getBody(dynamic body) { - final $url = '/test/get_body'; + final String $url = '/test/get_body'; final $body = body; - final $request = Request( + final Request $request = Request( 'GET', $url, client.baseUrl, @@ -211,9 +210,9 @@ class _$HttpTestService extends HttpTestService { @override Future> postTest(String data) { - final $url = '/test/post'; + final String $url = '/test/post'; final $body = data; - final $request = Request( + final Request $request = Request( 'POST', $url, client.baseUrl, @@ -224,9 +223,9 @@ class _$HttpTestService extends HttpTestService { @override Future> postStreamTest(Stream> byteStream) { - final $url = '/test/post'; + final String $url = '/test/post'; final $body = byteStream; - final $request = Request( + final Request $request = Request( 'POST', $url, client.baseUrl, @@ -240,9 +239,9 @@ class _$HttpTestService extends HttpTestService { String test, String data, ) { - final $url = '/test/put/${test}'; + final String $url = '/test/put/${test}'; final $body = data; - final $request = Request( + final Request $request = Request( 'PUT', $url, client.baseUrl, @@ -253,12 +252,11 @@ class _$HttpTestService extends HttpTestService { @override Future> deleteTest(String id) { - final $url = '/test/delete/${id}'; - final $headers = { + final String $url = '/test/delete/${id}'; + final Map $headers = { 'foo': 'bar', }; - - final $request = Request( + final Request $request = Request( 'DELETE', $url, client.baseUrl, @@ -272,9 +270,9 @@ class _$HttpTestService extends HttpTestService { String id, String data, ) { - final $url = '/test/patch/${id}'; + final String $url = '/test/patch/${id}'; final $body = data; - final $request = Request( + final Request $request = Request( 'PATCH', $url, client.baseUrl, @@ -285,9 +283,9 @@ class _$HttpTestService extends HttpTestService { @override Future> mapTest(Map map) { - final $url = '/test/map'; + final String $url = '/test/map'; final $body = map; - final $request = Request( + final Request $request = Request( 'POST', $url, client.baseUrl, @@ -298,9 +296,9 @@ class _$HttpTestService extends HttpTestService { @override Future> postForm(Map fields) { - final $url = '/test/form/body'; + final String $url = '/test/form/body'; final $body = fields; - final $request = Request( + final Request $request = Request( 'POST', $url, client.baseUrl, @@ -314,13 +312,12 @@ class _$HttpTestService extends HttpTestService { @override Future> postFormUsingHeaders(Map fields) { - final $url = '/test/form/body'; - final $headers = { + final String $url = '/test/form/body'; + final Map $headers = { 'content-type': 'application/x-www-form-urlencoded', }; - final $body = fields; - final $request = Request( + final Request $request = Request( 'POST', $url, client.baseUrl, @@ -335,12 +332,12 @@ class _$HttpTestService extends HttpTestService { String foo, int bar, ) { - final $url = '/test/form/body/fields'; + final String $url = '/test/form/body/fields'; final $body = { 'foo': foo, 'bar': bar, }; - final $request = Request( + final Request $request = Request( 'POST', $url, client.baseUrl, @@ -354,9 +351,9 @@ class _$HttpTestService extends HttpTestService { @override Future> forceJsonTest(Map map) { - final $url = '/test/map/json'; + final String $url = '/test/map/json'; final $body = map; - final $request = Request( + final Request $request = Request( 'POST', $url, client.baseUrl, @@ -374,8 +371,8 @@ class _$HttpTestService extends HttpTestService { Map a, Map b, ) { - final $url = '/test/multi'; - final $parts = [ + final String $url = '/test/multi'; + final List $parts = [ PartValue>( '1', a, @@ -385,7 +382,7 @@ class _$HttpTestService extends HttpTestService { b, ), ]; - final $request = Request( + final Request $request = Request( 'POST', $url, client.baseUrl, @@ -397,14 +394,14 @@ class _$HttpTestService extends HttpTestService { @override Future> postFile(List bytes) { - final $url = '/test/file'; - final $parts = [ + final String $url = '/test/file'; + final List $parts = [ PartValueFile>( 'file', bytes, ) ]; - final $request = Request( + final Request $request = Request( 'POST', $url, client.baseUrl, @@ -419,8 +416,8 @@ class _$HttpTestService extends HttpTestService { MultipartFile file, { String? id, }) { - final $url = '/test/file'; - final $parts = [ + final String $url = '/test/file'; + final List $parts = [ PartValue( 'id', id, @@ -430,7 +427,7 @@ class _$HttpTestService extends HttpTestService { file, ), ]; - final $request = Request( + final Request $request = Request( 'POST', $url, client.baseUrl, @@ -442,14 +439,14 @@ class _$HttpTestService extends HttpTestService { @override Future> postListFiles(List files) { - final $url = '/test/files'; - final $parts = [ + final String $url = '/test/files'; + final List $parts = [ PartValueFile>( 'files', files, ) ]; - final $request = Request( + final Request $request = Request( 'POST', $url, client.baseUrl, @@ -461,8 +458,8 @@ class _$HttpTestService extends HttpTestService { @override Future fullUrl() { - final $url = 'https://test.com'; - final $request = Request( + final String $url = 'https://test.com'; + final Request $request = Request( 'GET', $url, client.baseUrl, @@ -472,8 +469,8 @@ class _$HttpTestService extends HttpTestService { @override Future>> listString() { - final $url = '/test/list/string'; - final $request = Request( + final String $url = '/test/list/string'; + final Request $request = Request( 'GET', $url, client.baseUrl, @@ -483,8 +480,8 @@ class _$HttpTestService extends HttpTestService { @override Future> noBody() { - final $url = '/test/no-body'; - final $request = Request( + final String $url = '/test/no-body'; + final Request $request = Request( 'POST', $url, client.baseUrl, diff --git a/chopper_generator/lib/src/generator.dart b/chopper_generator/lib/src/generator.dart index c5388f33..4badfc70 100644 --- a/chopper_generator/lib/src/generator.dart +++ b/chopper_generator/lib/src/generator.dart @@ -174,12 +174,14 @@ class ChopperGenerator extends GeneratorForAnnotation { ); final List blocks = [ - declareFinal(_urlVar).assign(url).statement, + declareFinal(_urlVar, type: refer('String')).assign(url).statement, ]; if (queries.isNotEmpty) { blocks.add( - declareFinal(_parametersVar).assign(_generateMap(queries)).statement, + declareFinal(_parametersVar, type: refer('Map')) + .assign(_generateMap(queries)) + .statement, ); } @@ -202,7 +204,7 @@ class ChopperGenerator extends GeneratorForAnnotation { ).statement); } else { blocks.add( - declareFinal(_parametersVar) + declareFinal(_parametersVar, type: refer('Map')) .assign( // Check if the parameter is nullable optionalNullableParameters.contains(queryMap.keys.first) @@ -254,7 +256,7 @@ class ChopperGenerator extends GeneratorForAnnotation { bool hasParts = multipart && (parts.isNotEmpty || fileFields.isNotEmpty); if (hasParts) { blocks.add( - declareFinal(_partsVar) + declareFinal(_partsVar, type: refer('List')) .assign(_generateList(parts, fileFields)) .statement, ); @@ -270,7 +272,9 @@ class ChopperGenerator extends GeneratorForAnnotation { ); } else { blocks.add( - declareFinal(_partsVar).assign(refer(partMap.keys.first)).statement, + declareFinal(_partsVar, type: refer('List')) + .assign(refer(partMap.keys.first)) + .statement, ); } } @@ -285,7 +289,7 @@ class ChopperGenerator extends GeneratorForAnnotation { ); } else { blocks.add( - declareFinal(_partsVar) + declareFinal(_partsVar, type: refer('List')) .assign(refer(fileFieldMap.keys.first)) .statement, ); @@ -306,7 +310,7 @@ class ChopperGenerator extends GeneratorForAnnotation { } blocks.add( - declareFinal(_requestVar) + declareFinal(_requestVar, type: refer('Request')) .assign( _generateRequest( method, @@ -589,10 +593,14 @@ class ChopperGenerator extends GeneratorForAnnotation { } }); - codeBuffer.writeln('};'); + codeBuffer.writeln('}'); final String code = codeBuffer.toString(); - return code == '{\n};\n' ? null : Code('final $_headersVar = $code'); + return code == '{\n}\n' + ? null + : declareFinal(_headersVar, type: refer('Map')) + .assign(CodeExpression(Code(code))) + .statement; } } diff --git a/example/lib/built_value_resource.chopper.dart b/example/lib/built_value_resource.chopper.dart index 24e1b894..a68d4b5c 100644 --- a/example/lib/built_value_resource.chopper.dart +++ b/example/lib/built_value_resource.chopper.dart @@ -18,8 +18,8 @@ class _$MyService extends MyService { @override Future> getResource(String id) { - final $url = '/resources/${id}/'; - final $request = Request( + final String $url = '/resources/${id}/'; + final Request $request = Request( 'GET', $url, client.baseUrl, @@ -29,8 +29,8 @@ class _$MyService extends MyService { @override Future>> getBuiltListResources() { - final $url = '/resources/list'; - final $request = Request( + final String $url = '/resources/list'; + final Request $request = Request( 'GET', $url, client.baseUrl, @@ -40,12 +40,11 @@ class _$MyService extends MyService { @override Future> getTypedResource() { - final $url = '/resources/'; - final $headers = { + final String $url = '/resources/'; + final Map $headers = { 'foo': 'bar', }; - - final $request = Request( + final Request $request = Request( 'GET', $url, client.baseUrl, @@ -59,13 +58,12 @@ class _$MyService extends MyService { Resource resource, { String? name, }) { - final $url = '/resources'; - final $headers = { + final String $url = '/resources'; + final Map $headers = { if (name != null) 'name': name, }; - final $body = resource; - final $request = Request( + final Request $request = Request( 'POST', $url, client.baseUrl, diff --git a/example/lib/json_serializable.chopper.dart b/example/lib/json_serializable.chopper.dart index 8c6e6fbf..e9892bfc 100644 --- a/example/lib/json_serializable.chopper.dart +++ b/example/lib/json_serializable.chopper.dart @@ -18,8 +18,8 @@ class _$MyService extends MyService { @override Future> getResource(String id) { - final $url = '/resources/${id}/'; - final $request = Request( + final String $url = '/resources/${id}/'; + final Request $request = Request( 'GET', $url, client.baseUrl, @@ -29,12 +29,11 @@ class _$MyService extends MyService { @override Future>> getResources() { - final $url = '/resources/all'; - final $headers = { + final String $url = '/resources/all'; + final Map $headers = { 'test': 'list', }; - - final $request = Request( + final Request $request = Request( 'GET', $url, client.baseUrl, @@ -45,9 +44,9 @@ class _$MyService extends MyService { @override Future>> getMapResource(String id) { - final $url = '/resources/'; - final $params = {'id': id}; - final $request = Request( + final String $url = '/resources/'; + final Map $params = {'id': id}; + final Request $request = Request( 'GET', $url, client.baseUrl, @@ -58,12 +57,11 @@ class _$MyService extends MyService { @override Future> getTypedResource() { - final $url = '/resources/'; - final $headers = { + final String $url = '/resources/'; + final Map $headers = { 'foo': 'bar', }; - - final $request = Request( + final Request $request = Request( 'GET', $url, client.baseUrl, @@ -77,13 +75,12 @@ class _$MyService extends MyService { Resource resource, { String? name, }) { - final $url = '/resources'; - final $headers = { + final String $url = '/resources'; + final Map $headers = { if (name != null) 'name': name, }; - final $body = resource; - final $request = Request( + final Request $request = Request( 'POST', $url, client.baseUrl, From 6b3b8407d99844d89c800e8c517a72ca03c60b57 Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Mon, 19 Sep 2022 10:33:53 +0100 Subject: [PATCH 030/171] Provide an example using an Isolate Worker Pool with Squadron (#361) --- ...son_serializable_squadron_worker_pool.dart | 164 +++++++++++++++ .../lib/json_decode_service.activator.g.dart | 9 + example/lib/json_decode_service.dart | 21 ++ example/lib/json_decode_service.vm.g.dart | 13 ++ example/lib/json_decode_service.worker.g.dart | 59 ++++++ example/pubspec.yaml | 2 + faq.md | 194 ++++++++++++++++++ 7 files changed, 462 insertions(+) create mode 100644 example/bin/main_json_serializable_squadron_worker_pool.dart create mode 100644 example/lib/json_decode_service.activator.g.dart create mode 100644 example/lib/json_decode_service.dart create mode 100644 example/lib/json_decode_service.vm.g.dart create mode 100644 example/lib/json_decode_service.worker.g.dart diff --git a/example/bin/main_json_serializable_squadron_worker_pool.dart b/example/bin/main_json_serializable_squadron_worker_pool.dart new file mode 100644 index 00000000..2c3bbb08 --- /dev/null +++ b/example/bin/main_json_serializable_squadron_worker_pool.dart @@ -0,0 +1,164 @@ +/// This example uses +/// - https://github.com/google/json_serializable.dart +/// - https://github.com/d-markey/squadron +/// - https://github.com/d-markey/squadron_builder + +import 'dart:async' show FutureOr; +import 'dart:convert' show jsonDecode; + +import 'package:chopper/chopper.dart'; +import 'package:chopper_example/json_decode_service.dart'; +import 'package:chopper_example/json_serializable.dart'; +import 'package:http/testing.dart'; +import 'package:squadron/squadron.dart'; +import 'package:http/http.dart' as http; + +import 'main_json_serializable.dart' show authHeader; + +typedef JsonFactory = T Function(Map json); + +/// This JsonConverter works with or without a WorkerPool +class JsonSerializableWorkerPoolConverter extends JsonConverter { + const JsonSerializableWorkerPoolConverter(this.factories, [this.workerPool]); + + final Map factories; + final JsonDecodeServiceWorkerPool? workerPool; + + T? _decodeMap(Map values) { + /// Get jsonFactory using Type parameters + /// if not found or invalid, throw error or return null + final jsonFactory = factories[T]; + if (jsonFactory == null || jsonFactory is! JsonFactory) { + /// throw serializer not found error; + return null; + } + + return jsonFactory(values); + } + + List _decodeList(Iterable values) => + values.where((v) => v != null).map((v) => _decode(v)).toList(); + + dynamic _decode(entity) { + if (entity is Iterable) return _decodeList(entity as List); + + if (entity is Map) return _decodeMap(entity as Map); + + return entity; + } + + @override + FutureOr> convertResponse( + Response response, + ) async { + // use [JsonConverter] to decode json + final jsonRes = await super.convertResponse(response); + + return jsonRes.copyWith(body: _decode(jsonRes.body)); + } + + @override + FutureOr convertError(Response response) async { + // use [JsonConverter] to decode json + final jsonRes = await super.convertError(response); + + return jsonRes.copyWith( + body: ResourceError.fromJsonFactory(jsonRes.body), + ); + } + + @override + FutureOr tryDecodeJson(String data) async { + try { + // if there is a worker pool use it, otherwise run in the main thread + return workerPool != null + ? await workerPool!.jsonDecode(data) + : jsonDecode(data); + } catch (error) { + print(error); + + chopperLogger.warning(error); + + return data; + } + } +} + +/// Simple client to have working example without remote server +final client = MockClient((http.Request req) async { + if (req.method == 'POST') { + return http.Response('{"type":"Fatal","message":"fatal error"}', 500); + } + if (req.method == 'GET' && req.headers['test'] == 'list') { + return http.Response('[{"id":"1","name":"Foo"}]', 200); + } + + return http.Response('{"id":"1","name":"Foo"}', 200); +}); + +/// inspired by https://github.com/d-markey/squadron_sample/blob/main/lib/main.dart +void initSquadron(String id) { + Squadron.setId(id); + Squadron.setLogger(ConsoleSquadronLogger()); + Squadron.logLevel = SquadronLogLevel.all; + Squadron.debugMode = true; +} + +Future main() async { + /// initialize Squadron before using it + initSquadron('worker_pool_example'); + + final jsonDecodeServiceWorkerPool = JsonDecodeServiceWorkerPool( + // Set whatever you want here + concurrencySettings: ConcurrencySettings.oneCpuThread, + ); + + /// start the Worker Pool + await jsonDecodeServiceWorkerPool.start(); + + final converter = JsonSerializableWorkerPoolConverter( + { + Resource: Resource.fromJsonFactory, + }, + // make sure to provide the WorkerPool to the JsonConverter + jsonDecodeServiceWorkerPool, + ); + + final chopper = ChopperClient( + client: client, + baseUrl: 'http://localhost:8000', + // bind your object factories here + converter: converter, + errorConverter: converter, + services: [ + // the generated service + MyService.create(), + ], + /* ResponseInterceptorFunc | RequestInterceptorFunc | ResponseInterceptor | RequestInterceptor */ + interceptors: [authHeader], + ); + + final myService = chopper.getService(); + + /// All of the calls below will use jsonDecode in an Isolate worker + final response1 = await myService.getResource('1'); + print('response 1: ${response1.body}'); // undecoded String + + final response2 = await myService.getResources(); + print('response 2: ${response2.body}'); // decoded list of Resources + + final response3 = await myService.getTypedResource(); + print('response 3: ${response3.body}'); // decoded Resource + + final response4 = await myService.getMapResource('1'); + print('response 4: ${response4.body}'); // undecoded Resource + + try { + await myService.newResource(Resource('3', 'Super Name')); + } on Response catch (error) { + print(error.body); + } + + /// stop the Worker Pool + jsonDecodeServiceWorkerPool.stop(); +} diff --git a/example/lib/json_decode_service.activator.g.dart b/example/lib/json_decode_service.activator.g.dart new file mode 100644 index 00000000..1756d3d3 --- /dev/null +++ b/example/lib/json_decode_service.activator.g.dart @@ -0,0 +1,9 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +// ************************************************************************** +// SquadronWorkerGenerator +// ************************************************************************** + +import 'json_decode_service.vm.g.dart'; + +final $JsonDecodeServiceActivator = $getJsonDecodeServiceActivator(); diff --git a/example/lib/json_decode_service.dart b/example/lib/json_decode_service.dart new file mode 100644 index 00000000..41c94785 --- /dev/null +++ b/example/lib/json_decode_service.dart @@ -0,0 +1,21 @@ +/// This example uses https://github.com/d-markey/squadron_builder + +import 'dart:async'; +import 'dart:convert' show json; + +import 'package:squadron/squadron.dart'; +import 'package:squadron/squadron_annotations.dart'; + +import 'json_decode_service.activator.g.dart'; + +part 'json_decode_service.worker.g.dart'; + +@SquadronService( + // disable web to keep the number of generated files low for this example + web: false, +) +class JsonDecodeService extends WorkerService + with $JsonDecodeServiceOperations { + @SquadronMethod() + Future jsonDecode(String source) async => json.decode(source); +} diff --git a/example/lib/json_decode_service.vm.g.dart b/example/lib/json_decode_service.vm.g.dart new file mode 100644 index 00000000..7ad9a226 --- /dev/null +++ b/example/lib/json_decode_service.vm.g.dart @@ -0,0 +1,13 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +// ************************************************************************** +// SquadronWorkerGenerator +// ************************************************************************** + +import 'package:squadron/squadron_service.dart'; +import 'json_decode_service.dart'; + +// VM entry point +void _start(Map command) => run($JsonDecodeServiceInitializer, command); + +dynamic $getJsonDecodeServiceActivator() => _start; diff --git a/example/lib/json_decode_service.worker.g.dart b/example/lib/json_decode_service.worker.g.dart new file mode 100644 index 00000000..d50372ab --- /dev/null +++ b/example/lib/json_decode_service.worker.g.dart @@ -0,0 +1,59 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'json_decode_service.dart'; + +// ************************************************************************** +// SquadronWorkerGenerator +// ************************************************************************** + +// Operations map for JsonDecodeService +mixin $JsonDecodeServiceOperations on WorkerService { + @override + late final Map operations = + _getOperations(this as JsonDecodeService); + + static const int _$jsonDecodeId = 1; + + static Map _getOperations(JsonDecodeService svc) => { + _$jsonDecodeId: (r) => svc.jsonDecode(r.args[0]), + }; +} + +// Service initializer +JsonDecodeService $JsonDecodeServiceInitializer(WorkerRequest startRequest) => + JsonDecodeService(); + +// Worker for JsonDecodeService +class JsonDecodeServiceWorker extends Worker + with $JsonDecodeServiceOperations + implements JsonDecodeService { + JsonDecodeServiceWorker() : super($JsonDecodeServiceActivator); + + @override + Future jsonDecode(String source) => send( + $JsonDecodeServiceOperations._$jsonDecodeId, + args: [source], + token: null, + inspectRequest: false, + inspectResponse: false, + ); + + @override + Map get operations => WorkerService.noOperations; +} + +// Worker pool for JsonDecodeService +class JsonDecodeServiceWorkerPool extends WorkerPool + with $JsonDecodeServiceOperations + implements JsonDecodeService { + JsonDecodeServiceWorkerPool({ConcurrencySettings? concurrencySettings}) + : super(() => JsonDecodeServiceWorker(), + concurrencySettings: concurrencySettings); + + @override + Future jsonDecode(String source) => + execute((w) => w.jsonDecode(source)); + + @override + Map get operations => WorkerService.noOperations; +} diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 9975770b..3bce130d 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -14,6 +14,7 @@ dependencies: analyzer: http: built_collection: + squadron: ^4.3.0 dev_dependencies: build_runner: @@ -22,6 +23,7 @@ dev_dependencies: built_value_generator: dart_code_metrics: ^4.8.1 lints: ^2.0.0 + squadron_builder: ^0.9.0 dependency_overrides: chopper: diff --git a/faq.md b/faq.md index 40658686..fca603c6 100644 --- a/faq.md +++ b/faq.md @@ -169,3 +169,197 @@ interceptors: [ ``` The actual implementation of the algorithm above may vary based on how the backend API - more precisely the login and session handling - of your app looks like. + +## Decoding JSON using Isolates + +Sometimes you want to decode JSON outside the main thread in order to reduce janking. In this example we're going to go +even further and implement a Worker Pool using [Squadron](https://pub.dev/packages/squadron/install) which can +dynamically spawn a maximum number of Workers as they become needed. + +#### Install the dependencies + +- [squadron](https://pub.dev/packages/squadron) +- [squadron_builder](https://pub.dev/packages/squadron_builder) +- [json_annotation](https://pub.dev/packages/json_annotation) +- [json_serializable](https://pub.dev/packages/json_serializable) + +#### Write a JSON decode service + +We'll leverage [squadron_builder](https://pub.dev/packages/squadron_builder) and the power of code generation. + +```dart +import 'dart:async'; +import 'dart:convert' show json; + +import 'package:squadron/squadron.dart'; +import 'package:squadron/squadron_annotations.dart'; + +import 'json_decode_service.activator.g.dart'; + +part 'json_decode_service.worker.g.dart'; + +@SquadronService() +class JsonDecodeService extends WorkerService with $JsonDecodeServiceOperations { + @SquadronMethod() + Future jsonDecode(String source) async => json.decode(source); +} +``` + +Extracted from the [full example here](example/lib/json_decode_service.dart). + +#### Write a custom JsonConverter + +Using [json_serializable](https://pub.dev/packages/json_serializable) we'll create a [JsonConverter](https://github.com/lejard-h/chopper/blob/master/chopper/lib/src/interceptor.dart#L228) +which works with or without a [WorkerPool](https://github.com/d-markey/squadron#features). + +```dart +import 'dart:async' show FutureOr; +import 'dart:convert' show jsonDecode; + +import 'package:chopper/chopper.dart'; +import 'package:chopper_example/json_decode_service.dart'; +import 'package:chopper_example/json_serializable.dart'; + +typedef JsonFactory = T Function(Map json); + +class JsonSerializableWorkerPoolConverter extends JsonConverter { + const JsonSerializableWorkerPoolConverter(this.factories, [this.workerPool]); + + final Map factories; + + /// Make the WorkerPool optional so that the JsonConverter still works without it + final JsonDecodeServiceWorkerPool? workerPool; + + /// By overriding tryDecodeJson we give our JsonConverter + /// the ability to decode JSON in an Isolate. + @override + FutureOr tryDecodeJson(String data) async { + try { + return workerPool != null + ? await workerPool!.jsonDecode(data) + : jsonDecode(data); + } catch (error) { + print(error); + + chopperLogger.warning(error); + + return data; + } + } + + T? _decodeMap(Map values) { + final jsonFactory = factories[T]; + if (jsonFactory == null || jsonFactory is! JsonFactory) { + return null; + } + + return jsonFactory(values); + } + + List _decodeList(Iterable values) => + values.where((v) => v != null).map((v) => _decode(v)).toList(); + + dynamic _decode(entity) { + if (entity is Iterable) return _decodeList(entity as List); + + if (entity is Map) return _decodeMap(entity as Map); + + return entity; + } + + @override + FutureOr> convertResponse( + Response response, + ) async { + final jsonRes = await super.convertResponse(response); + + return jsonRes.copyWith(body: _decode(jsonRes.body)); + } + + @override + FutureOr convertError(Response response) async { + final jsonRes = await super.convertError(response); + + return jsonRes.copyWith( + body: ResourceError.fromJsonFactory(jsonRes.body), + ); + } +} +``` + +Extracted from the [full example here](example/bin/main_json_serializable_squadron_worker_pool.dart). + +#### Code generation + +It goes without saying that running the code generation is a pre-requisite at this stage + +```bash +flutter pub run build_runner build +``` + +#### Configure a WorkerPool and run the example + +```dart +/// inspired by https://github.com/d-markey/squadron_sample/blob/main/lib/main.dart +void initSquadron(String id) { + Squadron.setId(id); + Squadron.setLogger(ConsoleSquadronLogger()); + Squadron.logLevel = SquadronLogLevel.all; + Squadron.debugMode = true; +} + +Future main() async { + /// initialize Squadron before using it + initSquadron('worker_pool_example'); + + final jsonDecodeServiceWorkerPool = JsonDecodeServiceWorkerPool( + // Set whatever you want here + concurrencySettings: ConcurrencySettings.oneCpuThread, + ); + + /// start the Worker Pool + await jsonDecodeServiceWorkerPool.start(); + + /// Instantiate the JsonConverter from above + final converter = JsonSerializableWorkerPoolConverter( + { + Resource: Resource.fromJsonFactory, + }, + /// make sure to provide the WorkerPool to the JsonConverter + jsonDecodeServiceWorkerPool, + ); + + /// Instantiate a ChopperClient + final chopper = ChopperClient( + client: client, + baseUrl: 'http://localhost:8000', + // bind your object factories here + converter: converter, + errorConverter: converter, + services: [ + // the generated service + MyService.create(), + ], + /* ResponseInterceptorFunc | RequestInterceptorFunc | ResponseInterceptor | RequestInterceptor */ + interceptors: [authHeader], + ); + + /// Do stuff with myService + final myService = chopper.getService(); + + /// ...stuff... + + /// stop the Worker Pool once done + jsonDecodeServiceWorkerPool.stop(); +} +``` + +[The full example can be found here](example/bin/main_json_serializable_squadron_worker_pool.dart). + +#### Further reading + +This barely scratches the surface. If you want to know more about [squadron](https://github.com/d-markey/squadron) and +[squadron_builder](https://github.com/d-markey/squadron_builder) make sure to head over to their respective repositories. + +[David Markey](https://github.com/d-markey]), the author of squadron, was kind enough as to provide us with an [excellent Flutter example](https://github.com/d-markey/squadron_builder) using +both packages. \ No newline at end of file From d74790ce226bfadbada024ad6f9ed2191219ddeb Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Sat, 8 Oct 2022 16:45:46 +0100 Subject: [PATCH 031/171] mapToQuery changes (#364) --- chopper/lib/src/annotations.dart | 17 ++ chopper/lib/src/request.dart | 20 +- chopper/lib/src/utils.dart | 56 +++-- chopper/test/base_test.dart | 149 +++++++++++- chopper/test/test_service.chopper.dart | 56 +++++ chopper/test/test_service.dart | 20 ++ chopper/test/utils_test.dart | 277 +++++++++++++++++++++++ chopper_generator/analysis_options.yaml | 2 +- chopper_generator/lib/src/generator.dart | 15 +- 9 files changed, 586 insertions(+), 26 deletions(-) create mode 100644 chopper/test/utils_test.dart diff --git a/chopper/lib/src/annotations.dart b/chopper/lib/src/annotations.dart index dafed63b..10784c92 100644 --- a/chopper/lib/src/annotations.dart +++ b/chopper/lib/src/annotations.dart @@ -161,11 +161,21 @@ class Method { /// Mark the body as optional to suppress warnings during code generation final bool optionalBody; + /// Use brackets [ ] to when encoding + /// + /// - lists + /// hxxp://path/to/script?foo[]=123&foo[]=456&foo[]=789 + /// + /// - maps + /// hxxp://path/to/script?user[name]=john&user[surname]=doe&user[age]=21 + final bool useBrackets; + const Method( this.method, { this.optionalBody = false, this.path = '', this.headers = const {}, + this.useBrackets = false, }); } @@ -176,6 +186,7 @@ class Get extends Method { super.optionalBody = true, super.path, super.headers, + super.useBrackets, }) : super(HttpMethod.Get); } @@ -188,6 +199,7 @@ class Post extends Method { super.optionalBody, super.path, super.headers, + super.useBrackets, }) : super(HttpMethod.Post); } @@ -198,6 +210,7 @@ class Delete extends Method { super.optionalBody = true, super.path, super.headers, + super.useBrackets, }) : super(HttpMethod.Delete); } @@ -210,6 +223,7 @@ class Put extends Method { super.optionalBody, super.path, super.headers, + super.useBrackets, }) : super(HttpMethod.Put); } @@ -221,6 +235,7 @@ class Patch extends Method { super.optionalBody, super.path, super.headers, + super.useBrackets, }) : super(HttpMethod.Patch); } @@ -231,6 +246,7 @@ class Head extends Method { super.optionalBody = true, super.path, super.headers, + super.useBrackets, }) : super(HttpMethod.Head); } @@ -240,6 +256,7 @@ class Options extends Method { super.optionalBody = true, super.path, super.headers, + super.useBrackets, }) : super(HttpMethod.Options); } diff --git a/chopper/lib/src/request.dart b/chopper/lib/src/request.dart index 8378e276..acd335ad 100644 --- a/chopper/lib/src/request.dart +++ b/chopper/lib/src/request.dart @@ -1,5 +1,4 @@ import 'dart:async'; -import 'dart:convert'; import 'package:http/http.dart' as http; import 'package:meta/meta.dart'; @@ -18,6 +17,7 @@ class Request { final Map parameters; final Map headers; final bool multipart; + final bool useBrackets; const Request( this.method, @@ -28,6 +28,7 @@ class Request { this.headers = const {}, this.multipart = false, this.parts = const [], + this.useBrackets = false, }); /// Makes a copy of this request, replacing original values with the given ones. @@ -37,23 +38,25 @@ class Request { dynamic body, Map? parameters, Map? headers, - Encoding? encoding, List? parts, bool? multipart, String? baseUrl, + bool? useBrackets, }) => Request( (method ?? this.method) as String, url ?? this.url, - baseUrl ?? this.baseUrl, body: body ?? this.body, parameters: parameters ?? this.parameters, headers: headers ?? this.headers, parts: parts ?? this.parts, multipart: multipart ?? this.multipart, + baseUrl ?? this.baseUrl, + useBrackets: useBrackets ?? this.useBrackets, ); - Uri _buildUri() => buildUri(baseUrl, url, parameters); + Uri _buildUri() => + buildUri(baseUrl, url, parameters, useBrackets: useBrackets); Map _buildHeaders() => {...headers}; @@ -110,7 +113,12 @@ class PartValueFile extends PartValue { /// Builds a valid URI from [baseUrl], [url] and [parameters]. /// /// If [url] starts with 'http://' or 'https://', baseUrl is ignored. -Uri buildUri(String baseUrl, String url, Map parameters) { +Uri buildUri( + String baseUrl, + String url, + Map parameters, { + bool useBrackets = false, +}) { // If the request's url is already a fully qualified URL, we can use it // as-is and ignore the baseUrl. Uri uri = url.startsWith('http://') || url.startsWith('https://') @@ -119,7 +127,7 @@ Uri buildUri(String baseUrl, String url, Map parameters) { ? Uri.parse('$baseUrl/$url') : Uri.parse('$baseUrl$url'); - String query = mapToQuery(parameters); + String query = mapToQuery(parameters, useBrackets: useBrackets); if (query.isNotEmpty) { if (uri.hasQuery) { query += '&${uri.query}'; diff --git a/chopper/lib/src/utils.dart b/chopper/lib/src/utils.dart index e4d17f7d..85ff8c62 100644 --- a/chopper/lib/src/utils.dart +++ b/chopper/lib/src/utils.dart @@ -56,29 +56,39 @@ final chopperLogger = Logger('Chopper'); /// Creates a valid URI query string from [map]. /// /// E.g., `{'foo': 'bar', 'ints': [ 1337, 42 ] }` will become 'foo=bar&ints=1337&ints=42'. -String mapToQuery(Map map) => _mapToQuery(map).join('&'); +String mapToQuery(Map map, {bool useBrackets = false}) => + _mapToQuery(map, useBrackets: useBrackets).join('&'); Iterable<_Pair> _mapToQuery( Map map, { String? prefix, + bool useBrackets = false, }) { final Set<_Pair> pairs = {}; map.forEach((key, value) { - if (value != null) { - String name = Uri.encodeQueryComponent(key); + String name = Uri.encodeQueryComponent(key); - if (prefix != null) { - name = '$prefix.$name'; - } + if (prefix != null) { + name = useBrackets + ? '$prefix${Uri.encodeQueryComponent('[')}$name${Uri.encodeQueryComponent(']')}' + : '$prefix.$name'; + } + if (value != null) { if (value is Iterable) { - pairs.addAll(_iterableToQuery(name, value)); + pairs.addAll(_iterableToQuery(name, value, useBrackets: useBrackets)); } else if (value is Map) { - pairs.addAll(_mapToQuery(value, prefix: name)); - } else if (value.toString().isNotEmpty) { - pairs.add(_Pair(name, _normalizeValue(value))); + pairs.addAll( + _mapToQuery(value, prefix: name, useBrackets: useBrackets), + ); + } else { + pairs.add( + _Pair(name, _normalizeValue(value)), + ); } + } else { + pairs.add(_Pair(name, '')); } }); @@ -87,20 +97,34 @@ Iterable<_Pair> _mapToQuery( Iterable<_Pair> _iterableToQuery( String name, - Iterable values, -) => - values.map((v) => _Pair(name, _normalizeValue(v))); + Iterable values, { + bool useBrackets = false, +}) => + values.where((value) => value?.toString().isNotEmpty ?? false).map( + (value) => _Pair( + name, + _normalizeValue(value), + useBrackets: useBrackets, + ), + ); -String _normalizeValue(value) => Uri.encodeComponent(value.toString()); +String _normalizeValue(value) => Uri.encodeComponent(value?.toString() ?? ''); class _Pair { final A first; final B second; + final bool useBrackets; - const _Pair(this.first, this.second); + const _Pair( + this.first, + this.second, { + this.useBrackets = false, + }); @override - String toString() => '$first=$second'; + String toString() => useBrackets + ? '$first${Uri.encodeQueryComponent('[]')}=$second' + : '$first=$second'; } bool isTypeOf() => _Instance() is _Instance; diff --git a/chopper/test/base_test.dart b/chopper/test/base_test.dart index 99d81231..cb281caf 100644 --- a/chopper/test/base_test.dart +++ b/chopper/test/base_test.dart @@ -107,7 +107,7 @@ void main() { final httpClient = MockClient((request) async { expect( request.url.toString(), - equals('$baseUrl/test/query'), + equals('$baseUrl/test/query?name=&int=&default_value='), ); expect(request.method, equals('GET')); @@ -129,7 +129,7 @@ void main() { final httpClient = MockClient((request) async { expect( request.url.toString(), - equals('$baseUrl/test/query?default_value=42'), + equals('$baseUrl/test/query?name=&int=&default_value=42'), ); expect(request.method, equals('GET')); @@ -888,4 +888,149 @@ void main() { httpClient.close(); }); + + test('List query param', () async { + final httpClient = MockClient((request) async { + expect( + request.url.toString(), + equals('$baseUrl/test/list_query_param' + '?value=foo' + '&value=bar' + '&value=baz'), + ); + expect(request.method, equals('GET')); + + return http.Response('get response', 200); + }); + + final chopper = buildClient(httpClient); + final service = chopper.getService(); + + final response = await service.getUsingListQueryParam([ + 'foo', + 'bar', + 'baz', + ]); + + expect(response.body, equals('get response')); + expect(response.statusCode, equals(200)); + + httpClient.close(); + }); + + test('List query param with brackets', () async { + final httpClient = MockClient((request) async { + expect( + request.url.toString(), + equals('$baseUrl/test/list_query_param_with_brackets' + '?value%5B%5D=foo' + '&value%5B%5D=bar' + '&value%5B%5D=baz'), + ); + expect(request.method, equals('GET')); + + return http.Response('get response', 200); + }); + + final chopper = buildClient(httpClient); + final service = chopper.getService(); + + final response = await service.getUsingListQueryParamWithBrackets([ + 'foo', + 'bar', + 'baz', + ]); + + expect(response.body, equals('get response')); + expect(response.statusCode, equals(200)); + + httpClient.close(); + }); + + test('Map query param using default dot QueryMapSeparator', () async { + final httpClient = MockClient((request) async { + expect( + request.url.toString(), + equals('$baseUrl/test/map_query_param' + '?value.bar=baz' + '&value.zap=abc' + '&value.etc.abc=def' + '&value.etc.ghi=jkl' + '&value.etc.mno.opq=rst' + '&value.etc.mno.uvw=xyz' + '&value.etc.mno.list=a' + '&value.etc.mno.list=123' + '&value.etc.mno.list=false'), + ); + expect(request.method, equals('GET')); + + return http.Response('get response', 200); + }); + + final chopper = buildClient(httpClient); + final service = chopper.getService(); + + final response = await service.getUsingMapQueryParam({ + 'bar': 'baz', + 'zap': 'abc', + 'etc': { + 'abc': 'def', + 'ghi': 'jkl', + 'mno': { + 'opq': 'rst', + 'uvw': 'xyz', + 'list': ['a', 123, false], + }, + }, + }); + + expect(response.body, equals('get response')); + expect(response.statusCode, equals(200)); + + httpClient.close(); + }); + + test('Map query param with brackets QueryMapSeparator', () async { + final httpClient = MockClient((request) async { + expect( + request.url.toString(), + equals('$baseUrl/test/map_query_param_with_brackets' + '?value%5Bbar%5D=baz' + '&value%5Bzap%5D=abc' + '&value%5Betc%5D%5Babc%5D=def' + '&value%5Betc%5D%5Bghi%5D=jkl' + '&value%5Betc%5D%5Bmno%5D%5Bopq%5D=rst' + '&value%5Betc%5D%5Bmno%5D%5Buvw%5D=xyz' + '&value%5Betc%5D%5Bmno%5D%5Blist%5D%5B%5D=a' + '&value%5Betc%5D%5Bmno%5D%5Blist%5D%5B%5D=123' + '&value%5Betc%5D%5Bmno%5D%5Blist%5D%5B%5D=false'), + ); + expect(request.method, equals('GET')); + + return http.Response('get response', 200); + }); + + final chopper = buildClient(httpClient); + final service = chopper.getService(); + + final response = + await service.getUsingMapQueryParamWithBrackets({ + 'bar': 'baz', + 'zap': 'abc', + 'etc': { + 'abc': 'def', + 'ghi': 'jkl', + 'mno': { + 'opq': 'rst', + 'uvw': 'xyz', + 'list': ['a', 123, false], + }, + }, + }); + + expect(response.body, equals('get response')); + expect(response.statusCode, equals(200)); + + httpClient.close(); + }); } diff --git a/chopper/test/test_service.chopper.dart b/chopper/test/test_service.chopper.dart index 01d5757e..d0a43b5c 100644 --- a/chopper/test/test_service.chopper.dart +++ b/chopper/test/test_service.chopper.dart @@ -488,4 +488,60 @@ class _$HttpTestService extends HttpTestService { ); return client.send($request); } + + @override + Future> getUsingListQueryParam(List value) { + final String $url = '/test/list_query_param'; + final Map $params = {'value': value}; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + ); + return client.send($request); + } + + @override + Future> getUsingListQueryParamWithBrackets( + List value) { + final String $url = '/test/list_query_param_with_brackets'; + final Map $params = {'value': value}; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + useBrackets: true, + ); + return client.send($request); + } + + @override + Future> getUsingMapQueryParam(Map value) { + final String $url = '/test/map_query_param'; + final Map $params = {'value': value}; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + ); + return client.send($request); + } + + @override + Future> getUsingMapQueryParamWithBrackets( + Map value) { + final String $url = '/test/map_query_param_with_brackets'; + final Map $params = {'value': value}; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + useBrackets: true, + ); + return client.send($request); + } } diff --git a/chopper/test/test_service.dart b/chopper/test/test_service.dart index 03abc239..7789b361 100644 --- a/chopper/test/test_service.dart +++ b/chopper/test/test_service.dart @@ -138,6 +138,26 @@ abstract class HttpTestService extends ChopperService { @Post(path: 'no-body') Future noBody(); + + @Get(path: '/list_query_param') + Future> getUsingListQueryParam( + @Query('value') List value, + ); + + @Get(path: '/list_query_param_with_brackets', useBrackets: true) + Future> getUsingListQueryParamWithBrackets( + @Query('value') List value, + ); + + @Get(path: '/map_query_param') + Future> getUsingMapQueryParam( + @Query('value') Map value, + ); + + @Get(path: '/map_query_param_with_brackets', useBrackets: true) + Future> getUsingMapQueryParamWithBrackets( + @Query('value') Map value, + ); } Request customConvertRequest(Request req) { diff --git a/chopper/test/utils_test.dart b/chopper/test/utils_test.dart new file mode 100644 index 00000000..649a4651 --- /dev/null +++ b/chopper/test/utils_test.dart @@ -0,0 +1,277 @@ +import 'package:chopper/src/utils.dart'; +import 'package:test/test.dart'; + +void main() { + group('mapToQuery single', () { + , String>{ + {'foo': null}: 'foo=', + {'foo': ''}: 'foo=', + {'foo': ' '}: 'foo=%20', + {'foo': ' '}: 'foo=%20%20', + {'foo': '\t'}: 'foo=%09', + {'foo': '\t\t'}: 'foo=%09%09', + {'foo': 'null'}: 'foo=null', + {'foo': 'bar'}: 'foo=bar', + {'foo': ' bar '}: 'foo=%20bar%20', + {'foo': '\tbar\t'}: 'foo=%09bar%09', + {'foo': '\t\tbar\t\t'}: 'foo=%09%09bar%09%09', + {'foo': 123}: 'foo=123', + {'foo': 0}: 'foo=0', + {'foo': -0.01}: 'foo=-0.01', + {'foo': '0.00'}: 'foo=0.00', + {'foo': 123.456}: 'foo=123.456', + {'foo': 123.450}: 'foo=123.45', + {'foo': -123.456}: 'foo=-123.456', + {'foo': true}: 'foo=true', + {'foo': false}: 'foo=false', + }.forEach((map, query) => + test('$map -> $query', () => expect(mapToQuery(map), query))); + }); + + group('mapToQuery multiple', () { + , String>{ + {'foo': null, 'baz': null}: 'foo=&baz=', + {'foo': '', 'baz': ''}: 'foo=&baz=', + {'foo': null, 'baz': ''}: 'foo=&baz=', + {'foo': '', 'baz': null}: 'foo=&baz=', + {'foo': 'bar', 'baz': ''}: 'foo=bar&baz=', + {'foo': null, 'baz': 'etc'}: 'foo=&baz=etc', + {'foo': '', 'baz': 'etc'}: 'foo=&baz=etc', + {'foo': 'bar', 'baz': 'etc'}: 'foo=bar&baz=etc', + {'foo': 'null', 'baz': 'null'}: 'foo=null&baz=null', + {'foo': ' ', 'baz': ' '}: 'foo=%20&baz=%20', + {'foo': '\t', 'baz': '\t'}: 'foo=%09&baz=%09', + {'foo': 123, 'baz': 456}: 'foo=123&baz=456', + {'foo': 0, 'baz': 0}: 'foo=0&baz=0', + {'foo': '0.00', 'baz': '0.00'}: 'foo=0.00&baz=0.00', + {'foo': 123.456, 'baz': 789.012}: 'foo=123.456&baz=789.012', + {'foo': 123.450, 'baz': 789.010}: 'foo=123.45&baz=789.01', + {'foo': -123.456, 'baz': -789.012}: 'foo=-123.456&baz=-789.012', + {'foo': true, 'baz': true}: 'foo=true&baz=true', + {'foo': false, 'baz': false}: 'foo=false&baz=false', + }.forEach((map, query) => + test('$map -> $query', () => expect(mapToQuery(map), query))); + }); + + group('mapToQuery lists', () { + , String>{ + { + 'foo': ['bar', 'baz', 'etc'], + }: 'foo=bar&foo=baz&foo=etc', + { + 'foo': ['bar', 123, 456.789, 0, -123, -456.789], + }: 'foo=bar&foo=123&foo=456.789&foo=0&foo=-123&foo=-456.789', + { + 'foo': ['', 'baz', 'etc'], + }: 'foo=baz&foo=etc', + { + 'foo': ['bar', '', 'etc'], + }: 'foo=bar&foo=etc', + { + 'foo': ['bar', 'baz', ''], + }: 'foo=bar&foo=baz', + { + 'foo': [null, 'baz', 'etc'], + }: 'foo=baz&foo=etc', + { + 'foo': ['bar', null, 'etc'], + }: 'foo=bar&foo=etc', + { + 'foo': ['bar', 'baz', null], + }: 'foo=bar&foo=baz', + { + 'foo': ['bar', 'baz', ' '], + }: 'foo=bar&foo=baz&foo=%20', + { + 'foo': ['bar', 'baz', '\t'], + }: 'foo=bar&foo=baz&foo=%09', + { + 'foo': ['bar', 'baz', 'etc'], + 'bar': 'baz', + 'etc': '', + 'xyz': null, + }: 'foo=bar&foo=baz&foo=etc&bar=baz&etc=&xyz=', + }.forEach((map, query) => + test('$map -> $query', () => expect(mapToQuery(map), query))); + }); + + group('mapToQuery lists with brackets', () { + , String>{ + { + 'foo': ['bar', 'baz', 'etc'], + }: 'foo%5B%5D=bar&foo%5B%5D=baz&foo%5B%5D=etc', + { + 'foo': ['bar', 123, 456.789, 0, -123, -456.789], + }: 'foo%5B%5D=bar&foo%5B%5D=123&foo%5B%5D=456.789&foo%5B%5D=0&foo%5B%5D=-123&foo%5B%5D=-456.789', + { + 'foo': ['', 'baz', 'etc'], + }: 'foo%5B%5D=baz&foo%5B%5D=etc', + { + 'foo': ['bar', '', 'etc'], + }: 'foo%5B%5D=bar&foo%5B%5D=etc', + { + 'foo': ['bar', 'baz', ''], + }: 'foo%5B%5D=bar&foo%5B%5D=baz', + { + 'foo': [null, 'baz', 'etc'], + }: 'foo%5B%5D=baz&foo%5B%5D=etc', + { + 'foo': ['bar', null, 'etc'], + }: 'foo%5B%5D=bar&foo%5B%5D=etc', + { + 'foo': ['bar', 'baz', null], + }: 'foo%5B%5D=bar&foo%5B%5D=baz', + { + 'foo': ['bar', 'baz', ' '], + }: 'foo%5B%5D=bar&foo%5B%5D=baz&foo%5B%5D=%20', + { + 'foo': ['bar', 'baz', '\t'], + }: 'foo%5B%5D=bar&foo%5B%5D=baz&foo%5B%5D=%09', + { + 'foo': ['bar', 'baz', 'etc'], + 'bar': 'baz', + 'etc': '', + 'xyz': null, + }: 'foo%5B%5D=bar&foo%5B%5D=baz&foo%5B%5D=etc&bar=baz&etc=&xyz=', + }.forEach( + (map, query) => test( + '$map -> $query', + () => expect( + mapToQuery(map, useBrackets: true), + query, + ), + ), + ); + }); + + group('mapToQuery maps', () { + , String>{ + { + 'foo': {'bar': 'baz'}, + }: 'foo.bar=baz', + { + 'foo': {'bar': ''}, + }: 'foo.bar=', + { + 'foo': {'bar': null}, + }: 'foo.bar=', + { + 'foo': {'bar': ' '}, + }: 'foo.bar=%20', + { + 'foo': {'bar': '\t'}, + }: 'foo.bar=%09', + { + 'foo': {'bar': 'baz', 'etc': 'xyz', 'space': ' ', 'tab': '\t'}, + }: 'foo.bar=baz&foo.etc=xyz&foo.space=%20&foo.tab=%09', + { + 'foo': { + 'bar': 'baz', + 'int': 123, + 'double': 456.789, + 'zero': 0, + 'negInt': -123, + 'negDouble': -456.789, + 'emptyString': '', + 'nullValue': null, + 'space': ' ', + 'tab': '\t', + 'list': ['a', 123, false], + }, + }: 'foo.bar=baz&foo.int=123&foo.double=456.789&foo.zero=0&foo.negInt=-123&foo.negDouble=-456.789&foo.emptyString=&foo.nullValue=&foo.space=%20&foo.tab=%09&foo.list=a&foo.list=123&foo.list=false', + { + 'foo': {'bar': 'baz'}, + 'etc': 'xyz', + }: 'foo.bar=baz&etc=xyz', + { + 'foo': { + 'bar': 'baz', + 'zap': 'abc', + 'etc': { + 'abc': 'def', + 'ghi': 'jkl', + 'mno': { + 'opq': 'rst', + 'uvw': 'xyz', + 'aab': [ + 'bbc', + 'ccd', + 'eef', + ], + }, + }, + }, + }: 'foo.bar=baz&foo.zap=abc&foo.etc.abc=def&foo.etc.ghi=jkl&foo.etc.mno.opq=rst&foo.etc.mno.uvw=xyz&foo.etc.mno.aab=bbc&foo.etc.mno.aab=ccd&foo.etc.mno.aab=eef', + }.forEach((map, query) => + test('$map -> $query', () => expect(mapToQuery(map), query))); + }); + + group('mapToQuery maps with brackets', () { + , String>{ + { + 'foo': {'bar': 'baz'}, + }: 'foo%5Bbar%5D=baz', + { + 'foo': {'bar': ''}, + }: 'foo%5Bbar%5D=', + { + 'foo': {'bar': null}, + }: 'foo%5Bbar%5D=', + { + 'foo': {'bar': ' '}, + }: 'foo%5Bbar%5D=%20', + { + 'foo': {'bar': '\t'}, + }: 'foo%5Bbar%5D=%09', + { + 'foo': {'bar': 'baz', 'etc': 'xyz', 'space': ' ', 'tab': '\t'}, + }: 'foo%5Bbar%5D=baz&foo%5Betc%5D=xyz&foo%5Bspace%5D=%20&foo%5Btab%5D=%09', + { + 'foo': { + 'bar': 'baz', + 'int': 123, + 'double': 456.789, + 'zero': 0, + 'negInt': -123, + 'negDouble': -456.789, + 'emptyString': '', + 'nullValue': null, + 'space': ' ', + 'tab': '\t', + 'list': ['a', 123, false], + }, + }: 'foo%5Bbar%5D=baz&foo%5Bint%5D=123&foo%5Bdouble%5D=456.789&foo%5Bzero%5D=0&foo%5BnegInt%5D=-123&foo%5BnegDouble%5D=-456.789&foo%5BemptyString%5D=&foo%5BnullValue%5D=&foo%5Bspace%5D=%20&foo%5Btab%5D=%09&foo%5Blist%5D%5B%5D=a&foo%5Blist%5D%5B%5D=123&foo%5Blist%5D%5B%5D=false', + { + 'foo': {'bar': 'baz'}, + 'etc': 'xyz', + }: 'foo%5Bbar%5D=baz&etc=xyz', + { + 'foo': { + 'bar': 'baz', + 'zap': 'abc', + 'etc': { + 'abc': 'def', + 'ghi': 'jkl', + 'mno': { + 'opq': 'rst', + 'uvw': 'xyz', + 'aab': [ + 'bbc', + 'ccd', + 'eef', + ], + }, + }, + }, + }: 'foo%5Bbar%5D=baz&foo%5Bzap%5D=abc&foo%5Betc%5D%5Babc%5D=def&foo%5Betc%5D%5Bghi%5D=jkl&foo%5Betc%5D%5Bmno%5D%5Bopq%5D=rst&foo%5Betc%5D%5Bmno%5D%5Buvw%5D=xyz&foo%5Betc%5D%5Bmno%5D%5Baab%5D%5B%5D=bbc&foo%5Betc%5D%5Bmno%5D%5Baab%5D%5B%5D=ccd&foo%5Betc%5D%5Bmno%5D%5Baab%5D%5B%5D=eef', + }.forEach( + (map, query) => test( + '$map -> $query', + () => expect( + mapToQuery(map, useBrackets: true), + query, + ), + ), + ); + }); +} diff --git a/chopper_generator/analysis_options.yaml b/chopper_generator/analysis_options.yaml index ead6c70c..3a82dc3b 100644 --- a/chopper_generator/analysis_options.yaml +++ b/chopper_generator/analysis_options.yaml @@ -14,7 +14,7 @@ dart_code_metrics: cyclomatic-complexity: 20 number-of-arguments: 4 maximum-nesting-level: 5 - number-of-parameters: 5 + number-of-parameters: 6 source-lines-of-code: 250 metrics-exclude: - test/** diff --git a/chopper_generator/lib/src/generator.dart b/chopper_generator/lib/src/generator.dart index 4badfc70..6dda3c4a 100644 --- a/chopper_generator/lib/src/generator.dart +++ b/chopper_generator/lib/src/generator.dart @@ -309,6 +309,8 @@ class ChopperGenerator extends GeneratorForAnnotation { ); } + final bool useBrackets = getUseBrackets(method); + blocks.add( declareFinal(_requestVar, type: refer('Request')) .assign( @@ -318,6 +320,7 @@ class ChopperGenerator extends GeneratorForAnnotation { useQueries: hasQuery, useHeaders: headers != null, hasParts: hasParts, + useBrackets: useBrackets, ), ) .statement, @@ -490,6 +493,7 @@ class ChopperGenerator extends GeneratorForAnnotation { bool hasParts = false, bool useQueries = false, bool useHeaders = false, + bool useBrackets = false, }) { final List params = [ literal(getMethodName(method)), @@ -516,6 +520,10 @@ class ChopperGenerator extends GeneratorForAnnotation { namedParams['headers'] = refer(_headersVar); } + if (useBrackets) { + namedParams['useBrackets'] = literalBool(useBrackets); + } + return refer('Request').newInstance(params, namedParams); } @@ -542,7 +550,9 @@ class ChopperGenerator extends GeneratorForAnnotation { ]; list.add(refer( - 'PartValue<${p.type.getDisplayString(withNullability: p.type.isNullable)}>', + 'PartValue<${p.type.getDisplayString( + withNullability: p.type.isNullable, + )}>', ).newInstance(params)); }); fileFields.forEach((p, ConstantReader r) { @@ -618,6 +628,9 @@ String getMethodPath(ConstantReader method) => method.read('path').stringValue; String getMethodName(ConstantReader method) => method.read('method').stringValue; +bool getUseBrackets(ConstantReader method) => + method.peek('useBrackets')?.boolValue ?? false; + extension DartTypeExtension on DartType { bool get isNullable => nullabilitySuffix != NullabilitySuffix.none; } From 0882a7eaa606c640c3fde70f18dc49ed8f2ef039 Mon Sep 17 00:00:00 2001 From: Ivan Terekhin Date: Sat, 8 Oct 2022 19:30:46 +0300 Subject: [PATCH 032/171] Version bumped / changelog update (#367) --- chopper/CHANGELOG.md | 4 ++++ chopper/pubspec.yaml | 2 +- chopper_generator/CHANGELOG.md | 4 ++++ chopper_generator/pubspec.yaml | 2 +- example/pubspec.yaml | 2 +- 5 files changed, 11 insertions(+), 3 deletions(-) diff --git a/chopper/CHANGELOG.md b/chopper/CHANGELOG.md index e78e1909..e3d9a080 100644 --- a/chopper/CHANGELOG.md +++ b/chopper/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 5.0.1 + +- mapToQuery changes + ## 5.0.0 - API breaking changes (FutureOr) diff --git a/chopper/pubspec.yaml b/chopper/pubspec.yaml index 96817bb9..9033bc29 100644 --- a/chopper/pubspec.yaml +++ b/chopper/pubspec.yaml @@ -1,6 +1,6 @@ name: chopper description: Chopper is an http client generator using source_gen, inspired by Retrofit -version: 5.0.0 +version: 5.0.1 documentation: https://hadrien-lejard.gitbook.io/chopper repository: https://github.com/lejard-h/chopper diff --git a/chopper_generator/CHANGELOG.md b/chopper_generator/CHANGELOG.md index 22074e42..67f4b0e1 100644 --- a/chopper_generator/CHANGELOG.md +++ b/chopper_generator/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 5.0.1 + +- Types added + ## 5.0.0 - API breaking changes (FutureOr usage) diff --git a/chopper_generator/pubspec.yaml b/chopper_generator/pubspec.yaml index 93386abb..2fedb49a 100644 --- a/chopper_generator/pubspec.yaml +++ b/chopper_generator/pubspec.yaml @@ -1,6 +1,6 @@ name: chopper_generator description: Chopper is an http client generator using source_gen, inspired by Retrofit -version: 5.0.0+1 +version: 5.0.1 documentation: https://hadrien-lejard.gitbook.io/chopper repository: https://github.com/lejard-h/chopper diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 3bce130d..65be75a6 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -1,6 +1,6 @@ name: chopper_example description: Example usage of the Chopper package -version: 0.0.1 +version: 0.0.2 documentation: https://hadrien-lejard.gitbook.io/chopper/ #author: Hadrien Lejard From e3fd6237f45f74251211b263281bd9cb36bf6c46 Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Thu, 13 Oct 2022 17:32:33 +0100 Subject: [PATCH 033/171] Request extends http.BaseRequest (#370) --- chopper/lib/chopper.dart | 1 + chopper/lib/src/annotations.dart | 7 +- chopper/lib/src/base.dart | 15 +- chopper/lib/src/constants.dart | 2 +- chopper/lib/src/extensions.dart | 27 ++ chopper/lib/src/interceptor.dart | 13 +- chopper/lib/src/request.dart | 302 +++++++++---------- chopper/pubspec.yaml | 7 +- chopper/test/base_test.dart | 118 +++++--- chopper/test/extensions_test.dart | 66 ++++ chopper/test/interceptors_test.dart | 4 +- chopper/test/multipart_test.dart | 57 ++-- chopper/test/request_test.dart | 175 +++++++++++ chopper_built_value/test/converter_test.dart | 12 +- 14 files changed, 561 insertions(+), 245 deletions(-) create mode 100644 chopper/lib/src/extensions.dart create mode 100644 chopper/test/extensions_test.dart create mode 100644 chopper/test/request_test.dart diff --git a/chopper/lib/chopper.dart b/chopper/lib/chopper.dart index c004d675..e7230361 100644 --- a/chopper/lib/chopper.dart +++ b/chopper/lib/chopper.dart @@ -7,6 +7,7 @@ export 'src/annotations.dart'; export 'src/authenticator.dart'; export 'src/base.dart'; export 'src/constants.dart'; +export 'src/extensions.dart'; export 'src/interceptor.dart'; export 'src/request.dart'; export 'src/response.dart'; diff --git a/chopper/lib/src/annotations.dart b/chopper/lib/src/annotations.dart index 10784c92..5d850deb 100644 --- a/chopper/lib/src/annotations.dart +++ b/chopper/lib/src/annotations.dart @@ -1,11 +1,10 @@ import 'dart:async'; +import 'package:chopper/src/constants.dart'; +import 'package:chopper/src/request.dart'; +import 'package:chopper/src/response.dart'; import 'package:meta/meta.dart'; -import 'constants.dart'; -import 'request.dart'; -import 'response.dart'; - /// Defines a Chopper API. /// /// Must be used on an abstract class that extends the [ChopperService] class. diff --git a/chopper/lib/src/base.dart b/chopper/lib/src/base.dart index 3fa4e998..4e594584 100644 --- a/chopper/lib/src/base.dart +++ b/chopper/lib/src/base.dart @@ -1,16 +1,15 @@ import 'dart:async'; +import 'package:chopper/src/annotations.dart'; +import 'package:chopper/src/authenticator.dart'; +import 'package:chopper/src/constants.dart'; +import 'package:chopper/src/interceptor.dart'; +import 'package:chopper/src/request.dart'; +import 'package:chopper/src/response.dart'; +import 'package:chopper/src/utils.dart'; import 'package:http/http.dart' as http; import 'package:meta/meta.dart'; -import 'annotations.dart'; -import 'authenticator.dart'; -import 'constants.dart'; -import 'interceptor.dart'; -import 'request.dart'; -import 'response.dart'; -import 'utils.dart'; - Type _typeOf() => T; @visibleForTesting diff --git a/chopper/lib/src/constants.dart b/chopper/lib/src/constants.dart index e7e8faec..52db96c8 100644 --- a/chopper/lib/src/constants.dart +++ b/chopper/lib/src/constants.dart @@ -7,7 +7,7 @@ const String formEncodedHeaders = 'application/x-www-form-urlencoded'; // Represent the header for a json api response https://jsonapi.org/#mime-types const String jsonApiHeaders = 'application/vnd.api+json'; -class HttpMethod { +abstract class HttpMethod { static const String Get = 'GET'; static const String Post = 'POST'; static const String Put = 'PUT'; diff --git a/chopper/lib/src/extensions.dart b/chopper/lib/src/extensions.dart new file mode 100644 index 00000000..8d5586c2 --- /dev/null +++ b/chopper/lib/src/extensions.dart @@ -0,0 +1,27 @@ +extension StripStringExtension on String { + /// The string without any leading whitespace and optional [character] + String leftStrip([String? character]) { + final String trimmed = trimLeft(); + + if (character != null && trimmed.startsWith(character)) { + return trimmed.substring(1); + } + + return trimmed; + } + + /// The string without any trailing whitespace and optional [character] + String rightStrip([String? character]) { + final String trimmed = trimRight(); + + if (character != null && trimmed.endsWith(character)) { + return trimmed.substring(0, trimmed.length - 1); + } + + return trimmed; + } + + /// The string without any leading and trailing whitespace and optional [character] + String strip([String? character]) => + character != null ? leftStrip(character).rightStrip(character) : trim(); +} diff --git a/chopper/lib/src/interceptor.dart b/chopper/lib/src/interceptor.dart index 9d393afc..e5f7bbe0 100644 --- a/chopper/lib/src/interceptor.dart +++ b/chopper/lib/src/interceptor.dart @@ -1,14 +1,13 @@ import 'dart:async'; import 'dart:convert'; +import 'package:chopper/src/constants.dart'; +import 'package:chopper/src/request.dart'; +import 'package:chopper/src/response.dart'; +import 'package:chopper/src/utils.dart'; import 'package:http/http.dart' as http; import 'package:meta/meta.dart'; -import 'constants.dart'; -import 'request.dart'; -import 'response.dart'; -import 'utils.dart'; - /// An interface for implementing response interceptors. /// /// [ResponseInterceptor]s are called after [Converter.convertResponse]. @@ -172,7 +171,7 @@ class HttpLoggingInterceptor @override FutureOr onRequest(Request request) async { final http.BaseRequest base = await request.toBaseRequest(); - chopperLogger.info('--> ${base.method} ${base.url}'); + chopperLogger.info('--> ${base.method} ${base.url.toString()}'); base.headers.forEach((k, v) => chopperLogger.info('$k: $v')); String bytes = ''; @@ -192,7 +191,7 @@ class HttpLoggingInterceptor @override FutureOr onResponse(Response response) { final http.BaseRequest? base = response.base.request; - chopperLogger.info('<-- ${response.statusCode} ${base!.url}'); + chopperLogger.info('<-- ${response.statusCode} ${base!.url.toString()}'); response.base.headers.forEach((k, v) => chopperLogger.info('$k: $v')); diff --git a/chopper/lib/src/request.dart b/chopper/lib/src/request.dart index acd335ad..b515ec24 100644 --- a/chopper/lib/src/request.dart +++ b/chopper/lib/src/request.dart @@ -1,64 +1,110 @@ import 'dart:async'; +import 'package:chopper/src/extensions.dart'; +import 'package:chopper/src/utils.dart'; import 'package:http/http.dart' as http; import 'package:meta/meta.dart'; -import 'constants.dart'; -import 'utils.dart'; - /// This class represents an HTTP request that can be made with Chopper. -@immutable -class Request { - final String method; - final String baseUrl; - final String url; +class Request extends http.BaseRequest { + final String path; + final String origin; final dynamic body; - final List parts; final Map parameters; - final Map headers; final bool multipart; + final List parts; final bool useBrackets; - const Request( - this.method, - this.url, - this.baseUrl, { + Request( + String method, + this.path, + this.origin, { this.body, this.parameters = const {}, - this.headers = const {}, + Map headers = const {}, + this.multipart = false, + this.parts = const [], + this.useBrackets = false, + }) : super( + method, + buildUri(origin, path, parameters, useBrackets: useBrackets), + ) { + this.headers.addAll(headers); + } + + /// Build the Chopper [Request] using a [Uri] instead of a [path] and [origin]. + /// Both the query parameters in the [Uri] and those provided explicitly in + /// the [parameters] are merged together. + Request.uri( + String method, + Uri url, { + this.body, + Map? parameters, + Map headers = const {}, this.multipart = false, this.parts = const [], this.useBrackets = false, - }); + }) : origin = url.origin, + path = url.path, + parameters = {...url.queryParametersAll, ...?parameters}, + super( + method, + buildUri( + url.origin, + url.path, + {...url.queryParametersAll, ...?parameters}, + useBrackets: useBrackets, + ), + ) { + this.headers.addAll(headers); + } - /// Makes a copy of this request, replacing original values with the given ones. + /// Makes a copy of this [Request], replacing original values with the given ones. Request copyWith({ - HttpMethod? method, - String? url, + String? method, + String? path, + String? origin, dynamic body, Map? parameters, Map? headers, - List? parts, bool? multipart, - String? baseUrl, + List? parts, bool? useBrackets, }) => Request( - (method ?? this.method) as String, - url ?? this.url, + method ?? this.method, + path ?? this.path, + origin ?? this.origin, body: body ?? this.body, parameters: parameters ?? this.parameters, headers: headers ?? this.headers, - parts: parts ?? this.parts, multipart: multipart ?? this.multipart, - baseUrl ?? this.baseUrl, + parts: parts ?? this.parts, useBrackets: useBrackets ?? this.useBrackets, ); - Uri _buildUri() => - buildUri(baseUrl, url, parameters, useBrackets: useBrackets); - - Map _buildHeaders() => {...headers}; + /// Builds a valid URI from [baseUrl], [url] and [parameters]. + /// + /// If [url] starts with 'http://' or 'https://', baseUrl is ignored. + @visibleForTesting + static Uri buildUri( + String baseUrl, + String url, + Map parameters, { + bool useBrackets = false, + }) { + // If the request's url is already a fully qualified URL, we can use it + // as-is and ignore the baseUrl. + final Uri uri = url.startsWith('http://') || url.startsWith('https://') + ? Uri.parse(url) + : Uri.parse('${baseUrl.strip('/')}/${url.leftStrip('/')}'); + + final String query = mapToQuery(parameters, useBrackets: useBrackets); + + return query.isNotEmpty + ? uri.replace(query: uri.hasQuery ? '${uri.query}&$query' : query) + : uri; + } /// Converts this Chopper Request into a [http.BaseRequest]. /// @@ -69,18 +115,86 @@ class Request { /// - [http.MultipartRequest] if [multipart] is true /// - or a [http.Request] Future toBaseRequest() async { - final Uri uri = _buildUri(); - final Map heads = _buildHeaders(); + if (body is Stream>) return toStreamedRequest(body); + + if (multipart) return toMultipartRequest(); - if (body is Stream>) { - return toStreamedRequest(body, method, uri, heads); + return toHttpRequest(); + } + + /// Convert this [Request] to a [http.Request] + @visibleForTesting + http.Request toHttpRequest() { + final http.Request request = http.Request(method, url) + ..headers.addAll(headers); + + if (body != null) { + if (body is String) { + request.body = body; + } else if (body is List) { + request.bodyBytes = body; + } else if (body is Map) { + request.bodyFields = body; + } else { + throw ArgumentError.value('$body', 'body'); + } } - if (multipart) { - return toMultipartRequest(parts, method, uri, heads); + return request; + } + + /// Convert this [Request] to a [http.MultipartRequest] + @visibleForTesting + Future toMultipartRequest() async { + final http.MultipartRequest request = http.MultipartRequest(method, url) + ..headers.addAll(headers); + + for (final PartValue part in parts) { + if (part.value == null) continue; + + if (part.value is http.MultipartFile) { + request.files.add(part.value); + } else if (part.value is Iterable) { + request.files.addAll(part.value); + } else if (part is PartValueFile) { + if (part.value is List) { + request.files.add( + http.MultipartFile.fromBytes(part.name, part.value), + ); + } else if (part.value is String) { + request.files.add( + await http.MultipartFile.fromPath(part.name, part.value), + ); + } else { + throw ArgumentError( + 'Type ${part.value.runtimeType} is not a supported type for PartFile' + 'Please use one of the following types' + ' - List' + ' - String (path of your file) ' + ' - MultipartFile (from package:http)', + ); + } + } else { + request.fields[part.name] = part.value.toString(); + } } - return toHttpRequest(body, method, uri, heads); + return request; + } + + /// Convert this [Request] to a [http.StreamedRequest] + @visibleForTesting + http.StreamedRequest toStreamedRequest(Stream> bodyStream) { + final http.StreamedRequest request = http.StreamedRequest(method, url) + ..headers.addAll(headers); + + bodyStream.listen( + request.sink.add, + onDone: request.sink.close, + onError: request.sink.addError, + ); + + return request; } } @@ -109,119 +223,3 @@ class PartValue { class PartValueFile extends PartValue { const PartValueFile(super.name, super.value); } - -/// Builds a valid URI from [baseUrl], [url] and [parameters]. -/// -/// If [url] starts with 'http://' or 'https://', baseUrl is ignored. -Uri buildUri( - String baseUrl, - String url, - Map parameters, { - bool useBrackets = false, -}) { - // If the request's url is already a fully qualified URL, we can use it - // as-is and ignore the baseUrl. - Uri uri = url.startsWith('http://') || url.startsWith('https://') - ? Uri.parse(url) - : !baseUrl.endsWith('/') && !url.startsWith('/') - ? Uri.parse('$baseUrl/$url') - : Uri.parse('$baseUrl$url'); - - String query = mapToQuery(parameters, useBrackets: useBrackets); - if (query.isNotEmpty) { - if (uri.hasQuery) { - query += '&${uri.query}'; - } - - return uri.replace(query: query); - } - - return uri; -} - -@visibleForTesting -Future toHttpRequest( - body, - String method, - Uri uri, - Map headers, -) async { - final http.Request baseRequest = http.Request(method, uri) - ..headers.addAll(headers); - - if (body != null) { - if (body is String) { - baseRequest.body = body; - } else if (body is List) { - baseRequest.bodyBytes = body; - } else if (body is Map) { - baseRequest.bodyFields = body; - } else { - throw ArgumentError.value('$body', 'body'); - } - } - - return baseRequest; -} - -@visibleForTesting -Future toMultipartRequest( - List parts, - String method, - Uri uri, - Map headers, -) async { - final http.MultipartRequest baseRequest = http.MultipartRequest(method, uri) - ..headers.addAll(headers); - - for (final PartValue part in parts) { - if (part.value == null) continue; - - if (part.value is http.MultipartFile) { - baseRequest.files.add(part.value); - } else if (part.value is Iterable) { - baseRequest.files.addAll(part.value); - } else if (part is PartValueFile) { - if (part.value is List) { - baseRequest.files.add( - http.MultipartFile.fromBytes(part.name, part.value), - ); - } else if (part.value is String) { - baseRequest.files.add( - await http.MultipartFile.fromPath(part.name, part.value), - ); - } else { - throw ArgumentError( - 'Type ${part.value.runtimeType} is not a supported type for PartFile' - 'Please use one of the following types' - ' - List' - ' - String (path of your file) ' - ' - MultipartFile (from package:http)', - ); - } - } else { - baseRequest.fields[part.name] = part.value.toString(); - } - } - - return baseRequest; -} - -@visibleForTesting -Future toStreamedRequest( - Stream> bodyStream, - String method, - Uri uri, - Map headers, -) async { - final http.StreamedRequest req = http.StreamedRequest(method, uri) - ..headers.addAll(headers); - - bodyStream.listen( - req.sink.add, - onDone: req.sink.close, - onError: req.sink.addError, - ); - - return req; -} diff --git a/chopper/pubspec.yaml b/chopper/pubspec.yaml index 9033bc29..68122b13 100644 --- a/chopper/pubspec.yaml +++ b/chopper/pubspec.yaml @@ -9,16 +9,17 @@ environment: dependencies: http: ">=0.13.0 <1.0.0" - meta: ^1.3.0 logging: ^1.0.0 + meta: ^1.3.0 dev_dependencies: - test: ^1.16.4 build_runner: ^2.0.0 build_test: ^2.0.0 + collection: ^1.16.0 coverage: ^1.0.2 - http_parser: ^4.0.0 dart_code_metrics: ^4.8.1 + http_parser: ^4.0.0 lints: ^2.0.0 + test: ^1.16.4 chopper_generator: path: ../chopper_generator diff --git a/chopper/test/base_test.dart b/chopper/test/base_test.dart index cb281caf..ffdd5d9d 100644 --- a/chopper/test/base_test.dart +++ b/chopper/test/base_test.dart @@ -1,3 +1,5 @@ +// ignore_for_file: long-method + import 'dart:async'; import 'dart:convert'; @@ -49,6 +51,7 @@ void main() { ); } }); + test('GET', () async { final httpClient = MockClient((request) async { expect( @@ -467,58 +470,86 @@ void main() { }); test('url concatenation', () async { - final url1 = buildUri('foo', 'bar', {}); - expect(url1.toString(), equals('foo/bar')); + expect( + Request.buildUri('foo', 'bar', {}).toString(), + equals('foo/bar'), + ); - final url2 = buildUri('foo/', 'bar', {}); - expect(url2.toString(), equals('foo/bar')); + expect( + Request.buildUri('foo/', 'bar', {}).toString(), + equals('foo/bar'), + ); - final url3 = buildUri('foo', '/bar', {}); - expect(url3.toString(), equals('foo/bar')); + expect( + Request.buildUri('foo', '/bar', {}).toString(), + equals('foo/bar'), + ); - final url4 = buildUri('foo/', '/bar', {}); - expect(url4.toString(), equals('foo//bar')); + expect( + Request.buildUri('foo/', '/bar', {}).toString(), + equals('foo/bar'), + ); - final url5 = buildUri('http://foo', '/bar', {}); - expect(url5.toString(), equals('http://foo/bar')); + expect( + Request.buildUri('http://foo', '/bar', {}).toString(), + equals('http://foo/bar'), + ); - final url6 = buildUri('https://foo', '/bar', {}); - expect(url6.toString(), equals('https://foo/bar')); + expect( + Request.buildUri('https://foo', '/bar', {}).toString(), + equals('https://foo/bar'), + ); - final url7 = buildUri('https://foo/', '/bar', {}); - expect(url7.toString(), equals('https://foo//bar')); + expect( + Request.buildUri('https://foo/', '/bar', {}).toString(), + equals('https://foo/bar'), + ); + + expect( + Request.buildUri('https://foo/', '/bar', {'abc': 'xyz'}).toString(), + equals('https://foo/bar?abc=xyz'), + ); + + expect( + Request.buildUri( + 'https://foo/', + '/bar?first=123&second=456', + { + 'third': '789', + 'fourth': '012', + }, + ).toString(), + equals('https://foo/bar?first=123&second=456&third=789&fourth=012'), + ); }); - test('BodyBytes', () async { - final request = await toHttpRequest( - [1, 2, 3], + test('BodyBytes', () { + final request = Request.uri( HttpMethod.Post, - Uri.parse('/foo'), - {}, - ); + Uri.parse('https://foo/'), + body: [1, 2, 3], + ).toHttpRequest(); expect(request.bodyBytes, equals([1, 2, 3])); }); - test('BodyFields', () async { - final request = await toHttpRequest( - {'foo': 'bar'}, + test('BodyFields', () { + final request = Request.uri( HttpMethod.Post, - Uri.parse('/foo'), - {}, - ); + Uri.parse('https://foo/'), + body: {'foo': 'bar'}, + ).toHttpRequest(); expect(request.bodyFields, equals({'foo': 'bar'})); }); - test('Wrong body', () async { + test('Wrong body', () { try { - await toHttpRequest( - {'foo': 42}, + Request.uri( HttpMethod.Post, - Uri.parse('/foo'), - {}, - ); + Uri.parse('https://foo/'), + body: {'foo': 42}, + ).toHttpRequest(); } on ArgumentError catch (e) { expect(e.toString(), equals('Invalid argument (body): "{foo: 42}"')); } @@ -767,7 +798,7 @@ void main() { chopper.onRequest.listen((request) { expect( request.url.toString(), - equals('/test/get/1234'), + equals('$baseUrl/test/get/1234'), ); }); @@ -878,15 +909,18 @@ void main() { final chopper = buildClient(httpClient); final service = chopper.getService(); - try { - await service - .getTest('1234', dynamicHeader: '') - .timeout(const Duration(seconds: 3)); - } catch (e) { - expect(e is TimeoutException, isTrue); - } - - httpClient.close(); + expect( + () async { + try { + await service + .getTest('1234', dynamicHeader: '') + .timeout(const Duration(seconds: 3)); + } finally { + httpClient.close(); + } + }, + throwsA(isA()), + ); }); test('List query param', () async { diff --git a/chopper/test/extensions_test.dart b/chopper/test/extensions_test.dart new file mode 100644 index 00000000..14de4b03 --- /dev/null +++ b/chopper/test/extensions_test.dart @@ -0,0 +1,66 @@ +// ignore_for_file: long-method + +import 'package:chopper/src/extensions.dart'; +import 'package:test/test.dart'; + +void main() { + group('String.leftStrip', () { + test('leftStrip without character any leading whitespace', () { + expect('/foo'.leftStrip(), '/foo'); + expect(' /foo'.leftStrip(), '/foo'); + expect('/foo '.leftStrip(), '/foo '); + expect(' /foo '.leftStrip(), '/foo '); + }); + + test( + 'leftStrip with character removes single leading character and any leading whitespace', + () { + expect('/foo'.leftStrip('/'), 'foo'); + expect('//foo'.leftStrip('/'), '/foo'); + expect(' /foo'.leftStrip('/'), 'foo'); + expect('/foo '.leftStrip('/'), 'foo '); + expect(' /foo '.leftStrip('/'), 'foo '); + }, + ); + }); + + group('String.rightStrip', () { + test('rightStrip without character any trailing whitespace', () { + expect('foo/'.rightStrip(), 'foo/'); + expect(' foo/'.rightStrip(), ' foo/'); + expect('foo/ '.rightStrip(), 'foo/'); + expect(' foo/ '.rightStrip(), ' foo/'); + }); + + test( + 'rightStrip with character removes single trailing character and any trailing whitespace', + () { + expect('foo/'.rightStrip('/'), 'foo'); + expect('foo//'.rightStrip('/'), 'foo/'); + expect(' foo/'.rightStrip('/'), ' foo'); + expect('foo/ '.rightStrip('/'), 'foo'); + expect(' foo/ '.rightStrip('/'), ' foo'); + }, + ); + }); + + group('String.strip', () { + test('strip without character any leading and trailing whitespace', () { + expect('/foo/'.strip(), '/foo/'); + expect(' /foo/'.strip(), '/foo/'); + expect('/foo/ '.strip(), '/foo/'); + expect(' /foo/ '.strip(), '/foo/'); + }); + + test( + 'strip with character removes single leading and trailing character and any leading and trailing whitespace', + () { + expect('/foo/'.strip('/'), 'foo'); + expect('//foo//'.strip('/'), '/foo/'); + expect(' /foo/'.strip('/'), 'foo'); + expect('/foo/ '.strip('/'), 'foo'); + expect(' /foo/ '.strip('/'), 'foo'); + }, + ); + }); +} diff --git a/chopper/test/interceptors_test.dart b/chopper/test/interceptors_test.dart index 4650d89c..16257dca 100644 --- a/chopper/test/interceptors_test.dart +++ b/chopper/test/interceptors_test.dart @@ -48,7 +48,7 @@ void main() { final chopper = ChopperClient( interceptors: [ (Request request) => - request.copyWith(url: '${request.url}/intercept'), + request.copyWith(path: '${request.url}/intercept'), ], services: [ HttpTestService.create(), @@ -271,7 +271,7 @@ class ResponseIntercept implements ResponseInterceptor { class RequestIntercept implements RequestInterceptor { @override FutureOr onRequest(Request request) => - request.copyWith(url: '${request.url}/intercept'); + request.copyWith(path: '${request.url}/intercept'); } class _Intercepted { diff --git a/chopper/test/multipart_test.dart b/chopper/test/multipart_test.dart index 20d28044..1e461d2f 100644 --- a/chopper/test/multipart_test.dart +++ b/chopper/test/multipart_test.dart @@ -203,15 +203,14 @@ void main() { }); test('PartValue', () async { - final req = await toMultipartRequest( - [ + final req = await Request.uri( + HttpMethod.Post, + Uri.parse('https://foo/'), + parts: [ PartValue('foo', 'bar'), PartValue('int', 42), ], - HttpMethod.Post, - Uri.parse('/foo'), - {}, - ); + ).toMultipartRequest(); expect(req.fields['foo'], equals('bar')); expect(req.fields['int'], equals('42')); @@ -220,15 +219,14 @@ void main() { test( 'PartFile', () async { - final req = await toMultipartRequest( - [ + final req = await Request.uri( + HttpMethod.Post, + Uri.parse('https://foo/'), + parts: [ PartValueFile('foo', 'test/multipart_test.dart'), PartValueFile>('int', [1, 2]), ], - HttpMethod.Post, - Uri.parse('/foo'), - {}, - ); + ).toMultipartRequest(); expect( req.files.firstWhere((f) => f.field == 'foo').filename, @@ -259,17 +257,16 @@ void main() { }); test('Multipart request non nullable', () async { - final req = await toMultipartRequest( - [ + final req = await Request.uri( + HttpMethod.Post, + Uri.parse('https://foo/'), + parts: [ PartValue('int', 42), PartValueFile>('list int', [1, 2]), PartValue('null value', null), PartValueFile('null file', null), ], - HttpMethod.Post, - Uri.parse('/foo'), - {}, - ); + ).toMultipartRequest(); expect(req.fields.length, equals(1)); expect(req.fields['int'], equals('42')); @@ -279,8 +276,10 @@ void main() { }); test('PartValue with MultipartFile directly', () async { - final req = await toMultipartRequest( - [ + final req = await Request.uri( + HttpMethod.Post, + Uri.parse('https://foo/'), + parts: [ PartValue( '', http.MultipartFile.fromBytes( @@ -298,10 +297,7 @@ void main() { ), ), ], - HttpMethod.Post, - Uri.parse('/foo'), - {}, - ); + ).toMultipartRequest(); final first = req.files[0]; final second = req.files[1]; @@ -315,4 +311,17 @@ void main() { bytes = await second.finalize().first; expect(bytes, equals([2, 1])); }); + + test('Throw exception', () async { + expect( + () async => await Request.uri( + HttpMethod.Post, + Uri.parse('https://foo/'), + parts: [ + PartValueFile('', 123), + ], + ).toMultipartRequest(), + throwsA(isA()), + ); + }); } diff --git a/chopper/test/request_test.dart b/chopper/test/request_test.dart new file mode 100644 index 00000000..6f886afd --- /dev/null +++ b/chopper/test/request_test.dart @@ -0,0 +1,175 @@ +// ignore_for_file: long-method + +import 'package:chopper/chopper.dart'; +import 'package:test/test.dart'; +import 'package:http/http.dart' as http; +import 'package:collection/collection.dart'; + +void main() { + group('Request', () { + test('constructor produces a BaseRequest', () { + expect( + Request('GET', '/bar', 'https://foo/'), + isA(), + ); + }); + + test('method gets preserved in BaseRequest', () { + expect( + Request('GET', '/bar', 'https://foo/').method, + equals('GET'), + ); + }); + + test('url is correctly parsed and set in BaseRequest', () { + expect( + Request('GET', '/bar', 'https://foo/').url, + equals(Uri.parse('https://foo/bar')), + ); + + expect( + Request('GET', '/bar?lorem=ipsum&dolor=123', 'https://foo/').url, + equals(Uri.parse('https://foo/bar?lorem=ipsum&dolor=123')), + ); + + expect( + Request( + 'GET', + '/bar', + 'https://foo/', + parameters: { + 'lorem': 'ipsum', + 'dolor': 123, + }, + ).url, + equals(Uri.parse('https://foo/bar?lorem=ipsum&dolor=123')), + ); + + expect( + Request( + 'GET', + '/bar?first=sit&second=amet&first_list=a&first_list=b', + 'https://foo/', + parameters: { + 'lorem': 'ipsum', + 'dolor': 123, + 'second_list': ['a', 'b'], + }, + ).url, + equals(Uri.parse( + 'https://foo/bar?first=sit&second=amet&first_list=a&first_list=b&lorem=ipsum&dolor=123&second_list=a&second_list=b', + )), + ); + }); + + test('headers are preserved in BaseRequest', () { + final Map headers = { + 'content-type': 'application/json; charset=utf-8', + 'accept': 'application/json; charset=utf-8', + }; + + final Request request = Request( + 'GET', + '/bar', + 'https://foo/', + headers: headers, + ); + + expect( + MapEquality().equals(request.headers, headers), + true, + ); + }); + + test('copyWith creates a BaseRequest', () { + expect( + Request('GET', '/bar', 'https://foo/').copyWith(method: HttpMethod.Put), + isA(), + ); + }); + }); + + group('Request.uri', () { + test('constructor produces a BaseRequest', () { + expect( + Request.uri('GET', Uri.parse('https://foo/bar')), + isA(), + ); + }); + + test('method gets preserved in BaseRequest', () { + expect( + Request.uri('GET', Uri.parse('https://foo/bar')).method, + equals('GET'), + ); + }); + + test('url is correctly parsed and set in BaseRequest', () { + expect( + Request.uri('GET', Uri.parse('https://foo/bar')).url, + equals(Uri.parse('https://foo/bar')), + ); + + expect( + Request.uri('GET', Uri.parse('https://foo/bar?lorem=ipsum&dolor=123')) + .url, + equals(Uri.parse('https://foo/bar?lorem=ipsum&dolor=123')), + ); + + expect( + Request.uri( + 'GET', + Uri.parse('https://foo/bar'), + parameters: { + 'lorem': 'ipsum', + 'dolor': 123, + }, + ).url, + equals(Uri.parse('https://foo/bar?lorem=ipsum&dolor=123')), + ); + + expect( + Request.uri( + 'GET', + Uri.parse( + 'https://foo/bar?first=sit&second=amet&first_list=a&first_list=b', + ), + parameters: { + 'lorem': 'ipsum', + 'dolor': 123, + 'second_list': ['a', 'b'], + }, + ).url, + equals(Uri.parse( + 'https://foo/bar?first=sit&second=amet&first_list=a&first_list=b&lorem=ipsum&dolor=123&second_list=a&second_list=b', + )), + ); + }); + + test('headers are preserved in BaseRequest', () { + final Map headers = { + 'content-type': 'application/json; charset=utf-8', + 'accept': 'application/json; charset=utf-8', + }; + + final Request request = Request.uri( + 'GET', + Uri.parse('https://foo/bar'), + headers: headers, + ); + + expect( + MapEquality().equals(request.headers, headers), + true, + ); + }); + + test('copyWith creates a BaseRequest', () { + expect( + Request.uri('GET', Uri.parse('https://foo/bar')) + .copyWith(method: HttpMethod.Put), + isA(), + ); + }); + }); +} diff --git a/chopper_built_value/test/converter_test.dart b/chopper_built_value/test/converter_test.dart index 2766f645..f47cd763 100644 --- a/chopper_built_value/test/converter_test.dart +++ b/chopper_built_value/test/converter_test.dart @@ -26,7 +26,11 @@ void main() { group('BuiltValueConverter', () { test('convert request', () { - var request = Request('', '', '', body: data); + var request = Request.uri( + HttpMethod.Post, + Uri.parse('https://foo/'), + body: data, + ); request = converter.convertRequest(request); expect(request.body, '{"\$":"DataModel","id":42,"name":"foo"}'); }); @@ -65,7 +69,11 @@ void main() { }); test('has json headers', () { - var request = Request('', '', '', body: data); + var request = Request.uri( + HttpMethod.Get, + Uri.parse('https://foo/'), + body: data, + ); request = converter.convertRequest(request); expect(request.headers['content-type'], equals('application/json')); From bd8d65f5eb97cc61058cc075399bd7c406f8fbe9 Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Fri, 14 Oct 2022 10:59:00 +0100 Subject: [PATCH 034/171] Exclude null query vars by default and add new @Method annotation includeNullQueryVars (#372) --- chopper/lib/src/annotations.dart | 34 +++ chopper/lib/src/request.dart | 21 +- chopper/lib/src/utils.dart | 24 +- chopper/test/base_test.dart | 116 ++++++++- chopper/test/test_service.chopper.dart | 37 +++ chopper/test/test_service.dart | 15 ++ chopper/test/utils_test.dart | 305 ++++++++++++++++++++++- chopper_generator/analysis_options.yaml | 2 +- chopper_generator/lib/src/generator.dart | 11 + 9 files changed, 548 insertions(+), 17 deletions(-) diff --git a/chopper/lib/src/annotations.dart b/chopper/lib/src/annotations.dart index 5d850deb..5ddda966 100644 --- a/chopper/lib/src/annotations.dart +++ b/chopper/lib/src/annotations.dart @@ -169,12 +169,39 @@ class Method { /// hxxp://path/to/script?user[name]=john&user[surname]=doe&user[age]=21 final bool useBrackets; + /// Set to [true] to include query variables with null values. This includes nested maps. + /// The default is to exclude them. + /// + /// NOTE: Empty strings are always included. + /// + /// ```dart + /// @Get( + /// path: '/script', + /// includeNullQueryVars: true, + /// ) + /// Future> getData({ + /// @Query('foo') String? foo, + /// @Query('bar') String? bar, + /// @Query('baz') String? baz, + /// }); + /// + /// final response = await service.getData( + /// foo: 'foo_val', + /// bar: null, // omitting it would have the same effect + /// baz: 'baz_val', + /// ); + /// ``` + /// + /// The above code produces hxxp://path/to/script&foo=foo_var&bar=&baz=baz_var + final bool includeNullQueryVars; + const Method( this.method, { this.optionalBody = false, this.path = '', this.headers = const {}, this.useBrackets = false, + this.includeNullQueryVars = false, }); } @@ -186,6 +213,7 @@ class Get extends Method { super.path, super.headers, super.useBrackets, + super.includeNullQueryVars, }) : super(HttpMethod.Get); } @@ -199,6 +227,7 @@ class Post extends Method { super.path, super.headers, super.useBrackets, + super.includeNullQueryVars, }) : super(HttpMethod.Post); } @@ -210,6 +239,7 @@ class Delete extends Method { super.path, super.headers, super.useBrackets, + super.includeNullQueryVars, }) : super(HttpMethod.Delete); } @@ -223,6 +253,7 @@ class Put extends Method { super.path, super.headers, super.useBrackets, + super.includeNullQueryVars, }) : super(HttpMethod.Put); } @@ -235,6 +266,7 @@ class Patch extends Method { super.path, super.headers, super.useBrackets, + super.includeNullQueryVars, }) : super(HttpMethod.Patch); } @@ -246,6 +278,7 @@ class Head extends Method { super.path, super.headers, super.useBrackets, + super.includeNullQueryVars, }) : super(HttpMethod.Head); } @@ -256,6 +289,7 @@ class Options extends Method { super.path, super.headers, super.useBrackets, + super.includeNullQueryVars, }) : super(HttpMethod.Options); } diff --git a/chopper/lib/src/request.dart b/chopper/lib/src/request.dart index b515ec24..f4189cc9 100644 --- a/chopper/lib/src/request.dart +++ b/chopper/lib/src/request.dart @@ -14,6 +14,7 @@ class Request extends http.BaseRequest { final bool multipart; final List parts; final bool useBrackets; + final bool includeNullQueryVars; Request( String method, @@ -25,9 +26,16 @@ class Request extends http.BaseRequest { this.multipart = false, this.parts = const [], this.useBrackets = false, + this.includeNullQueryVars = false, }) : super( method, - buildUri(origin, path, parameters, useBrackets: useBrackets), + buildUri( + origin, + path, + parameters, + useBrackets: useBrackets, + includeNullQueryVars: includeNullQueryVars, + ), ) { this.headers.addAll(headers); } @@ -44,6 +52,7 @@ class Request extends http.BaseRequest { this.multipart = false, this.parts = const [], this.useBrackets = false, + this.includeNullQueryVars = false, }) : origin = url.origin, path = url.path, parameters = {...url.queryParametersAll, ...?parameters}, @@ -54,6 +63,7 @@ class Request extends http.BaseRequest { url.path, {...url.queryParametersAll, ...?parameters}, useBrackets: useBrackets, + includeNullQueryVars: includeNullQueryVars, ), ) { this.headers.addAll(headers); @@ -70,6 +80,7 @@ class Request extends http.BaseRequest { bool? multipart, List? parts, bool? useBrackets, + bool? includeNullQueryVars, }) => Request( method ?? this.method, @@ -81,6 +92,7 @@ class Request extends http.BaseRequest { multipart: multipart ?? this.multipart, parts: parts ?? this.parts, useBrackets: useBrackets ?? this.useBrackets, + includeNullQueryVars: includeNullQueryVars ?? this.includeNullQueryVars, ); /// Builds a valid URI from [baseUrl], [url] and [parameters]. @@ -92,6 +104,7 @@ class Request extends http.BaseRequest { String url, Map parameters, { bool useBrackets = false, + bool includeNullQueryVars = false, }) { // If the request's url is already a fully qualified URL, we can use it // as-is and ignore the baseUrl. @@ -99,7 +112,11 @@ class Request extends http.BaseRequest { ? Uri.parse(url) : Uri.parse('${baseUrl.strip('/')}/${url.leftStrip('/')}'); - final String query = mapToQuery(parameters, useBrackets: useBrackets); + final String query = mapToQuery( + parameters, + useBrackets: useBrackets, + includeNullQueryVars: includeNullQueryVars, + ); return query.isNotEmpty ? uri.replace(query: uri.hasQuery ? '${uri.query}&$query' : query) diff --git a/chopper/lib/src/utils.dart b/chopper/lib/src/utils.dart index 85ff8c62..299ddead 100644 --- a/chopper/lib/src/utils.dart +++ b/chopper/lib/src/utils.dart @@ -56,13 +56,22 @@ final chopperLogger = Logger('Chopper'); /// Creates a valid URI query string from [map]. /// /// E.g., `{'foo': 'bar', 'ints': [ 1337, 42 ] }` will become 'foo=bar&ints=1337&ints=42'. -String mapToQuery(Map map, {bool useBrackets = false}) => - _mapToQuery(map, useBrackets: useBrackets).join('&'); +String mapToQuery( + Map map, { + bool useBrackets = false, + bool includeNullQueryVars = false, +}) => + _mapToQuery( + map, + useBrackets: useBrackets, + includeNullQueryVars: includeNullQueryVars, + ).join('&'); Iterable<_Pair> _mapToQuery( Map map, { String? prefix, bool useBrackets = false, + bool includeNullQueryVars = false, }) { final Set<_Pair> pairs = {}; @@ -80,7 +89,12 @@ Iterable<_Pair> _mapToQuery( pairs.addAll(_iterableToQuery(name, value, useBrackets: useBrackets)); } else if (value is Map) { pairs.addAll( - _mapToQuery(value, prefix: name, useBrackets: useBrackets), + _mapToQuery( + value, + prefix: name, + useBrackets: useBrackets, + includeNullQueryVars: includeNullQueryVars, + ), ); } else { pairs.add( @@ -88,7 +102,9 @@ Iterable<_Pair> _mapToQuery( ); } } else { - pairs.add(_Pair(name, '')); + if (includeNullQueryVars) { + pairs.add(_Pair(name, '')); + } } }); diff --git a/chopper/test/base_test.dart b/chopper/test/base_test.dart index ffdd5d9d..42b2a39c 100644 --- a/chopper/test/base_test.dart +++ b/chopper/test/base_test.dart @@ -110,7 +110,7 @@ void main() { final httpClient = MockClient((request) async { expect( request.url.toString(), - equals('$baseUrl/test/query?name=&int=&default_value='), + equals('$baseUrl/test/query?name='), ); expect(request.method, equals('GET')); @@ -132,7 +132,7 @@ void main() { final httpClient = MockClient((request) async { expect( request.url.toString(), - equals('$baseUrl/test/query?name=&int=&default_value=42'), + equals('$baseUrl/test/query?name=&default_value=42'), ); expect(request.method, equals('GET')); @@ -923,6 +923,34 @@ void main() { ); }); + test('Include null query vars', () async { + final httpClient = MockClient((request) async { + expect( + request.url.toString(), + equals('$baseUrl/test/query_param_include_null_query_vars' + '?foo=foo_val' + '&bar=' + '&baz=baz_val'), + ); + expect(request.method, equals('GET')); + + return http.Response('get response', 200); + }); + + final chopper = buildClient(httpClient); + final service = chopper.getService(); + + final response = await service.getUsingQueryParamIncludeNullQueryVars( + foo: 'foo_val', + baz: 'baz_val', + ); + + expect(response.body, equals('get response')); + expect(response.statusCode, equals(200)); + + httpClient.close(); + }); + test('List query param', () async { final httpClient = MockClient((request) async { expect( @@ -1067,4 +1095,88 @@ void main() { httpClient.close(); }); + + test('Map query param without including null query vars', () async { + final httpClient = MockClient((request) async { + expect( + request.url.toString(), + equals('$baseUrl/test/map_query_param' + '?value.bar=baz' + '&value.etc.abc=def' + '&value.etc.mno.opq=rst' + '&value.etc.mno.list=a' + '&value.etc.mno.list=123' + '&value.etc.mno.list=false'), + ); + expect(request.method, equals('GET')); + + return http.Response('get response', 200); + }); + + final chopper = buildClient(httpClient); + final service = chopper.getService(); + + final response = await service.getUsingMapQueryParam({ + 'bar': 'baz', + 'zap': null, + 'etc': { + 'abc': 'def', + 'ghi': null, + 'mno': { + 'opq': 'rst', + 'uvw': null, + 'list': ['a', 123, false], + }, + }, + }); + + expect(response.body, equals('get response')); + expect(response.statusCode, equals(200)); + + httpClient.close(); + }); + + test('Map query param including null query vars', () async { + final httpClient = MockClient((request) async { + expect( + request.url.toString(), + equals('$baseUrl/test/map_query_param_include_null_query_vars' + '?value.bar=baz' + '&value.zap=' + '&value.etc.abc=def' + '&value.etc.ghi=' + '&value.etc.mno.opq=rst' + '&value.etc.mno.uvw=' + '&value.etc.mno.list=a' + '&value.etc.mno.list=123' + '&value.etc.mno.list=false'), + ); + expect(request.method, equals('GET')); + + return http.Response('get response', 200); + }); + + final chopper = buildClient(httpClient); + final service = chopper.getService(); + + final response = await service + .getUsingMapQueryParamIncludeNullQueryVars({ + 'bar': 'baz', + 'zap': null, + 'etc': { + 'abc': 'def', + 'ghi': null, + 'mno': { + 'opq': 'rst', + 'uvw': null, + 'list': ['a', 123, false], + }, + }, + }); + + expect(response.body, equals('get response')); + expect(response.statusCode, equals(200)); + + httpClient.close(); + }); } diff --git a/chopper/test/test_service.chopper.dart b/chopper/test/test_service.chopper.dart index d0a43b5c..fce9c168 100644 --- a/chopper/test/test_service.chopper.dart +++ b/chopper/test/test_service.chopper.dart @@ -489,6 +489,28 @@ class _$HttpTestService extends HttpTestService { return client.send($request); } + @override + Future> getUsingQueryParamIncludeNullQueryVars({ + String? foo, + String? bar, + String? baz, + }) { + final String $url = '/test/query_param_include_null_query_vars'; + final Map $params = { + 'foo': foo, + 'bar': bar, + 'baz': baz, + }; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + includeNullQueryVars: true, + ); + return client.send($request); + } + @override Future> getUsingListQueryParam(List value) { final String $url = '/test/list_query_param'; @@ -530,6 +552,21 @@ class _$HttpTestService extends HttpTestService { return client.send($request); } + @override + Future> getUsingMapQueryParamIncludeNullQueryVars( + Map value) { + final String $url = '/test/map_query_param_include_null_query_vars'; + final Map $params = {'value': value}; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + includeNullQueryVars: true, + ); + return client.send($request); + } + @override Future> getUsingMapQueryParamWithBrackets( Map value) { diff --git a/chopper/test/test_service.dart b/chopper/test/test_service.dart index 7789b361..7d19418c 100644 --- a/chopper/test/test_service.dart +++ b/chopper/test/test_service.dart @@ -139,6 +139,13 @@ abstract class HttpTestService extends ChopperService { @Post(path: 'no-body') Future noBody(); + @Get(path: '/query_param_include_null_query_vars', includeNullQueryVars: true) + Future> getUsingQueryParamIncludeNullQueryVars({ + @Query('foo') String? foo, + @Query('bar') String? bar, + @Query('baz') String? baz, + }); + @Get(path: '/list_query_param') Future> getUsingListQueryParam( @Query('value') List value, @@ -154,6 +161,14 @@ abstract class HttpTestService extends ChopperService { @Query('value') Map value, ); + @Get( + path: '/map_query_param_include_null_query_vars', + includeNullQueryVars: true, + ) + Future> getUsingMapQueryParamIncludeNullQueryVars( + @Query('value') Map value, + ); + @Get(path: '/map_query_param_with_brackets', useBrackets: true) Future> getUsingMapQueryParamWithBrackets( @Query('value') Map value, diff --git a/chopper/test/utils_test.dart b/chopper/test/utils_test.dart index 649a4651..bf3f8e6f 100644 --- a/chopper/test/utils_test.dart +++ b/chopper/test/utils_test.dart @@ -4,7 +4,7 @@ import 'package:test/test.dart'; void main() { group('mapToQuery single', () { , String>{ - {'foo': null}: 'foo=', + {'foo': null}: '', {'foo': ''}: 'foo=', {'foo': ' '}: 'foo=%20', {'foo': ' '}: 'foo=%20%20', @@ -28,7 +28,62 @@ void main() { test('$map -> $query', () => expect(mapToQuery(map), query))); }); + group('mapToQuery single with includeNullQueryVars', () { + , String>{ + {'foo': null}: 'foo=', + {'foo': ''}: 'foo=', + {'foo': ' '}: 'foo=%20', + {'foo': ' '}: 'foo=%20%20', + {'foo': '\t'}: 'foo=%09', + {'foo': '\t\t'}: 'foo=%09%09', + {'foo': 'null'}: 'foo=null', + {'foo': 'bar'}: 'foo=bar', + {'foo': ' bar '}: 'foo=%20bar%20', + {'foo': '\tbar\t'}: 'foo=%09bar%09', + {'foo': '\t\tbar\t\t'}: 'foo=%09%09bar%09%09', + {'foo': 123}: 'foo=123', + {'foo': 0}: 'foo=0', + {'foo': -0.01}: 'foo=-0.01', + {'foo': '0.00'}: 'foo=0.00', + {'foo': 123.456}: 'foo=123.456', + {'foo': 123.450}: 'foo=123.45', + {'foo': -123.456}: 'foo=-123.456', + {'foo': true}: 'foo=true', + {'foo': false}: 'foo=false', + }.forEach( + (map, query) => test( + '$map -> $query', + () => expect(mapToQuery(map, includeNullQueryVars: true), query), + ), + ); + }); + group('mapToQuery multiple', () { + , String>{ + {'foo': null, 'baz': null}: '', + {'foo': '', 'baz': ''}: 'foo=&baz=', + {'foo': null, 'baz': ''}: 'baz=', + {'foo': '', 'baz': null}: 'foo=', + {'foo': 'bar', 'baz': ''}: 'foo=bar&baz=', + {'foo': null, 'baz': 'etc'}: 'baz=etc', + {'foo': '', 'baz': 'etc'}: 'foo=&baz=etc', + {'foo': 'bar', 'baz': 'etc'}: 'foo=bar&baz=etc', + {'foo': 'null', 'baz': 'null'}: 'foo=null&baz=null', + {'foo': ' ', 'baz': ' '}: 'foo=%20&baz=%20', + {'foo': '\t', 'baz': '\t'}: 'foo=%09&baz=%09', + {'foo': 123, 'baz': 456}: 'foo=123&baz=456', + {'foo': 0, 'baz': 0}: 'foo=0&baz=0', + {'foo': '0.00', 'baz': '0.00'}: 'foo=0.00&baz=0.00', + {'foo': 123.456, 'baz': 789.012}: 'foo=123.456&baz=789.012', + {'foo': 123.450, 'baz': 789.010}: 'foo=123.45&baz=789.01', + {'foo': -123.456, 'baz': -789.012}: 'foo=-123.456&baz=-789.012', + {'foo': true, 'baz': true}: 'foo=true&baz=true', + {'foo': false, 'baz': false}: 'foo=false&baz=false', + }.forEach((map, query) => + test('$map -> $query', () => expect(mapToQuery(map), query))); + }); + + group('mapToQuery multiple with includeNullQueryVars', () { , String>{ {'foo': null, 'baz': null}: 'foo=&baz=', {'foo': '', 'baz': ''}: 'foo=&baz=', @@ -49,8 +104,12 @@ void main() { {'foo': -123.456, 'baz': -789.012}: 'foo=-123.456&baz=-789.012', {'foo': true, 'baz': true}: 'foo=true&baz=true', {'foo': false, 'baz': false}: 'foo=false&baz=false', - }.forEach((map, query) => - test('$map -> $query', () => expect(mapToQuery(map), query))); + }.forEach( + (map, query) => test( + '$map -> $query', + () => expect(mapToQuery(map, includeNullQueryVars: true), query), + ), + ); }); group('mapToQuery lists', () { @@ -90,11 +149,57 @@ void main() { 'bar': 'baz', 'etc': '', 'xyz': null, - }: 'foo=bar&foo=baz&foo=etc&bar=baz&etc=&xyz=', + }: 'foo=bar&foo=baz&foo=etc&bar=baz&etc=', }.forEach((map, query) => test('$map -> $query', () => expect(mapToQuery(map), query))); }); + group('mapToQuery lists with includeNullQueryVars', () { + , String>{ + { + 'foo': ['bar', 'baz', 'etc'], + }: 'foo=bar&foo=baz&foo=etc', + { + 'foo': ['bar', 123, 456.789, 0, -123, -456.789], + }: 'foo=bar&foo=123&foo=456.789&foo=0&foo=-123&foo=-456.789', + { + 'foo': ['', 'baz', 'etc'], + }: 'foo=baz&foo=etc', + { + 'foo': ['bar', '', 'etc'], + }: 'foo=bar&foo=etc', + { + 'foo': ['bar', 'baz', ''], + }: 'foo=bar&foo=baz', + { + 'foo': [null, 'baz', 'etc'], + }: 'foo=baz&foo=etc', + { + 'foo': ['bar', null, 'etc'], + }: 'foo=bar&foo=etc', + { + 'foo': ['bar', 'baz', null], + }: 'foo=bar&foo=baz', + { + 'foo': ['bar', 'baz', ' '], + }: 'foo=bar&foo=baz&foo=%20', + { + 'foo': ['bar', 'baz', '\t'], + }: 'foo=bar&foo=baz&foo=%09', + { + 'foo': ['bar', 'baz', 'etc'], + 'bar': 'baz', + 'etc': '', + 'xyz': null, + }: 'foo=bar&foo=baz&foo=etc&bar=baz&etc=&xyz=', + }.forEach( + (map, query) => test( + '$map -> $query', + () => expect(mapToQuery(map, includeNullQueryVars: true), query), + ), + ); + }); + group('mapToQuery lists with brackets', () { , String>{ { @@ -132,7 +237,7 @@ void main() { 'bar': 'baz', 'etc': '', 'xyz': null, - }: 'foo%5B%5D=bar&foo%5B%5D=baz&foo%5B%5D=etc&bar=baz&etc=&xyz=', + }: 'foo%5B%5D=bar&foo%5B%5D=baz&foo%5B%5D=etc&bar=baz&etc=', }.forEach( (map, query) => test( '$map -> $query', @@ -144,6 +249,55 @@ void main() { ); }); + group('mapToQuery lists with brackets with includeNullQueryVars', () { + , String>{ + { + 'foo': ['bar', 'baz', 'etc'], + }: 'foo%5B%5D=bar&foo%5B%5D=baz&foo%5B%5D=etc', + { + 'foo': ['bar', 123, 456.789, 0, -123, -456.789], + }: 'foo%5B%5D=bar&foo%5B%5D=123&foo%5B%5D=456.789&foo%5B%5D=0&foo%5B%5D=-123&foo%5B%5D=-456.789', + { + 'foo': ['', 'baz', 'etc'], + }: 'foo%5B%5D=baz&foo%5B%5D=etc', + { + 'foo': ['bar', '', 'etc'], + }: 'foo%5B%5D=bar&foo%5B%5D=etc', + { + 'foo': ['bar', 'baz', ''], + }: 'foo%5B%5D=bar&foo%5B%5D=baz', + { + 'foo': [null, 'baz', 'etc'], + }: 'foo%5B%5D=baz&foo%5B%5D=etc', + { + 'foo': ['bar', null, 'etc'], + }: 'foo%5B%5D=bar&foo%5B%5D=etc', + { + 'foo': ['bar', 'baz', null], + }: 'foo%5B%5D=bar&foo%5B%5D=baz', + { + 'foo': ['bar', 'baz', ' '], + }: 'foo%5B%5D=bar&foo%5B%5D=baz&foo%5B%5D=%20', + { + 'foo': ['bar', 'baz', '\t'], + }: 'foo%5B%5D=bar&foo%5B%5D=baz&foo%5B%5D=%09', + { + 'foo': ['bar', 'baz', 'etc'], + 'bar': 'baz', + 'etc': '', + 'xyz': null, + }: 'foo%5B%5D=bar&foo%5B%5D=baz&foo%5B%5D=etc&bar=baz&etc=&xyz=', + }.forEach( + (map, query) => test( + '$map -> $query', + () => expect( + mapToQuery(map, useBrackets: true, includeNullQueryVars: true), + query, + ), + ), + ); + }); + group('mapToQuery maps', () { , String>{ { @@ -154,7 +308,7 @@ void main() { }: 'foo.bar=', { 'foo': {'bar': null}, - }: 'foo.bar=', + }: '', { 'foo': {'bar': ' '}, }: 'foo.bar=%20', @@ -178,7 +332,7 @@ void main() { 'tab': '\t', 'list': ['a', 123, false], }, - }: 'foo.bar=baz&foo.int=123&foo.double=456.789&foo.zero=0&foo.negInt=-123&foo.negDouble=-456.789&foo.emptyString=&foo.nullValue=&foo.space=%20&foo.tab=%09&foo.list=a&foo.list=123&foo.list=false', + }: 'foo.bar=baz&foo.int=123&foo.double=456.789&foo.zero=0&foo.negInt=-123&foo.negDouble=-456.789&foo.emptyString=&foo.space=%20&foo.tab=%09&foo.list=a&foo.list=123&foo.list=false', { 'foo': {'bar': 'baz'}, 'etc': 'xyz', @@ -206,7 +360,142 @@ void main() { test('$map -> $query', () => expect(mapToQuery(map), query))); }); + group('mapToQuery maps with includeNullQueryVars', () { + , String>{ + { + 'foo': {'bar': 'baz'}, + }: 'foo.bar=baz', + { + 'foo': {'bar': ''}, + }: 'foo.bar=', + { + 'foo': {'bar': null}, + }: 'foo.bar=', + { + 'foo': {'bar': ' '}, + }: 'foo.bar=%20', + { + 'foo': {'bar': '\t'}, + }: 'foo.bar=%09', + { + 'foo': {'bar': 'baz', 'etc': 'xyz', 'space': ' ', 'tab': '\t'}, + }: 'foo.bar=baz&foo.etc=xyz&foo.space=%20&foo.tab=%09', + { + 'foo': { + 'bar': 'baz', + 'int': 123, + 'double': 456.789, + 'zero': 0, + 'negInt': -123, + 'negDouble': -456.789, + 'emptyString': '', + 'nullValue': null, + 'space': ' ', + 'tab': '\t', + 'list': ['a', 123, false], + }, + }: 'foo.bar=baz&foo.int=123&foo.double=456.789&foo.zero=0&foo.negInt=-123&foo.negDouble=-456.789&foo.emptyString=&foo.nullValue=&foo.space=%20&foo.tab=%09&foo.list=a&foo.list=123&foo.list=false', + { + 'foo': {'bar': 'baz'}, + 'etc': 'xyz', + }: 'foo.bar=baz&etc=xyz', + { + 'foo': { + 'bar': 'baz', + 'zap': 'abc', + 'etc': { + 'abc': 'def', + 'ghi': 'jkl', + 'mno': { + 'opq': 'rst', + 'uvw': 'xyz', + 'aab': [ + 'bbc', + 'ccd', + 'eef', + ], + }, + }, + }, + }: 'foo.bar=baz&foo.zap=abc&foo.etc.abc=def&foo.etc.ghi=jkl&foo.etc.mno.opq=rst&foo.etc.mno.uvw=xyz&foo.etc.mno.aab=bbc&foo.etc.mno.aab=ccd&foo.etc.mno.aab=eef', + }.forEach( + (map, query) => test( + '$map -> $query', + () => expect(mapToQuery(map, includeNullQueryVars: true), query), + ), + ); + }); + group('mapToQuery maps with brackets', () { + , String>{ + { + 'foo': {'bar': 'baz'}, + }: 'foo%5Bbar%5D=baz', + { + 'foo': {'bar': ''}, + }: 'foo%5Bbar%5D=', + { + 'foo': {'bar': null}, + }: '', + { + 'foo': {'bar': ' '}, + }: 'foo%5Bbar%5D=%20', + { + 'foo': {'bar': '\t'}, + }: 'foo%5Bbar%5D=%09', + { + 'foo': {'bar': 'baz', 'etc': 'xyz', 'space': ' ', 'tab': '\t'}, + }: 'foo%5Bbar%5D=baz&foo%5Betc%5D=xyz&foo%5Bspace%5D=%20&foo%5Btab%5D=%09', + { + 'foo': { + 'bar': 'baz', + 'int': 123, + 'double': 456.789, + 'zero': 0, + 'negInt': -123, + 'negDouble': -456.789, + 'emptyString': '', + 'nullValue': null, + 'space': ' ', + 'tab': '\t', + 'list': ['a', 123, false], + }, + }: 'foo%5Bbar%5D=baz&foo%5Bint%5D=123&foo%5Bdouble%5D=456.789&foo%5Bzero%5D=0&foo%5BnegInt%5D=-123&foo%5BnegDouble%5D=-456.789&foo%5BemptyString%5D=&foo%5Bspace%5D=%20&foo%5Btab%5D=%09&foo%5Blist%5D%5B%5D=a&foo%5Blist%5D%5B%5D=123&foo%5Blist%5D%5B%5D=false', + { + 'foo': {'bar': 'baz'}, + 'etc': 'xyz', + }: 'foo%5Bbar%5D=baz&etc=xyz', + { + 'foo': { + 'bar': 'baz', + 'zap': 'abc', + 'etc': { + 'abc': 'def', + 'ghi': 'jkl', + 'mno': { + 'opq': 'rst', + 'uvw': 'xyz', + 'aab': [ + 'bbc', + 'ccd', + 'eef', + ], + }, + }, + }, + }: 'foo%5Bbar%5D=baz&foo%5Bzap%5D=abc&foo%5Betc%5D%5Babc%5D=def&foo%5Betc%5D%5Bghi%5D=jkl&foo%5Betc%5D%5Bmno%5D%5Bopq%5D=rst&foo%5Betc%5D%5Bmno%5D%5Buvw%5D=xyz&foo%5Betc%5D%5Bmno%5D%5Baab%5D%5B%5D=bbc&foo%5Betc%5D%5Bmno%5D%5Baab%5D%5B%5D=ccd&foo%5Betc%5D%5Bmno%5D%5Baab%5D%5B%5D=eef', + }.forEach( + (map, query) => test( + '$map -> $query', + () => expect( + mapToQuery(map, useBrackets: true), + query, + ), + ), + ); + }); + + group('mapToQuery maps with brackets with includeNullQueryVars', () { , String>{ { 'foo': {'bar': 'baz'}, @@ -268,7 +557,7 @@ void main() { (map, query) => test( '$map -> $query', () => expect( - mapToQuery(map, useBrackets: true), + mapToQuery(map, useBrackets: true, includeNullQueryVars: true), query, ), ), diff --git a/chopper_generator/analysis_options.yaml b/chopper_generator/analysis_options.yaml index 3a82dc3b..2caa0f09 100644 --- a/chopper_generator/analysis_options.yaml +++ b/chopper_generator/analysis_options.yaml @@ -14,7 +14,7 @@ dart_code_metrics: cyclomatic-complexity: 20 number-of-arguments: 4 maximum-nesting-level: 5 - number-of-parameters: 6 + number-of-parameters: 10 source-lines-of-code: 250 metrics-exclude: - test/** diff --git a/chopper_generator/lib/src/generator.dart b/chopper_generator/lib/src/generator.dart index 6dda3c4a..2ccd6cd3 100644 --- a/chopper_generator/lib/src/generator.dart +++ b/chopper_generator/lib/src/generator.dart @@ -311,6 +311,8 @@ class ChopperGenerator extends GeneratorForAnnotation { final bool useBrackets = getUseBrackets(method); + final bool includeNullQueryVars = getIncludeNullQueryVars(method); + blocks.add( declareFinal(_requestVar, type: refer('Request')) .assign( @@ -321,6 +323,7 @@ class ChopperGenerator extends GeneratorForAnnotation { useHeaders: headers != null, hasParts: hasParts, useBrackets: useBrackets, + includeNullQueryVars: includeNullQueryVars, ), ) .statement, @@ -494,6 +497,7 @@ class ChopperGenerator extends GeneratorForAnnotation { bool useQueries = false, bool useHeaders = false, bool useBrackets = false, + bool includeNullQueryVars = false, }) { final List params = [ literal(getMethodName(method)), @@ -524,6 +528,10 @@ class ChopperGenerator extends GeneratorForAnnotation { namedParams['useBrackets'] = literalBool(useBrackets); } + if (includeNullQueryVars) { + namedParams['includeNullQueryVars'] = literalBool(includeNullQueryVars); + } + return refer('Request').newInstance(params, namedParams); } @@ -631,6 +639,9 @@ String getMethodName(ConstantReader method) => bool getUseBrackets(ConstantReader method) => method.peek('useBrackets')?.boolValue ?? false; +bool getIncludeNullQueryVars(ConstantReader method) => + method.peek('includeNullQueryVars')?.boolValue ?? false; + extension DartTypeExtension on DartType { bool get isNullable => nullabilitySuffix != NullabilitySuffix.none; } From 786c1535b1b85e1f55f4451c0284cd66e64d7dbe Mon Sep 17 00:00:00 2001 From: Ivan Terekhin Date: Sat, 15 Oct 2022 08:30:21 +0300 Subject: [PATCH 035/171] 5.1.0 (dev) (#373) Co-authored-by: Ivan Terekhin <231950+JEuler@users.noreply.github.com> --- chopper/CHANGELOG.md | 5 +++++ chopper/pubspec.yaml | 2 +- chopper_generator/CHANGELOG.md | 4 ++++ chopper_generator/pubspec.yaml | 2 +- 4 files changed, 11 insertions(+), 2 deletions(-) diff --git a/chopper/CHANGELOG.md b/chopper/CHANGELOG.md index e3d9a080..56519d03 100644 --- a/chopper/CHANGELOG.md +++ b/chopper/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## 5.1.0 + +- Base class changed for http.BaseRequest +- Annotation to include null vars in query + ## 5.0.1 - mapToQuery changes diff --git a/chopper/pubspec.yaml b/chopper/pubspec.yaml index 68122b13..51d7abcd 100644 --- a/chopper/pubspec.yaml +++ b/chopper/pubspec.yaml @@ -1,6 +1,6 @@ name: chopper description: Chopper is an http client generator using source_gen, inspired by Retrofit -version: 5.0.1 +version: 5.1.0 documentation: https://hadrien-lejard.gitbook.io/chopper repository: https://github.com/lejard-h/chopper diff --git a/chopper_generator/CHANGELOG.md b/chopper_generator/CHANGELOG.md index 67f4b0e1..185ca6c8 100644 --- a/chopper_generator/CHANGELOG.md +++ b/chopper_generator/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 5.1.0 + +- Annotation to include null vars in query + ## 5.0.1 - Types added diff --git a/chopper_generator/pubspec.yaml b/chopper_generator/pubspec.yaml index 2fedb49a..6937cc90 100644 --- a/chopper_generator/pubspec.yaml +++ b/chopper_generator/pubspec.yaml @@ -1,6 +1,6 @@ name: chopper_generator description: Chopper is an http client generator using source_gen, inspired by Retrofit -version: 5.0.1 +version: 5.1.0 documentation: https://hadrien-lejard.gitbook.io/chopper repository: https://github.com/lejard-h/chopper From 9b2b867f441b5952561dfcdce2c01aa1cb42ba35 Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Sat, 15 Oct 2022 17:22:20 +0100 Subject: [PATCH 036/171] [workflow] Upgrade mono_repo to v6.4.1 (#375) --- .github/workflows/dart.yml | 85 ++++++++++++++++++++--------------- .github/workflows/publish.yml | 12 ++--- mono_repo.yaml | 4 +- tool/ci.sh | 2 +- 4 files changed, 57 insertions(+), 46 deletions(-) diff --git a/.github/workflows/dart.yml b/.github/workflows/dart.yml index 9bf38a8f..df934002 100644 --- a/.github/workflows/dart.yml +++ b/.github/workflows/dart.yml @@ -1,4 +1,4 @@ -# Created with package:mono_repo v6.0.0 +# Created with package:mono_repo v6.4.1 name: Dart CI on: push: @@ -14,6 +14,7 @@ defaults: shell: bash env: PUB_ENVIRONMENT: bot.github +permissions: read-all jobs: job_001: @@ -21,20 +22,22 @@ jobs: runs-on: ubuntu-latest steps: - name: Cache Pub hosted dependencies - uses: actions/cache@v2.1.7 + uses: actions/cache@ac8075791e805656e71b4ba23325ace9e3421120 with: path: "~/.pub-cache/hosted" key: "os:ubuntu-latest;pub-cache-hosted;sdk:stable" restore-keys: | os:ubuntu-latest;pub-cache-hosted os:ubuntu-latest - - uses: dart-lang/setup-dart@v1.3 + - name: Setup Dart SDK + uses: dart-lang/setup-dart@6a218f2413a3e78e9087f638a238f6b40893203d with: sdk: stable - id: checkout - uses: actions/checkout@v2.4.0 + name: Checkout repository + uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b - name: mono_repo self validate - run: dart pub global activate mono_repo 6.0.0 + run: dart pub global activate mono_repo 6.4.1 - name: mono_repo self validate run: dart pub global run mono_repo generate --validate job_002: @@ -42,7 +45,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Cache Pub hosted dependencies - uses: actions/cache@v2.1.7 + uses: actions/cache@ac8075791e805656e71b4ba23325ace9e3421120 with: path: "~/.pub-cache/hosted" key: "os:ubuntu-latest;pub-cache-hosted;sdk:stable;packages:chopper_built_value-chopper_generator;commands:format-analyze" @@ -51,43 +54,45 @@ jobs: os:ubuntu-latest;pub-cache-hosted;sdk:stable os:ubuntu-latest;pub-cache-hosted os:ubuntu-latest - - uses: dart-lang/setup-dart@v1.3 + - name: Setup Dart SDK + uses: dart-lang/setup-dart@6a218f2413a3e78e9087f638a238f6b40893203d with: sdk: stable - id: checkout - uses: actions/checkout@v2.4.0 + name: Checkout repository + uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b - id: chopper_built_value_pub_upgrade name: chopper_built_value; dart pub upgrade + run: dart pub upgrade if: "always() && steps.checkout.conclusion == 'success'" working-directory: chopper_built_value - run: dart pub upgrade - name: "chopper_built_value; dart format --output=none --set-exit-if-changed ." + run: "dart format --output=none --set-exit-if-changed ." if: "always() && steps.chopper_built_value_pub_upgrade.conclusion == 'success'" working-directory: chopper_built_value - run: "dart format --output=none --set-exit-if-changed ." - name: "chopper_built_value; dart analyze --fatal-infos ." + run: dart analyze --fatal-infos . if: "always() && steps.chopper_built_value_pub_upgrade.conclusion == 'success'" working-directory: chopper_built_value - run: dart analyze --fatal-infos . - id: chopper_generator_pub_upgrade name: chopper_generator; dart pub upgrade + run: dart pub upgrade if: "always() && steps.checkout.conclusion == 'success'" working-directory: chopper_generator - run: dart pub upgrade - name: "chopper_generator; dart format --output=none --set-exit-if-changed ." + run: "dart format --output=none --set-exit-if-changed ." if: "always() && steps.chopper_generator_pub_upgrade.conclusion == 'success'" working-directory: chopper_generator - run: "dart format --output=none --set-exit-if-changed ." - name: "chopper_generator; dart analyze --fatal-infos ." + run: dart analyze --fatal-infos . if: "always() && steps.chopper_generator_pub_upgrade.conclusion == 'success'" working-directory: chopper_generator - run: dart analyze --fatal-infos . job_003: name: "analyze_and_format; PKG: chopper; `dart format --output=none --set-exit-if-changed .`, `dart analyze --fatal-infos .`" runs-on: ubuntu-latest steps: - name: Cache Pub hosted dependencies - uses: actions/cache@v2.1.7 + uses: actions/cache@ac8075791e805656e71b4ba23325ace9e3421120 with: path: "~/.pub-cache/hosted" key: "os:ubuntu-latest;pub-cache-hosted;sdk:stable;packages:chopper;commands:format-analyze" @@ -96,24 +101,26 @@ jobs: os:ubuntu-latest;pub-cache-hosted;sdk:stable os:ubuntu-latest;pub-cache-hosted os:ubuntu-latest - - uses: dart-lang/setup-dart@v1.3 + - name: Setup Dart SDK + uses: dart-lang/setup-dart@6a218f2413a3e78e9087f638a238f6b40893203d with: sdk: stable - id: checkout - uses: actions/checkout@v2.4.0 + name: Checkout repository + uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b - id: chopper_pub_upgrade name: chopper; dart pub upgrade + run: dart pub upgrade if: "always() && steps.checkout.conclusion == 'success'" working-directory: chopper - run: dart pub upgrade - name: "chopper; dart format --output=none --set-exit-if-changed ." + run: "dart format --output=none --set-exit-if-changed ." if: "always() && steps.chopper_pub_upgrade.conclusion == 'success'" working-directory: chopper - run: "dart format --output=none --set-exit-if-changed ." - name: "chopper; dart analyze --fatal-infos ." + run: dart analyze --fatal-infos . if: "always() && steps.chopper_pub_upgrade.conclusion == 'success'" working-directory: chopper - run: dart analyze --fatal-infos . needs: - job_001 - job_002 @@ -122,7 +129,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Cache Pub hosted dependencies - uses: actions/cache@v2.1.7 + uses: actions/cache@ac8075791e805656e71b4ba23325ace9e3421120 with: path: "~/.pub-cache/hosted" key: "os:ubuntu-latest;pub-cache-hosted;sdk:stable;packages:chopper-chopper_built_value;commands:test_1" @@ -131,29 +138,31 @@ jobs: os:ubuntu-latest;pub-cache-hosted;sdk:stable os:ubuntu-latest;pub-cache-hosted os:ubuntu-latest - - uses: dart-lang/setup-dart@v1.3 + - name: Setup Dart SDK + uses: dart-lang/setup-dart@6a218f2413a3e78e9087f638a238f6b40893203d with: sdk: stable - id: checkout - uses: actions/checkout@v2.4.0 + name: Checkout repository + uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b - id: chopper_pub_upgrade name: chopper; dart pub upgrade + run: dart pub upgrade if: "always() && steps.checkout.conclusion == 'success'" working-directory: chopper - run: dart pub upgrade - name: "chopper; dart test -p chrome" + run: dart test -p chrome if: "always() && steps.chopper_pub_upgrade.conclusion == 'success'" working-directory: chopper - run: dart test -p chrome - id: chopper_built_value_pub_upgrade name: chopper_built_value; dart pub upgrade + run: dart pub upgrade if: "always() && steps.checkout.conclusion == 'success'" working-directory: chopper_built_value - run: dart pub upgrade - name: "chopper_built_value; dart test -p chrome" + run: dart test -p chrome if: "always() && steps.chopper_built_value_pub_upgrade.conclusion == 'success'" working-directory: chopper_built_value - run: dart test -p chrome needs: - job_001 - job_002 @@ -163,7 +172,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Cache Pub hosted dependencies - uses: actions/cache@v2.1.7 + uses: actions/cache@ac8075791e805656e71b4ba23325ace9e3421120 with: path: "~/.pub-cache/hosted" key: "os:ubuntu-latest;pub-cache-hosted;sdk:stable;packages:chopper-chopper_built_value;commands:test_0" @@ -172,29 +181,31 @@ jobs: os:ubuntu-latest;pub-cache-hosted;sdk:stable os:ubuntu-latest;pub-cache-hosted os:ubuntu-latest - - uses: dart-lang/setup-dart@v1.3 + - name: Setup Dart SDK + uses: dart-lang/setup-dart@6a218f2413a3e78e9087f638a238f6b40893203d with: sdk: stable - id: checkout - uses: actions/checkout@v2.4.0 + name: Checkout repository + uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b - id: chopper_pub_upgrade name: chopper; dart pub upgrade + run: dart pub upgrade if: "always() && steps.checkout.conclusion == 'success'" working-directory: chopper - run: dart pub upgrade - name: chopper; dart test + run: dart test if: "always() && steps.chopper_pub_upgrade.conclusion == 'success'" working-directory: chopper - run: dart test - id: chopper_built_value_pub_upgrade name: chopper_built_value; dart pub upgrade + run: dart pub upgrade if: "always() && steps.checkout.conclusion == 'success'" working-directory: chopper_built_value - run: dart pub upgrade - name: chopper_built_value; dart test + run: dart test if: "always() && steps.chopper_built_value_pub_upgrade.conclusion == 'success'" working-directory: chopper_built_value - run: dart test needs: - job_001 - job_002 @@ -203,15 +214,15 @@ jobs: name: Coverage runs-on: ubuntu-latest steps: - - uses: dart-lang/setup-dart@v1.0 + - uses: dart-lang/setup-dart@v1.3 with: sdk: stable - id: checkout - uses: actions/checkout@v2 + uses: actions/checkout@v3 - id: upload_coverage name: chopper; tool/coverage.sh - if: "always() && steps.checkout.conclusion == 'success'" run: bash tool/coverage.sh + if: "always() && steps.checkout.conclusion == 'success'" env: CODECOV_TOKEN: "${{ secrets.CODECOV_TOKEN }}" needs: diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index b2ac60ec..aea02d47 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -9,11 +9,11 @@ jobs: name: "Publish chopper" runs-on: ubuntu-latest steps: - - uses: dart-lang/setup-dart@v1.0 + - uses: dart-lang/setup-dart@v1.3 with: sdk: stable - id: checkout - uses: actions/checkout@v2 + uses: actions/checkout@v3 - id: publish run: bash tool/publish.sh chopper env: @@ -22,11 +22,11 @@ jobs: name: "Publish chopper_generator" runs-on: ubuntu-latest steps: - - uses: dart-lang/setup-dart@v1.0 + - uses: dart-lang/setup-dart@v1.3 with: sdk: stable - id: checkout - uses: actions/checkout@v2 + uses: actions/checkout@v3 - id: publish run: bash tool/publish.sh chopper_generator env: @@ -35,11 +35,11 @@ jobs: name: "Publish chopper_built_value" runs-on: ubuntu-latest steps: - - uses: dart-lang/setup-dart@v1.0 + - uses: dart-lang/setup-dart@v1.3 with: sdk: stable - id: checkout - uses: actions/checkout@v2 + uses: actions/checkout@v3 - id: publish run: bash tool/publish.sh chopper_built_value env: diff --git a/mono_repo.yaml b/mono_repo.yaml index a7fafb4c..08251824 100644 --- a/mono_repo.yaml +++ b/mono_repo.yaml @@ -14,11 +14,11 @@ github: - name: "Coverage" runs-on: ubuntu-latest steps: - - uses: dart-lang/setup-dart@v1.0 + - uses: dart-lang/setup-dart@v1.3 with: sdk: stable - id: checkout - uses: actions/checkout@v2 + uses: actions/checkout@v3 - id: upload_coverage name: "chopper; tool/coverage.sh" if: "always() && steps.checkout.conclusion == 'success'" diff --git a/tool/ci.sh b/tool/ci.sh index d614ed80..5017b716 100755 --- a/tool/ci.sh +++ b/tool/ci.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Created with package:mono_repo v6.0.0 +# Created with package:mono_repo v6.4.1 # Support built in commands on windows out of the box. # When it is a flutter repo (check the pubspec.yaml for "sdk: flutter") From 233678e90a6d07847cdbfa18337ae8b6adc8c29a Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Mon, 17 Oct 2022 07:38:11 +0100 Subject: [PATCH 037/171] Update analyzer to >=4.4.0 <6.0.0 (#378) --- chopper_generator/pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chopper_generator/pubspec.yaml b/chopper_generator/pubspec.yaml index 6937cc90..a0fe86dc 100644 --- a/chopper_generator/pubspec.yaml +++ b/chopper_generator/pubspec.yaml @@ -8,7 +8,7 @@ environment: sdk: ">=2.17.0 <3.0.0" dependencies: - analyzer: ^4.4.0 + analyzer: '>=4.4.0 <6.0.0' build: ^2.0.0 built_collection: ^5.0.0 chopper: ^5.0.0 From 1b2f1fa91369f13b4e8be0438ce9b93095f9a7d3 Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Tue, 18 Oct 2022 06:00:49 +0100 Subject: [PATCH 038/171] Update mono_repo to 6.4.2 (#380) --- .github/workflows/dart.yml | 24 ++++++++++++------------ tool/ci.sh | 2 +- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/.github/workflows/dart.yml b/.github/workflows/dart.yml index df934002..25ae548e 100644 --- a/.github/workflows/dart.yml +++ b/.github/workflows/dart.yml @@ -1,4 +1,4 @@ -# Created with package:mono_repo v6.4.1 +# Created with package:mono_repo v6.4.2 name: Dart CI on: push: @@ -22,7 +22,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Cache Pub hosted dependencies - uses: actions/cache@ac8075791e805656e71b4ba23325ace9e3421120 + uses: actions/cache@9b0c1fce7a93df8e3bb8926b0d6e9d89e92f20a7 with: path: "~/.pub-cache/hosted" key: "os:ubuntu-latest;pub-cache-hosted;sdk:stable" @@ -35,9 +35,9 @@ jobs: sdk: stable - id: checkout name: Checkout repository - uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b + uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 - name: mono_repo self validate - run: dart pub global activate mono_repo 6.4.1 + run: dart pub global activate mono_repo 6.4.2 - name: mono_repo self validate run: dart pub global run mono_repo generate --validate job_002: @@ -45,7 +45,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Cache Pub hosted dependencies - uses: actions/cache@ac8075791e805656e71b4ba23325ace9e3421120 + uses: actions/cache@9b0c1fce7a93df8e3bb8926b0d6e9d89e92f20a7 with: path: "~/.pub-cache/hosted" key: "os:ubuntu-latest;pub-cache-hosted;sdk:stable;packages:chopper_built_value-chopper_generator;commands:format-analyze" @@ -60,7 +60,7 @@ jobs: sdk: stable - id: checkout name: Checkout repository - uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b + uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 - id: chopper_built_value_pub_upgrade name: chopper_built_value; dart pub upgrade run: dart pub upgrade @@ -92,7 +92,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Cache Pub hosted dependencies - uses: actions/cache@ac8075791e805656e71b4ba23325ace9e3421120 + uses: actions/cache@9b0c1fce7a93df8e3bb8926b0d6e9d89e92f20a7 with: path: "~/.pub-cache/hosted" key: "os:ubuntu-latest;pub-cache-hosted;sdk:stable;packages:chopper;commands:format-analyze" @@ -107,7 +107,7 @@ jobs: sdk: stable - id: checkout name: Checkout repository - uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b + uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 - id: chopper_pub_upgrade name: chopper; dart pub upgrade run: dart pub upgrade @@ -129,7 +129,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Cache Pub hosted dependencies - uses: actions/cache@ac8075791e805656e71b4ba23325ace9e3421120 + uses: actions/cache@9b0c1fce7a93df8e3bb8926b0d6e9d89e92f20a7 with: path: "~/.pub-cache/hosted" key: "os:ubuntu-latest;pub-cache-hosted;sdk:stable;packages:chopper-chopper_built_value;commands:test_1" @@ -144,7 +144,7 @@ jobs: sdk: stable - id: checkout name: Checkout repository - uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b + uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 - id: chopper_pub_upgrade name: chopper; dart pub upgrade run: dart pub upgrade @@ -172,7 +172,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Cache Pub hosted dependencies - uses: actions/cache@ac8075791e805656e71b4ba23325ace9e3421120 + uses: actions/cache@9b0c1fce7a93df8e3bb8926b0d6e9d89e92f20a7 with: path: "~/.pub-cache/hosted" key: "os:ubuntu-latest;pub-cache-hosted;sdk:stable;packages:chopper-chopper_built_value;commands:test_0" @@ -187,7 +187,7 @@ jobs: sdk: stable - id: checkout name: Checkout repository - uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b + uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 - id: chopper_pub_upgrade name: chopper; dart pub upgrade run: dart pub upgrade diff --git a/tool/ci.sh b/tool/ci.sh index 5017b716..372d5024 100755 --- a/tool/ci.sh +++ b/tool/ci.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Created with package:mono_repo v6.4.1 +# Created with package:mono_repo v6.4.2 # Support built in commands on windows out of the box. # When it is a flutter repo (check the pubspec.yaml for "sdk: flutter") From 1e3e7f4c6917e356590f18d278f990495c5987d4 Mon Sep 17 00:00:00 2001 From: Ivan Terekhin Date: Tue, 18 Oct 2022 10:18:29 +0300 Subject: [PATCH 039/171] add techouse (#382) Co-authored-by: Ivan Terekhin <231950+JEuler@users.noreply.github.com> --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index a0b3f6a5..a6a0d409 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
Hadrien Lejard

๐Ÿ’ป ๐Ÿ‘€ โš ๏ธ ๐Ÿ“–
Istvรกn Juhos

๐Ÿ’ป ๐Ÿ‘€ โš ๏ธ ๐Ÿ“– +
Klemen Tusar

๐Ÿ’ป ๐Ÿ‘€ โš ๏ธ ๐Ÿ“–
Ivan Terekhin

๐Ÿ’ป ๐Ÿ‘€ โš ๏ธ ๐Ÿ“–
Eugeny Sampir

๐Ÿ’ป
Uladzimir_Paliukhovich

๐Ÿ’ป @@ -50,4 +51,4 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d -This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome! \ No newline at end of file +This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome! From b0fd18f7e1fb46968dc4a47f3dd61b69aaaaa92e Mon Sep 17 00:00:00 2001 From: Job Guldemeester Date: Mon, 31 Oct 2022 07:06:22 +0100 Subject: [PATCH 040/171] [Feature] Replace the String based path with Uri (#383) --- chopper/example/definition.chopper.dart | 12 +- chopper/example/main.dart | 2 +- chopper/lib/src/base.dart | 50 +++--- chopper/lib/src/request.dart | 92 +++++----- chopper/test/base_test.dart | 168 +++++++++++++++--- chopper/test/client_test.dart | 38 ++-- chopper/test/converter_test.dart | 9 +- chopper/test/interceptors_test.dart | 14 +- chopper/test/multipart_test.dart | 15 +- chopper/test/request_test.dart | 145 ++++++++++++--- chopper/test/test_service.chopper.dart | 72 ++++---- chopper_built_value/test/converter_test.dart | 6 +- chopper_generator/lib/src/generator.dart | 19 +- example/bin/main_built_value.dart | 2 +- example/bin/main_json_serializable.dart | 2 +- ...son_serializable_squadron_worker_pool.dart | 2 +- example/lib/built_value_resource.chopper.dart | 8 +- example/lib/json_serializable.chopper.dart | 10 +- faq.md | 8 +- 19 files changed, 467 insertions(+), 207 deletions(-) diff --git a/chopper/example/definition.chopper.dart b/chopper/example/definition.chopper.dart index 884b3470..c7c86ef7 100644 --- a/chopper/example/definition.chopper.dart +++ b/chopper/example/definition.chopper.dart @@ -18,7 +18,7 @@ class _$MyService extends MyService { @override Future> getResource(String id) { - final String $url = '/resources/${id}'; + final Uri $url = Uri.parse('/resources/${id}'); final Request $request = Request( 'GET', $url, @@ -29,7 +29,7 @@ class _$MyService extends MyService { @override Future>> getMapResource(String id) { - final String $url = '/resources/'; + final Uri $url = Uri.parse('/resources/'); final Map $params = {'id': id}; final Map $headers = { 'foo': 'bar', @@ -46,7 +46,7 @@ class _$MyService extends MyService { @override Future>>> getListResources() { - final String $url = '/resources/resources'; + final Uri $url = Uri.parse('/resources/resources'); final Request $request = Request( 'GET', $url, @@ -61,7 +61,7 @@ class _$MyService extends MyService { String toto, String b, ) { - final String $url = '/resources/'; + final Uri $url = Uri.parse('/resources/'); final $body = { 'a': toto, 'b': b, @@ -81,7 +81,7 @@ class _$MyService extends MyService { Map b, String c, ) { - final String $url = '/resources/multi'; + final Uri $url = Uri.parse('/resources/multi'); final List $parts = [ PartValue>( '1', @@ -108,7 +108,7 @@ class _$MyService extends MyService { @override Future> postFile(List bytes) { - final String $url = '/resources/file'; + final Uri $url = Uri.parse('/resources/file'); final List $parts = [ PartValue>( 'file', diff --git a/chopper/example/main.dart b/chopper/example/main.dart index 16f66abc..b7a2cad4 100644 --- a/chopper/example/main.dart +++ b/chopper/example/main.dart @@ -4,7 +4,7 @@ import 'definition.dart'; Future main() async { final chopper = ChopperClient( - baseUrl: 'http://localhost:8000', + baseUrl: Uri.parse('http://localhost:8000'), services: [ // the generated service MyService.create(ChopperClient()), diff --git a/chopper/lib/src/base.dart b/chopper/lib/src/base.dart index 4e594584..624607b3 100644 --- a/chopper/lib/src/base.dart +++ b/chopper/lib/src/base.dart @@ -29,7 +29,7 @@ final List allowedInterceptorsType = [ class ChopperClient { /// Base URL of each request of the registered services. /// E.g., the hostname of your service. - final String baseUrl; + final Uri baseUrl; /// The [http.Client] used to make network calls. final http.Client httpClient; @@ -60,6 +60,7 @@ class ChopperClient { /// The base URL of each request of the registered services can be defined /// with the [baseUrl] parameter. /// E.g., the hostname of your service. + /// If not provided, a empty default [Uri] will be used. /// /// A custom HTTP client can be passed as the [client] parameter to be used /// with the created [ChopperClient]. @@ -70,7 +71,7 @@ class ChopperClient { /// /// ```dart /// final chopper = ChopperClient( - /// baseUrl: 'localhost:8000', + /// baseUrl: Uri.parse('localhost:8000'), /// services: [ /// // Add a generated service /// TodosListService.create() @@ -111,14 +112,19 @@ class ChopperClient { /// ); /// ``` ChopperClient({ - this.baseUrl = '', + Uri? baseUrl, http.Client? client, Iterable interceptors = const [], this.authenticator, this.converter, this.errorConverter, Iterable services = const [], - }) : httpClient = client ?? http.Client(), + }) : assert( + baseUrl == null || !baseUrl.hasQuery, + 'baseUrl should not contain query parameters.' + 'Use a request interceptor to add default query parameters'), + baseUrl = baseUrl ?? Uri(), + httpClient = client ?? http.Client(), _clientIsInternal = client == null { if (!interceptors.every(_isAnInterceptor)) { throw ArgumentError( @@ -152,7 +158,7 @@ class ChopperClient { /// /// ```dart /// final chopper = ChopperClient( - /// baseUrl: 'localhost:8000', + /// baseUrl: Uri.parse('localhost:8000'), /// services: [ /// // Add a generated service /// TodosListService.create() @@ -341,10 +347,10 @@ class ChopperClient { /// Makes a HTTP GET request using the [send] function. Future> get( - String url, { + Uri url, { Map headers = const {}, + Uri? baseUrl, Map parameters = const {}, - String? baseUrl, dynamic body, }) => send( @@ -360,13 +366,13 @@ class ChopperClient { /// Makes a HTTP POST request using the [send] function Future> post( - String url, { + Uri url, { dynamic body, List parts = const [], Map headers = const {}, Map parameters = const {}, bool multipart = false, - String? baseUrl, + Uri? baseUrl, }) => send( Request( @@ -376,20 +382,20 @@ class ChopperClient { body: body, parts: parts, headers: headers, - multipart: multipart, parameters: parameters, + multipart: multipart, ), ); /// Makes a HTTP PUT request using the [send] function. Future> put( - String url, { + Uri url, { dynamic body, List parts = const [], Map headers = const {}, Map parameters = const {}, bool multipart = false, - String? baseUrl, + Uri? baseUrl, }) => send( Request( @@ -399,20 +405,20 @@ class ChopperClient { body: body, parts: parts, headers: headers, - multipart: multipart, parameters: parameters, + multipart: multipart, ), ); /// Makes a HTTP PATCH request using the [send] function. Future> patch( - String url, { + Uri url, { dynamic body, List parts = const [], Map headers = const {}, Map parameters = const {}, bool multipart = false, - String? baseUrl, + Uri? baseUrl, }) => send( Request( @@ -422,17 +428,17 @@ class ChopperClient { body: body, parts: parts, headers: headers, - multipart: multipart, parameters: parameters, + multipart: multipart, ), ); /// Makes a HTTP DELETE request using the [send] function. Future> delete( - String url, { + Uri url, { Map headers = const {}, Map parameters = const {}, - String? baseUrl, + Uri? baseUrl, }) => send( Request( @@ -446,10 +452,10 @@ class ChopperClient { /// Makes a HTTP HEAD request using the [send] function. Future> head( - String url, { + Uri url, { Map headers = const {}, Map parameters = const {}, - String? baseUrl, + Uri? baseUrl, }) => send( Request( @@ -463,10 +469,10 @@ class ChopperClient { /// Makes a HTTP OPTIONS request using the [send] function. Future> options( - String url, { + Uri url, { Map headers = const {}, Map parameters = const {}, - String? baseUrl, + Uri? baseUrl, }) => send( Request( diff --git a/chopper/lib/src/request.dart b/chopper/lib/src/request.dart index f4189cc9..e418f2c2 100644 --- a/chopper/lib/src/request.dart +++ b/chopper/lib/src/request.dart @@ -7,8 +7,8 @@ import 'package:meta/meta.dart'; /// This class represents an HTTP request that can be made with Chopper. class Request extends http.BaseRequest { - final String path; - final String origin; + final Uri uri; + final Uri baseUri; final dynamic body; final Map parameters; final bool multipart; @@ -18,34 +18,8 @@ class Request extends http.BaseRequest { Request( String method, - this.path, - this.origin, { - this.body, - this.parameters = const {}, - Map headers = const {}, - this.multipart = false, - this.parts = const [], - this.useBrackets = false, - this.includeNullQueryVars = false, - }) : super( - method, - buildUri( - origin, - path, - parameters, - useBrackets: useBrackets, - includeNullQueryVars: includeNullQueryVars, - ), - ) { - this.headers.addAll(headers); - } - - /// Build the Chopper [Request] using a [Uri] instead of a [path] and [origin]. - /// Both the query parameters in the [Uri] and those provided explicitly in - /// the [parameters] are merged together. - Request.uri( - String method, - Uri url, { + this.uri, + this.baseUri, { this.body, Map? parameters, Map headers = const {}, @@ -53,15 +27,18 @@ class Request extends http.BaseRequest { this.parts = const [], this.useBrackets = false, this.includeNullQueryVars = false, - }) : origin = url.origin, - path = url.path, - parameters = {...url.queryParametersAll, ...?parameters}, + }) : assert( + !baseUri.hasQuery, + 'baseUri should not contain query parameters.' + 'Use a request interceptor to add default query parameters'), + // Merge uri.queryParametersAll in the final parameters object so the request object reflects all configured queryParameters + parameters = {...uri.queryParametersAll, ...?parameters}, super( method, buildUri( - url.origin, - url.path, - {...url.queryParametersAll, ...?parameters}, + baseUri, + uri, + {...uri.queryParametersAll, ...?parameters}, useBrackets: useBrackets, includeNullQueryVars: includeNullQueryVars, ), @@ -72,8 +49,8 @@ class Request extends http.BaseRequest { /// Makes a copy of this [Request], replacing original values with the given ones. Request copyWith({ String? method, - String? path, - String? origin, + Uri? uri, + Uri? baseUri, dynamic body, Map? parameters, Map? headers, @@ -84,8 +61,8 @@ class Request extends http.BaseRequest { }) => Request( method ?? this.method, - path ?? this.path, - origin ?? this.origin, + uri ?? this.uri, + baseUri ?? this.baseUri, body: body ?? this.body, parameters: parameters ?? this.parameters, headers: headers ?? this.headers, @@ -100,27 +77,44 @@ class Request extends http.BaseRequest { /// If [url] starts with 'http://' or 'https://', baseUrl is ignored. @visibleForTesting static Uri buildUri( - String baseUrl, - String url, + Uri baseUrl, + Uri url, Map parameters, { bool useBrackets = false, bool includeNullQueryVars = false, }) { // If the request's url is already a fully qualified URL, we can use it // as-is and ignore the baseUrl. - final Uri uri = url.startsWith('http://') || url.startsWith('https://') - ? Uri.parse(url) - : Uri.parse('${baseUrl.strip('/')}/${url.leftStrip('/')}'); + final Uri uri = url.isScheme('HTTP') || url.isScheme('HTTPS') + ? url + : _mergeUri(baseUrl, url); + + // Check if parameter also has all the queryParameters from the url (not the merged uri) + final bool parametersContainsUriQuery = parameters.keys + .every((element) => url.queryParametersAll.keys.contains(element)); + final Map allParameters = parametersContainsUriQuery + ? parameters + : {...url.queryParametersAll, ...parameters}; final String query = mapToQuery( - parameters, + allParameters, useBrackets: useBrackets, includeNullQueryVars: includeNullQueryVars, ); - return query.isNotEmpty - ? uri.replace(query: uri.hasQuery ? '${uri.query}&$query' : query) - : uri; + return query.isNotEmpty ? uri.replace(query: query) : uri; + } + + /// Merges Uri into another Uri preserving queries and paths + static Uri _mergeUri(Uri baseUri, Uri addToUri) { + final path = baseUri.hasEmptyPath + ? addToUri.path + : '${baseUri.path.rightStrip('/')}/${addToUri.path.leftStrip('/')}'; + + return baseUri.replace( + path: path, + query: addToUri.hasQuery ? addToUri.query : null, + ); } /// Converts this Chopper Request into a [http.BaseRequest]. diff --git a/chopper/test/base_test.dart b/chopper/test/base_test.dart index 42b2a39c..d6486c2e 100644 --- a/chopper/test/base_test.dart +++ b/chopper/test/base_test.dart @@ -10,7 +10,7 @@ import 'package:test/test.dart'; import 'test_service.dart'; -const baseUrl = 'http://localhost:8000'; +final baseUrl = Uri.parse('http://localhost:8000'); void main() { ChopperClient buildClient([ @@ -404,7 +404,7 @@ void main() { test('applyHeader', () { final req1 = applyHeader( - Request('GET', '/', baseUrl), + Request('GET', Uri.parse('/'), baseUrl), 'foo', 'bar', ); @@ -412,7 +412,7 @@ void main() { expect(req1.headers, equals({'foo': 'bar'})); final req2 = applyHeader( - Request('GET', '/', baseUrl, headers: {'foo': 'bar'}), + Request('GET', Uri.parse('/'), baseUrl, headers: {'foo': 'bar'}), 'bar', 'foo', ); @@ -420,7 +420,7 @@ void main() { expect(req2.headers, equals({'foo': 'bar', 'bar': 'foo'})); final req3 = applyHeader( - Request('GET', '/', baseUrl, headers: {'foo': 'bar'}), + Request('GET', Uri.parse('/'), baseUrl, headers: {'foo': 'bar'}), 'foo', 'foo', ); @@ -429,19 +429,20 @@ void main() { }); test('applyHeaders', () { - final req1 = applyHeaders(Request('GET', '/', baseUrl), {'foo': 'bar'}); + final req1 = + applyHeaders(Request('GET', Uri.parse('/'), baseUrl), {'foo': 'bar'}); expect(req1.headers, equals({'foo': 'bar'})); final req2 = applyHeaders( - Request('GET', '/', baseUrl, headers: {'foo': 'bar'}), + Request('GET', Uri.parse('/'), baseUrl, headers: {'foo': 'bar'}), {'bar': 'foo'}, ); expect(req2.headers, equals({'foo': 'bar', 'bar': 'foo'})); final req3 = applyHeaders( - Request('GET', '/', baseUrl, headers: {'foo': 'bar'}), + Request('GET', Uri.parse('/'), baseUrl, headers: {'foo': 'bar'}), {'foo': 'foo'}, ); @@ -471,49 +472,56 @@ void main() { test('url concatenation', () async { expect( - Request.buildUri('foo', 'bar', {}).toString(), + Request.buildUri(Uri.parse('foo'), Uri.parse('bar'), {}).toString(), equals('foo/bar'), ); expect( - Request.buildUri('foo/', 'bar', {}).toString(), + Request.buildUri(Uri.parse('foo/'), Uri.parse('bar'), {}).toString(), equals('foo/bar'), ); expect( - Request.buildUri('foo', '/bar', {}).toString(), + Request.buildUri(Uri.parse('foo'), Uri.parse('/bar'), {}).toString(), equals('foo/bar'), ); expect( - Request.buildUri('foo/', '/bar', {}).toString(), + Request.buildUri(Uri.parse('foo/'), Uri.parse('/bar'), {}).toString(), equals('foo/bar'), ); expect( - Request.buildUri('http://foo', '/bar', {}).toString(), + Request.buildUri(Uri.parse('http://foo'), Uri.parse('/bar'), {}) + .toString(), equals('http://foo/bar'), ); expect( - Request.buildUri('https://foo', '/bar', {}).toString(), + Request.buildUri(Uri.parse('https://foo'), Uri.parse('/bar'), {}) + .toString(), equals('https://foo/bar'), ); expect( - Request.buildUri('https://foo/', '/bar', {}).toString(), + Request.buildUri(Uri.parse('https://foo/'), Uri.parse('/bar'), {}) + .toString(), equals('https://foo/bar'), ); expect( - Request.buildUri('https://foo/', '/bar', {'abc': 'xyz'}).toString(), + Request.buildUri( + Uri.parse('https://foo/'), + Uri.parse('/bar'), + {'abc': 'xyz'}, + ).toString(), equals('https://foo/bar?abc=xyz'), ); expect( Request.buildUri( - 'https://foo/', - '/bar?first=123&second=456', + Uri.parse('https://foo/'), + Uri.parse('/bar?first=123&second=456'), { 'third': '789', 'fourth': '012', @@ -521,12 +529,81 @@ void main() { ).toString(), equals('https://foo/bar?first=123&second=456&third=789&fourth=012'), ); + + expect( + Request.buildUri( + Uri.parse('https://foo?first=123&second=456'), + Uri.parse('/bar'), + { + 'third': '789', + 'fourth': '012', + }, + ).toString(), + equals('https://foo/bar?third=789&fourth=012'), + ); + + expect( + Request.buildUri( + Uri.parse('https://foo?first=123&second=456'), + Uri.parse('/bar?third=789&fourth=012'), + { + 'fifth': '345', + 'sixth': '678', + }, + ).toString(), + equals( + 'https://foo/bar?third=789&fourth=012&fifth=345&sixth=678', + ), + ); + + expect( + Request.buildUri( + Uri.parse('https://foo.bar/foobar'), + Uri.parse('whatbar'), + {}, + ).toString(), + equals('https://foo.bar/foobar/whatbar'), + ); + + expect( + Request.buildUri( + Uri.parse('https://foo/bar?first=123&second=456'), + Uri.parse('https://bar/foo?fourth=789&fifth=012'), + {}, + ).toString(), + equals('https://bar/foo?fourth=789&fifth=012'), + ); + + expect( + Request('GET', Uri(path: '/bar'), Uri.parse('foo')).url.toString(), + equals('foo/bar'), + ); + + expect( + Request('GET', Uri(host: 'bar'), Uri.parse('foo')).url.toString(), + equals('foo/'), + ); + + expect( + Request('GET', Uri.https('bar'), Uri.parse('foo')).url.toString(), + equals('https://bar'), + ); + + expect( + Request( + 'GET', + Uri(scheme: 'https', host: 'bar', port: 666), + Uri.parse('foo'), + ).url.toString(), + equals('https://bar:666'), + ); }); test('BodyBytes', () { - final request = Request.uri( + final request = Request( HttpMethod.Post, Uri.parse('https://foo/'), + Uri.parse(''), body: [1, 2, 3], ).toHttpRequest(); @@ -534,9 +611,10 @@ void main() { }); test('BodyFields', () { - final request = Request.uri( + final request = Request( HttpMethod.Post, Uri.parse('https://foo/'), + Uri.parse(''), body: {'foo': 'bar'}, ).toHttpRequest(); @@ -545,9 +623,10 @@ void main() { test('Wrong body', () { try { - Request.uri( + Request( HttpMethod.Post, Uri.parse('https://foo/'), + Uri.parse(''), body: {'foo': 42}, ).toHttpRequest(); } on ArgumentError catch (e) { @@ -1179,4 +1258,53 @@ void main() { httpClient.close(); }); + + test('client baseUrl cannot contain query parameters', () { + expect( + () => ChopperClient( + baseUrl: Uri.http( + 'foo', + 'bar', + { + 'first': '123', + 'second': '456', + }, + ), + ), + throwsA( + TypeMatcher(), + ), + ); + + expect( + () => ChopperClient( + baseUrl: Uri.parse('foo/bar?first=123'), + ), + throwsA( + TypeMatcher(), + ), + ); + + expect( + () => ChopperClient( + baseUrl: Uri( + queryParameters: { + 'first': '123', + 'second': '456', + }, + ), + ), + throwsA( + TypeMatcher(), + ), + ); + expect( + () => ChopperClient( + baseUrl: Uri(query: 'first=123&second=456'), + ), + throwsA( + TypeMatcher(), + ), + ); + }); } diff --git a/chopper/test/client_test.dart b/chopper/test/client_test.dart index babb172d..88b0d2a1 100644 --- a/chopper/test/client_test.dart +++ b/chopper/test/client_test.dart @@ -5,7 +5,7 @@ import 'package:http/http.dart' as http; import 'package:http/testing.dart'; import 'package:test/test.dart'; -const baseUrl = 'http://localhost:8000'; +final baseUrl = Uri.parse('http://localhost:8000'); void main() { ChopperClient buildClient([http.Client? httpClient]) => ChopperClient( @@ -33,9 +33,11 @@ void main() { final chopper = buildClient(httpClient); final response = await chopper.get( - '/test/get', + Uri( + path: '/test/get', + queryParameters: {'key': 'val'}, + ), headers: {'int': '42'}, - parameters: {'key': 'val'}, ); expect(response.body, equals('get response')); @@ -59,10 +61,12 @@ void main() { final chopper = buildClient(httpClient); final response = await chopper.post( - '/test/post', + Uri( + path: '/test/post', + queryParameters: {'key': 'val'}, + ), headers: {'int': '42'}, body: {'content': 'body'}, - parameters: {'key': 'val'}, ); expect(response.body, equals('post response')); @@ -87,10 +91,12 @@ void main() { final chopper = buildClient(httpClient); final response = await chopper.put( - '/test/put', + Uri( + path: '/test/put', + queryParameters: {'key': 'val'}, + ), headers: {'int': '42'}, body: {'content': 'body'}, - parameters: {'key': 'val'}, ); expect(response.body, equals('put response')); @@ -115,10 +121,12 @@ void main() { final chopper = buildClient(httpClient); final response = await chopper.patch( - '/test/patch', + Uri( + path: '/test/patch', + queryParameters: {'key': 'val'}, + ), headers: {'int': '42'}, body: {'content': 'body'}, - parameters: {'key': 'val'}, ); expect(response.body, equals('patch response')); @@ -142,9 +150,11 @@ void main() { final chopper = buildClient(httpClient); final response = await chopper.delete( - '/test/delete', + Uri( + path: '/test/delete', + queryParameters: {'key': 'val'}, + ), headers: {'int': '42'}, - parameters: {'key': 'val'}, ); expect(response.body, equals('delete response')); @@ -167,9 +177,11 @@ void main() { final chopper = buildClient(httpClient); final response = await chopper.options( - '/test/get', + Uri( + path: '/test/get', + queryParameters: {'key': 'val'}, + ), headers: {'int': '42'}, - parameters: {'key': 'val'}, ); expect(response.body, equals('get response')); diff --git a/chopper/test/converter_test.dart b/chopper/test/converter_test.dart index 2fae6736..6b5d868c 100644 --- a/chopper/test/converter_test.dart +++ b/chopper/test/converter_test.dart @@ -7,7 +7,7 @@ import 'package:test/test.dart'; import 'test_service.dart'; -const baseUrl = 'http://localhost:8000'; +final baseUrl = Uri.parse('http://localhost:8000'); void main() { group('Converter', () { @@ -34,7 +34,12 @@ void main() { final converter = TestConverter(); final encoded = converter.convertRequest( - Request('GET', '/', baseUrl, body: _Converted('foo')), + Request( + 'GET', + Uri.parse('/'), + baseUrl, + body: _Converted('foo'), + ), ); expect(encoded.body is String, isTrue); diff --git a/chopper/test/interceptors_test.dart b/chopper/test/interceptors_test.dart index 16257dca..bffcd040 100644 --- a/chopper/test/interceptors_test.dart +++ b/chopper/test/interceptors_test.dart @@ -47,8 +47,9 @@ void main() { test('RequestInterceptorFunc', () async { final chopper = ChopperClient( interceptors: [ - (Request request) => - request.copyWith(path: '${request.url}/intercept'), + (Request request) => request.copyWith( + uri: request.uri.replace(path: '${request.uri.path}/intercept'), + ), ], services: [ HttpTestService.create(), @@ -184,8 +185,8 @@ void main() { final fakeRequest = Request( 'POST', - '/', - 'base', + Uri.parse('/'), + Uri.parse('base'), body: 'test', headers: {'foo': 'bar'}, ); @@ -270,8 +271,9 @@ class ResponseIntercept implements ResponseInterceptor { class RequestIntercept implements RequestInterceptor { @override - FutureOr onRequest(Request request) => - request.copyWith(path: '${request.url}/intercept'); + FutureOr onRequest(Request request) => request.copyWith( + uri: request.uri.replace(path: '${request.uri}/intercept'), + ); } class _Intercepted { diff --git a/chopper/test/multipart_test.dart b/chopper/test/multipart_test.dart index 1e461d2f..9bffea50 100644 --- a/chopper/test/multipart_test.dart +++ b/chopper/test/multipart_test.dart @@ -203,9 +203,10 @@ void main() { }); test('PartValue', () async { - final req = await Request.uri( + final req = await Request( HttpMethod.Post, Uri.parse('https://foo/'), + Uri.parse(''), parts: [ PartValue('foo', 'bar'), PartValue('int', 42), @@ -219,9 +220,10 @@ void main() { test( 'PartFile', () async { - final req = await Request.uri( + final req = await Request( HttpMethod.Post, Uri.parse('https://foo/'), + Uri.parse(''), parts: [ PartValueFile('foo', 'test/multipart_test.dart'), PartValueFile>('int', [1, 2]), @@ -257,9 +259,10 @@ void main() { }); test('Multipart request non nullable', () async { - final req = await Request.uri( + final req = await Request( HttpMethod.Post, Uri.parse('https://foo/'), + Uri.parse(''), parts: [ PartValue('int', 42), PartValueFile>('list int', [1, 2]), @@ -276,9 +279,10 @@ void main() { }); test('PartValue with MultipartFile directly', () async { - final req = await Request.uri( + final req = await Request( HttpMethod.Post, Uri.parse('https://foo/'), + Uri.parse(''), parts: [ PartValue( '', @@ -314,9 +318,10 @@ void main() { test('Throw exception', () async { expect( - () async => await Request.uri( + () async => await Request( HttpMethod.Post, Uri.parse('https://foo/'), + Uri.parse(''), parts: [ PartValueFile('', 123), ], diff --git a/chopper/test/request_test.dart b/chopper/test/request_test.dart index 6f886afd..45d9e2e0 100644 --- a/chopper/test/request_test.dart +++ b/chopper/test/request_test.dart @@ -1,42 +1,46 @@ // ignore_for_file: long-method import 'package:chopper/chopper.dart'; -import 'package:test/test.dart'; -import 'package:http/http.dart' as http; import 'package:collection/collection.dart'; +import 'package:http/http.dart' as http; +import 'package:test/test.dart'; void main() { group('Request', () { test('constructor produces a BaseRequest', () { expect( - Request('GET', '/bar', 'https://foo/'), + Request('GET', Uri.parse('/bar'), Uri.parse('https://foo/')), isA(), ); }); test('method gets preserved in BaseRequest', () { expect( - Request('GET', '/bar', 'https://foo/').method, + Request('GET', Uri.parse('/bar'), Uri.parse('https://foo/')).method, equals('GET'), ); }); test('url is correctly parsed and set in BaseRequest', () { expect( - Request('GET', '/bar', 'https://foo/').url, + Request('GET', Uri.parse('/bar'), Uri.parse('https://foo/')).url, equals(Uri.parse('https://foo/bar')), ); expect( - Request('GET', '/bar?lorem=ipsum&dolor=123', 'https://foo/').url, + Request( + 'GET', + Uri.parse('/bar?lorem=ipsum&dolor=123'), + Uri.parse('https://foo/'), + ).url, equals(Uri.parse('https://foo/bar?lorem=ipsum&dolor=123')), ); expect( Request( 'GET', - '/bar', - 'https://foo/', + Uri.parse('/bar'), + Uri.parse('https://foo/'), parameters: { 'lorem': 'ipsum', 'dolor': 123, @@ -48,8 +52,8 @@ void main() { expect( Request( 'GET', - '/bar?first=sit&second=amet&first_list=a&first_list=b', - 'https://foo/', + Uri.parse('/bar?first=sit&second=amet&first_list=a&first_list=b'), + Uri.parse('https://foo/'), parameters: { 'lorem': 'ipsum', 'dolor': 123, @@ -70,8 +74,8 @@ void main() { final Request request = Request( 'GET', - '/bar', - 'https://foo/', + Uri.parse('/bar'), + Uri.parse('https://foo/'), headers: headers, ); @@ -83,43 +87,48 @@ void main() { test('copyWith creates a BaseRequest', () { expect( - Request('GET', '/bar', 'https://foo/').copyWith(method: HttpMethod.Put), + Request('GET', Uri.parse('/bar'), Uri.parse('https://foo/')) + .copyWith(method: HttpMethod.Put), isA(), ); }); }); - group('Request.uri', () { + group('Request', () { test('constructor produces a BaseRequest', () { expect( - Request.uri('GET', Uri.parse('https://foo/bar')), + Request('GET', Uri.parse('https://foo/bar'), Uri.parse('')), isA(), ); }); test('method gets preserved in BaseRequest', () { expect( - Request.uri('GET', Uri.parse('https://foo/bar')).method, + Request('GET', Uri.parse('https://foo/bar'), Uri.parse('')).method, equals('GET'), ); }); test('url is correctly parsed and set in BaseRequest', () { expect( - Request.uri('GET', Uri.parse('https://foo/bar')).url, + Request('GET', Uri.parse('https://foo/bar'), Uri.parse('')).url, equals(Uri.parse('https://foo/bar')), ); expect( - Request.uri('GET', Uri.parse('https://foo/bar?lorem=ipsum&dolor=123')) - .url, + Request( + 'GET', + Uri.parse('https://foo/bar?lorem=ipsum&dolor=123'), + Uri.parse(''), + ).url, equals(Uri.parse('https://foo/bar?lorem=ipsum&dolor=123')), ); expect( - Request.uri( + Request( 'GET', Uri.parse('https://foo/bar'), + Uri.parse(''), parameters: { 'lorem': 'ipsum', 'dolor': 123, @@ -129,11 +138,12 @@ void main() { ); expect( - Request.uri( + Request( 'GET', Uri.parse( 'https://foo/bar?first=sit&second=amet&first_list=a&first_list=b', ), + Uri.parse(''), parameters: { 'lorem': 'ipsum', 'dolor': 123, @@ -144,6 +154,37 @@ void main() { 'https://foo/bar?first=sit&second=amet&first_list=a&first_list=b&lorem=ipsum&dolor=123&second_list=a&second_list=b', )), ); + + expect( + Request( + 'GET', + Uri.parse( + 'https://chopper.dev/test3', + ), + Uri.parse(''), + parameters: { + 'foo': 'bar', + 'foo_list': [ + 'one', + 'two', + 'three', + ], + 'user': { + 'name': 'john', + 'surname': 'doe', + }, + }, + ).url.toString(), + equals( + 'https://chopper.dev/test3' + '?foo=bar' + '&foo_list=one' + '&foo_list=two' + '&foo_list=three' + '&user.name=john' + '&user.surname=doe', + ), + ); }); test('headers are preserved in BaseRequest', () { @@ -152,9 +193,10 @@ void main() { 'accept': 'application/json; charset=utf-8', }; - final Request request = Request.uri( + final Request request = Request( 'GET', Uri.parse('https://foo/bar'), + Uri.parse(''), headers: headers, ); @@ -166,10 +208,67 @@ void main() { test('copyWith creates a BaseRequest', () { expect( - Request.uri('GET', Uri.parse('https://foo/bar')) + Request('GET', Uri.parse('https://foo/bar'), Uri.parse('')) .copyWith(method: HttpMethod.Put), isA(), ); }); }); + + test('request baseUri cannot contain query parameters', () { + expect( + () => Request( + 'GET', + Uri.parse('foo'), + Uri.http( + 'foo', + 'bar', + { + 'first': '123', + 'second': '456', + }, + ), + ), + throwsA( + TypeMatcher(), + ), + ); + + expect( + () => Request( + 'GET', + Uri.parse('foo'), + Uri.parse('foo/bar?first=123'), + ), + throwsA( + TypeMatcher(), + ), + ); + + expect( + () => Request( + 'GET', + Uri.parse('foo'), + Uri( + queryParameters: { + 'first': '123', + 'second': '456', + }, + ), + ), + throwsA( + TypeMatcher(), + ), + ); + expect( + () => Request( + 'GET', + Uri.parse('foo'), + Uri(query: 'first=123&second=456'), + ), + throwsA( + TypeMatcher(), + ), + ); + }); } diff --git a/chopper/test/test_service.chopper.dart b/chopper/test/test_service.chopper.dart index fce9c168..97ce5db3 100644 --- a/chopper/test/test_service.chopper.dart +++ b/chopper/test/test_service.chopper.dart @@ -21,7 +21,7 @@ class _$HttpTestService extends HttpTestService { String id, { required String dynamicHeader, }) { - final String $url = '/test/get/${id}'; + final Uri $url = Uri.parse('/test/get/${id}'); final Map $headers = { 'test': dynamicHeader, }; @@ -36,7 +36,7 @@ class _$HttpTestService extends HttpTestService { @override Future> headTest() { - final String $url = '/test/head'; + final Uri $url = Uri.parse('/test/head'); final Request $request = Request( 'HEAD', $url, @@ -47,7 +47,7 @@ class _$HttpTestService extends HttpTestService { @override Future> optionsTest() { - final String $url = '/test/options'; + final Uri $url = Uri.parse('/test/options'); final Request $request = Request( 'OPTIONS', $url, @@ -58,7 +58,7 @@ class _$HttpTestService extends HttpTestService { @override Future>>> getStreamTest() { - final String $url = '/test/get'; + final Uri $url = Uri.parse('/test/get'); final Request $request = Request( 'GET', $url, @@ -69,7 +69,7 @@ class _$HttpTestService extends HttpTestService { @override Future> getAll() { - final String $url = '/test'; + final Uri $url = Uri.parse('/test'); final Request $request = Request( 'GET', $url, @@ -80,7 +80,7 @@ class _$HttpTestService extends HttpTestService { @override Future> getAllWithTrailingSlash() { - final String $url = '/test/'; + final Uri $url = Uri.parse('/test/'); final Request $request = Request( 'GET', $url, @@ -95,7 +95,7 @@ class _$HttpTestService extends HttpTestService { int? number, int? def = 42, }) { - final String $url = '/test/query'; + final Uri $url = Uri.parse('/test/query'); final Map $params = { 'name': name, 'int': number, @@ -112,7 +112,7 @@ class _$HttpTestService extends HttpTestService { @override Future> getQueryMapTest(Map query) { - final String $url = '/test/query_map'; + final Uri $url = Uri.parse('/test/query_map'); final Map $params = query; final Request $request = Request( 'GET', @@ -128,7 +128,7 @@ class _$HttpTestService extends HttpTestService { Map query, { bool? test, }) { - final String $url = '/test/query_map'; + final Uri $url = Uri.parse('/test/query_map'); final Map $params = {'test': test}; $params.addAll(query); final Request $request = Request( @@ -146,7 +146,7 @@ class _$HttpTestService extends HttpTestService { int? number, Map filters = const {}, }) { - final String $url = '/test/query_map'; + final Uri $url = Uri.parse('/test/query_map'); final Map $params = { 'name': name, 'number': number, @@ -167,7 +167,7 @@ class _$HttpTestService extends HttpTestService { int? number, Map? filters, }) { - final String $url = '/test/query_map'; + final Uri $url = Uri.parse('/test/query_map'); final Map $params = { 'name': name, 'number': number, @@ -184,7 +184,7 @@ class _$HttpTestService extends HttpTestService { @override Future> getQueryMapTest5({Map? filters}) { - final String $url = '/test/query_map'; + final Uri $url = Uri.parse('/test/query_map'); final Map $params = filters ?? const {}; final Request $request = Request( 'GET', @@ -197,7 +197,7 @@ class _$HttpTestService extends HttpTestService { @override Future> getBody(dynamic body) { - final String $url = '/test/get_body'; + final Uri $url = Uri.parse('/test/get_body'); final $body = body; final Request $request = Request( 'GET', @@ -210,7 +210,7 @@ class _$HttpTestService extends HttpTestService { @override Future> postTest(String data) { - final String $url = '/test/post'; + final Uri $url = Uri.parse('/test/post'); final $body = data; final Request $request = Request( 'POST', @@ -223,7 +223,7 @@ class _$HttpTestService extends HttpTestService { @override Future> postStreamTest(Stream> byteStream) { - final String $url = '/test/post'; + final Uri $url = Uri.parse('/test/post'); final $body = byteStream; final Request $request = Request( 'POST', @@ -239,7 +239,7 @@ class _$HttpTestService extends HttpTestService { String test, String data, ) { - final String $url = '/test/put/${test}'; + final Uri $url = Uri.parse('/test/put/${test}'); final $body = data; final Request $request = Request( 'PUT', @@ -252,7 +252,7 @@ class _$HttpTestService extends HttpTestService { @override Future> deleteTest(String id) { - final String $url = '/test/delete/${id}'; + final Uri $url = Uri.parse('/test/delete/${id}'); final Map $headers = { 'foo': 'bar', }; @@ -270,7 +270,7 @@ class _$HttpTestService extends HttpTestService { String id, String data, ) { - final String $url = '/test/patch/${id}'; + final Uri $url = Uri.parse('/test/patch/${id}'); final $body = data; final Request $request = Request( 'PATCH', @@ -283,7 +283,7 @@ class _$HttpTestService extends HttpTestService { @override Future> mapTest(Map map) { - final String $url = '/test/map'; + final Uri $url = Uri.parse('/test/map'); final $body = map; final Request $request = Request( 'POST', @@ -296,7 +296,7 @@ class _$HttpTestService extends HttpTestService { @override Future> postForm(Map fields) { - final String $url = '/test/form/body'; + final Uri $url = Uri.parse('/test/form/body'); final $body = fields; final Request $request = Request( 'POST', @@ -312,7 +312,7 @@ class _$HttpTestService extends HttpTestService { @override Future> postFormUsingHeaders(Map fields) { - final String $url = '/test/form/body'; + final Uri $url = Uri.parse('/test/form/body'); final Map $headers = { 'content-type': 'application/x-www-form-urlencoded', }; @@ -332,7 +332,7 @@ class _$HttpTestService extends HttpTestService { String foo, int bar, ) { - final String $url = '/test/form/body/fields'; + final Uri $url = Uri.parse('/test/form/body/fields'); final $body = { 'foo': foo, 'bar': bar, @@ -351,7 +351,7 @@ class _$HttpTestService extends HttpTestService { @override Future> forceJsonTest(Map map) { - final String $url = '/test/map/json'; + final Uri $url = Uri.parse('/test/map/json'); final $body = map; final Request $request = Request( 'POST', @@ -371,7 +371,7 @@ class _$HttpTestService extends HttpTestService { Map a, Map b, ) { - final String $url = '/test/multi'; + final Uri $url = Uri.parse('/test/multi'); final List $parts = [ PartValue>( '1', @@ -394,7 +394,7 @@ class _$HttpTestService extends HttpTestService { @override Future> postFile(List bytes) { - final String $url = '/test/file'; + final Uri $url = Uri.parse('/test/file'); final List $parts = [ PartValueFile>( 'file', @@ -416,7 +416,7 @@ class _$HttpTestService extends HttpTestService { MultipartFile file, { String? id, }) { - final String $url = '/test/file'; + final Uri $url = Uri.parse('/test/file'); final List $parts = [ PartValue( 'id', @@ -439,7 +439,7 @@ class _$HttpTestService extends HttpTestService { @override Future> postListFiles(List files) { - final String $url = '/test/files'; + final Uri $url = Uri.parse('/test/files'); final List $parts = [ PartValueFile>( 'files', @@ -458,7 +458,7 @@ class _$HttpTestService extends HttpTestService { @override Future fullUrl() { - final String $url = 'https://test.com'; + final Uri $url = Uri.parse('https://test.com'); final Request $request = Request( 'GET', $url, @@ -469,7 +469,7 @@ class _$HttpTestService extends HttpTestService { @override Future>> listString() { - final String $url = '/test/list/string'; + final Uri $url = Uri.parse('/test/list/string'); final Request $request = Request( 'GET', $url, @@ -480,7 +480,7 @@ class _$HttpTestService extends HttpTestService { @override Future> noBody() { - final String $url = '/test/no-body'; + final Uri $url = Uri.parse('/test/no-body'); final Request $request = Request( 'POST', $url, @@ -495,7 +495,7 @@ class _$HttpTestService extends HttpTestService { String? bar, String? baz, }) { - final String $url = '/test/query_param_include_null_query_vars'; + final Uri $url = Uri.parse('/test/query_param_include_null_query_vars'); final Map $params = { 'foo': foo, 'bar': bar, @@ -513,7 +513,7 @@ class _$HttpTestService extends HttpTestService { @override Future> getUsingListQueryParam(List value) { - final String $url = '/test/list_query_param'; + final Uri $url = Uri.parse('/test/list_query_param'); final Map $params = {'value': value}; final Request $request = Request( 'GET', @@ -527,7 +527,7 @@ class _$HttpTestService extends HttpTestService { @override Future> getUsingListQueryParamWithBrackets( List value) { - final String $url = '/test/list_query_param_with_brackets'; + final Uri $url = Uri.parse('/test/list_query_param_with_brackets'); final Map $params = {'value': value}; final Request $request = Request( 'GET', @@ -541,7 +541,7 @@ class _$HttpTestService extends HttpTestService { @override Future> getUsingMapQueryParam(Map value) { - final String $url = '/test/map_query_param'; + final Uri $url = Uri.parse('/test/map_query_param'); final Map $params = {'value': value}; final Request $request = Request( 'GET', @@ -555,7 +555,7 @@ class _$HttpTestService extends HttpTestService { @override Future> getUsingMapQueryParamIncludeNullQueryVars( Map value) { - final String $url = '/test/map_query_param_include_null_query_vars'; + final Uri $url = Uri.parse('/test/map_query_param_include_null_query_vars'); final Map $params = {'value': value}; final Request $request = Request( 'GET', @@ -570,7 +570,7 @@ class _$HttpTestService extends HttpTestService { @override Future> getUsingMapQueryParamWithBrackets( Map value) { - final String $url = '/test/map_query_param_with_brackets'; + final Uri $url = Uri.parse('/test/map_query_param_with_brackets'); final Map $params = {'value': value}; final Request $request = Request( 'GET', diff --git a/chopper_built_value/test/converter_test.dart b/chopper_built_value/test/converter_test.dart index f47cd763..084636d5 100644 --- a/chopper_built_value/test/converter_test.dart +++ b/chopper_built_value/test/converter_test.dart @@ -26,9 +26,10 @@ void main() { group('BuiltValueConverter', () { test('convert request', () { - var request = Request.uri( + var request = Request( HttpMethod.Post, Uri.parse('https://foo/'), + Uri.parse(''), body: data, ); request = converter.convertRequest(request); @@ -69,9 +70,10 @@ void main() { }); test('has json headers', () { - var request = Request.uri( + var request = Request( HttpMethod.Get, Uri.parse('https://foo/'), + Uri.parse(''), body: data, ); request = converter.convertRequest(request); diff --git a/chopper_generator/lib/src/generator.dart b/chopper_generator/lib/src/generator.dart index 2ccd6cd3..67fb905f 100644 --- a/chopper_generator/lib/src/generator.dart +++ b/chopper_generator/lib/src/generator.dart @@ -174,7 +174,7 @@ class ChopperGenerator extends GeneratorForAnnotation { ); final List blocks = [ - declareFinal(_urlVar, type: refer('String')).assign(url).statement, + declareFinal(_urlVar, type: refer('Uri')).assign(url).statement, ]; if (queries.isNotEmpty) { @@ -475,21 +475,26 @@ class ChopperGenerator extends GeneratorForAnnotation { if (path.startsWith('http://') || path.startsWith('https://')) { // if the request's url is already a fully qualified URL, we can use // as-is and ignore the baseUrl - return literal(path); + return _generateUri(path); } else if (path.isEmpty && baseUrl.isEmpty) { - return literal(''); + return _generateUri(''); } else { if (path.isNotEmpty && baseUrl.isNotEmpty && !baseUrl.endsWith('/') && !path.startsWith('/')) { - return literal('$baseUrl/$path'); + return _generateUri('$baseUrl/$path'); } - return literal('$baseUrl$path'); + return _generateUri('$baseUrl$path'); } } + Expression _generateUri(String url) => refer('Uri').newInstanceNamed( + 'parse', + [literal(url)], + ); + Expression _generateRequest( ConstantReader method, { bool hasBody = false, @@ -571,7 +576,9 @@ class ChopperGenerator extends GeneratorForAnnotation { ]; list.add( - refer('PartValueFile<${p.type.getDisplayString(withNullability: p.type.isNullable)}>') + refer('PartValueFile<${p.type.getDisplayString( + withNullability: p.type.isNullable, + )}>') .newInstance(params), ); }); diff --git a/example/bin/main_built_value.dart b/example/bin/main_built_value.dart index 9f87becc..7d7fe8cc 100644 --- a/example/bin/main_built_value.dart +++ b/example/bin/main_built_value.dart @@ -27,7 +27,7 @@ final client = MockClient((req) async { main() async { final chopper = ChopperClient( client: client, - baseUrl: 'http://localhost:8000', + baseUrl: Uri.parse('http://localhost:8000'), converter: BuiltValueConverter(), errorConverter: BuiltValueConverter(), services: [ diff --git a/example/bin/main_json_serializable.dart b/example/bin/main_json_serializable.dart index 956014f5..d95b7f24 100644 --- a/example/bin/main_json_serializable.dart +++ b/example/bin/main_json_serializable.dart @@ -24,7 +24,7 @@ main() async { final chopper = ChopperClient( client: client, - baseUrl: 'http://localhost:8000', + baseUrl: Uri.parse('http://localhost:8000'), // bind your object factories here converter: converter, errorConverter: converter, diff --git a/example/bin/main_json_serializable_squadron_worker_pool.dart b/example/bin/main_json_serializable_squadron_worker_pool.dart index 2c3bbb08..e9564899 100644 --- a/example/bin/main_json_serializable_squadron_worker_pool.dart +++ b/example/bin/main_json_serializable_squadron_worker_pool.dart @@ -126,7 +126,7 @@ Future main() async { final chopper = ChopperClient( client: client, - baseUrl: 'http://localhost:8000', + baseUrl: Uri.parse('http://localhost:8000'), // bind your object factories here converter: converter, errorConverter: converter, diff --git a/example/lib/built_value_resource.chopper.dart b/example/lib/built_value_resource.chopper.dart index a68d4b5c..9b83b3fb 100644 --- a/example/lib/built_value_resource.chopper.dart +++ b/example/lib/built_value_resource.chopper.dart @@ -18,7 +18,7 @@ class _$MyService extends MyService { @override Future> getResource(String id) { - final String $url = '/resources/${id}/'; + final Uri $url = Uri.parse('/resources/${id}/'); final Request $request = Request( 'GET', $url, @@ -29,7 +29,7 @@ class _$MyService extends MyService { @override Future>> getBuiltListResources() { - final String $url = '/resources/list'; + final Uri $url = Uri.parse('/resources/list'); final Request $request = Request( 'GET', $url, @@ -40,7 +40,7 @@ class _$MyService extends MyService { @override Future> getTypedResource() { - final String $url = '/resources/'; + final Uri $url = Uri.parse('/resources/'); final Map $headers = { 'foo': 'bar', }; @@ -58,7 +58,7 @@ class _$MyService extends MyService { Resource resource, { String? name, }) { - final String $url = '/resources'; + final Uri $url = Uri.parse('/resources'); final Map $headers = { if (name != null) 'name': name, }; diff --git a/example/lib/json_serializable.chopper.dart b/example/lib/json_serializable.chopper.dart index e9892bfc..7a60b5ab 100644 --- a/example/lib/json_serializable.chopper.dart +++ b/example/lib/json_serializable.chopper.dart @@ -18,7 +18,7 @@ class _$MyService extends MyService { @override Future> getResource(String id) { - final String $url = '/resources/${id}/'; + final Uri $url = Uri.parse('/resources/${id}/'); final Request $request = Request( 'GET', $url, @@ -29,7 +29,7 @@ class _$MyService extends MyService { @override Future>> getResources() { - final String $url = '/resources/all'; + final Uri $url = Uri.parse('/resources/all'); final Map $headers = { 'test': 'list', }; @@ -44,7 +44,7 @@ class _$MyService extends MyService { @override Future>> getMapResource(String id) { - final String $url = '/resources/'; + final Uri $url = Uri.parse('/resources/'); final Map $params = {'id': id}; final Request $request = Request( 'GET', @@ -57,7 +57,7 @@ class _$MyService extends MyService { @override Future> getTypedResource() { - final String $url = '/resources/'; + final Uri $url = Uri.parse('/resources/'); final Map $headers = { 'foo': 'bar', }; @@ -75,7 +75,7 @@ class _$MyService extends MyService { Resource resource, { String? name, }) { - final String $url = '/resources'; + final Uri $url = Uri.parse('/resources'); final Map $headers = { if (name != null) 'name': name, }; diff --git a/faq.md b/faq.md index fca603c6..f5d2b193 100644 --- a/faq.md +++ b/faq.md @@ -71,8 +71,8 @@ You may need to change the base URL of your network calls during runtime, for ex (Request request) async => SharedPreferences.containsKey('baseUrl') ? request.copyWith( - baseUrl: SharedPreferences.getString('baseUrl')) - : request + baseUri: Uri.parse(SharedPreferences.getString('baseUrl')) + ): request ... ``` @@ -109,7 +109,7 @@ abstract class ApiService extends ChopperService { } return http.Response(json.encode(result), 200); }), - baseUrl: 'https://mysite.com/api', + baseUrl: Uri.parse('https://mysite.com/api'), services: [ _$ApiService(), ], @@ -332,7 +332,7 @@ Future main() async { /// Instantiate a ChopperClient final chopper = ChopperClient( client: client, - baseUrl: 'http://localhost:8000', + baseUrl: Uri.parse('http://localhost:8000'), // bind your object factories here converter: converter, errorConverter: converter, From 9d1d86c80063deceb0c3f4e1161e346d3a892eb5 Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Sun, 20 Nov 2022 09:30:21 +0000 Subject: [PATCH 041/171] :memo: Add Authenticator example (#386) --- faq.md | 51 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/faq.md b/faq.md index f5d2b193..a2f31633 100644 --- a/faq.md +++ b/faq.md @@ -170,6 +170,57 @@ interceptors: [ The actual implementation of the algorithm above may vary based on how the backend API - more precisely the login and session handling - of your app looks like. +### Authorized HTTP requests using the special Authenticator interceptor + +Similar to OkHTTP's [authenticator](https://github.com/square/okhttp/blob/480c20e46bb1745e280e42607bbcc73b2c953d97/okhttp/src/main/kotlin/okhttp3/Authenticator.kt), +the idea here is to provide a reactive authentication in the event that an auth challenge is raised. It returns a +nullable Request that contains a possible update to the original Request to satisfy the authentication challenge. + +```dart +import 'dart:async' show FutureOr; +import 'dart:io' show HttpHeaders, HttpStatus; + +import 'package:chopper/chopper.dart'; + +/// This method returns a [Request] that includes credentials to satisfy an authentication challenge received in +/// [response]. It returns `null` if the challenge cannot be satisfied. +class MyAuthenticator extends Authenticator { + @override + FutureOr authenticate( + Request request, + Response response, [ + Request? originalRequest, + ]) async { + if (response.statusCode == HttpStatus.unauthorized) { + final String? newToken = await refreshToken(); + + if (newToken != null) { + return request.copyWith(headers: { + ...request.headers, + HttpHeaders.authorizationHeader: newToken, + }); + } + } + + return null; + } + + Future refreshToken() async { + /// Refresh the accessToken using refreshToken however needed. + /// This could be done either via an HTTP client, or a ChopperService, or a + /// repository could be a dependency. + /// This approach is intentionally not opinionated about how this works. + throw UnimplementedError(); + } +} + +/// When initializing your ChopperClient +final client = ChopperClient( + /// register your Authenticator here + authenticator: MyAuthenticator(), +); +``` + ## Decoding JSON using Isolates Sometimes you want to decode JSON outside the main thread in order to reduce janking. In this example we're going to go From 2775a735aea6fe4fb77e143f374359a0ada65f79 Mon Sep 17 00:00:00 2001 From: Erlang Parasu Date: Sun, 11 Dec 2022 19:17:35 +0800 Subject: [PATCH 042/171] Update getting-started.md (#391) --- getting-started.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/getting-started.md b/getting-started.md index f7107ffa..fe9cbd01 100644 --- a/getting-started.md +++ b/getting-started.md @@ -41,7 +41,7 @@ part "YOUR_FILE.chopper.dart"; abstract class TodosListService extends ChopperService { // A helper method that helps instantiating the service. You can omit this method and use the generated class directly instead. - static TodosListService create([ChopperClient client]) => + static TodosListService create([ChopperClient? client]) => _$TodosListService(client); } ``` From 11a9ca50f55bebad7a7794c0a45bbde898aa1080 Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Sun, 11 Dec 2022 12:06:02 +0000 Subject: [PATCH 043/171] Fix #388 Triggering Authenticator double encodes body in POST, PUT, PATCH (#390) fixes https://github.com/lejard-h/chopper/issues/388 --- chopper/lib/src/base.dart | 9 +- chopper/test/authenticator_test.dart | 392 +++++++++++++++++++++++++++ chopper/test/fake_authenticator.dart | 23 ++ 3 files changed, 422 insertions(+), 2 deletions(-) create mode 100644 chopper/test/authenticator_test.dart create mode 100644 chopper/test/fake_authenticator.dart diff --git a/chopper/lib/src/base.dart b/chopper/lib/src/base.dart index 624607b3..4fa556c2 100644 --- a/chopper/lib/src/base.dart +++ b/chopper/lib/src/base.dart @@ -293,9 +293,10 @@ class ChopperClient { ConvertRequest? requestConverter, ConvertResponse? responseConverter, }) async { - var req = await _interceptRequest( + final Request req = await _interceptRequest( await _handleRequestConverter(request, requestConverter), ); + _requestController.add(req); final streamRes = await httpClient.send(await req.toBaseRequest()); @@ -307,7 +308,11 @@ class ChopperClient { dynamic res = Response(response, response.body); if (authenticator != null) { - var updatedRequest = await authenticator!.authenticate(req, res, request); + final Request? updatedRequest = await authenticator!.authenticate( + request, + res, + request, + ); if (updatedRequest != null) { res = await send( diff --git a/chopper/test/authenticator_test.dart b/chopper/test/authenticator_test.dart new file mode 100644 index 00000000..8f4ebb20 --- /dev/null +++ b/chopper/test/authenticator_test.dart @@ -0,0 +1,392 @@ +import 'dart:convert' show jsonEncode; + +import 'package:chopper/chopper.dart'; +import 'package:http/http.dart' as http; +import 'package:http/testing.dart'; +import 'package:test/test.dart'; + +import 'fake_authenticator.dart'; + +void main() async { + final Uri baseUrl = Uri.parse('http://localhost:8000'); + + ChopperClient buildClient([http.Client? httpClient]) => ChopperClient( + baseUrl: baseUrl, + client: httpClient, + interceptors: [ + (Request req) => applyHeader(req, 'foo', 'bar'), + ], + converter: JsonConverter(), + authenticator: FakeAuthenticator(), + ); + + late bool authenticated; + final Map tested = { + 'unauthenticated': false, + 'authenticated': false, + }; + + setUp(() { + authenticated = false; + tested['unauthenticated'] = false; + tested['authenticated'] = false; + }); + + group('GET', () { + test('authorized', () async { + final httpClient = MockClient((request) async { + expect( + request.url.toString(), + equals('$baseUrl/test/get?key=val'), + ); + expect(request.method, equals('GET')); + expect(request.headers['foo'], equals('bar')); + expect(request.headers['int'], equals('42')); + + return http.Response('ok', 200); + }); + + final chopper = buildClient(httpClient); + final response = await chopper.get( + Uri( + path: '/test/get', + queryParameters: {'key': 'val'}, + ), + headers: {'int': '42'}, + ); + + expect(response.body, equals('ok')); + expect(response.statusCode, equals(200)); + + httpClient.close(); + }); + + test('unauthorized', () async { + final httpClient = MockClient((request) async { + expect( + request.url.toString(), + equals('$baseUrl/test/get?key=val'), + ); + expect(request.method, equals('GET')); + expect(request.headers['foo'], equals('bar')); + expect(request.headers['int'], equals('42')); + + if (!authenticated) { + tested['unauthenticated'] = true; + authenticated = true; + + return http.Response('unauthorized', 401); + } else { + tested['authenticated'] = true; + expect(request.headers['authorization'], equals('some_fake_token')); + } + + return http.Response('ok', 200); + }); + + final chopper = buildClient(httpClient); + final response = await chopper.get( + Uri( + path: '/test/get', + queryParameters: {'key': 'val'}, + ), + headers: {'int': '42'}, + ); + + expect(response.body, equals('ok')); + expect(response.statusCode, equals(200)); + expect(tested['authenticated'], equals(true)); + expect(tested['unauthenticated'], equals(true)); + + httpClient.close(); + }); + }); + + group('POST', () { + test('authorized', () async { + final httpClient = MockClient((request) async { + expect( + request.url.toString(), + equals('$baseUrl/test/post?key=val'), + ); + expect(request.method, equals('POST')); + expect(request.headers['foo'], equals('bar')); + expect(request.headers['int'], equals('42')); + expect( + request.body, + jsonEncode( + { + 'name': 'john', + 'surname': 'doe', + }, + ), + ); + + return http.Response('ok', 200); + }); + + final chopper = buildClient(httpClient); + final response = await chopper.post( + Uri( + path: '/test/post', + queryParameters: {'key': 'val'}, + ), + headers: {'int': '42'}, + body: { + 'name': 'john', + 'surname': 'doe', + }, + ); + + expect(response.body, equals('ok')); + expect(response.statusCode, equals(200)); + + httpClient.close(); + }); + + test('unauthorized', () async { + final httpClient = MockClient((request) async { + expect( + request.url.toString(), + equals('$baseUrl/test/post?key=val'), + ); + expect(request.method, equals('POST')); + expect(request.headers['foo'], equals('bar')); + expect(request.headers['int'], equals('42')); + expect( + request.body, + jsonEncode( + { + 'name': 'john', + 'surname': 'doe', + }, + ), + ); + + if (!authenticated) { + tested['unauthenticated'] = true; + authenticated = true; + + return http.Response('unauthorized', 401); + } else { + tested['authenticated'] = true; + expect(request.headers['authorization'], equals('some_fake_token')); + } + + return http.Response('ok', 200); + }); + + final chopper = buildClient(httpClient); + final response = await chopper.post( + Uri( + path: '/test/post', + queryParameters: {'key': 'val'}, + ), + headers: {'int': '42'}, + body: { + 'name': 'john', + 'surname': 'doe', + }, + ); + + expect(response.body, equals('ok')); + expect(response.statusCode, equals(200)); + expect(tested['authenticated'], equals(true)); + expect(tested['unauthenticated'], equals(true)); + + httpClient.close(); + }); + }); + + group('PUT', () { + test('authorized', () async { + final httpClient = MockClient((request) async { + expect( + request.url.toString(), + equals('$baseUrl/test/put?key=val'), + ); + expect(request.method, equals('PUT')); + expect(request.headers['foo'], equals('bar')); + expect(request.headers['int'], equals('42')); + expect( + request.body, + jsonEncode( + { + 'name': 'john', + 'surname': 'doe', + }, + ), + ); + + return http.Response('ok', 200); + }); + + final chopper = buildClient(httpClient); + final response = await chopper.put( + Uri( + path: '/test/put', + queryParameters: {'key': 'val'}, + ), + headers: {'int': '42'}, + body: { + 'name': 'john', + 'surname': 'doe', + }, + ); + + expect(response.body, equals('ok')); + expect(response.statusCode, equals(200)); + + httpClient.close(); + }); + + test('unauthorized', () async { + final httpClient = MockClient((request) async { + expect( + request.url.toString(), + equals('$baseUrl/test/put?key=val'), + ); + expect(request.method, equals('PUT')); + expect(request.headers['foo'], equals('bar')); + expect(request.headers['int'], equals('42')); + expect( + request.body, + jsonEncode( + { + 'name': 'john', + 'surname': 'doe', + }, + ), + ); + + if (!authenticated) { + tested['unauthenticated'] = true; + authenticated = true; + + return http.Response('unauthorized', 401); + } else { + tested['authenticated'] = true; + expect(request.headers['authorization'], equals('some_fake_token')); + } + + return http.Response('ok', 200); + }); + + final chopper = buildClient(httpClient); + final response = await chopper.put( + Uri( + path: '/test/put', + queryParameters: {'key': 'val'}, + ), + headers: {'int': '42'}, + body: { + 'name': 'john', + 'surname': 'doe', + }, + ); + + expect(response.body, equals('ok')); + expect(response.statusCode, equals(200)); + expect(tested['authenticated'], equals(true)); + expect(tested['unauthenticated'], equals(true)); + + httpClient.close(); + }); + }); + + group('PATCH', () { + test('authorized', () async { + final httpClient = MockClient((request) async { + expect( + request.url.toString(), + equals('$baseUrl/test/patch?key=val'), + ); + expect(request.method, equals('PATCH')); + expect(request.headers['foo'], equals('bar')); + expect(request.headers['int'], equals('42')); + expect( + request.body, + jsonEncode( + { + 'name': 'john', + 'surname': 'doe', + }, + ), + ); + + return http.Response('ok', 200); + }); + + final chopper = buildClient(httpClient); + final response = await chopper.patch( + Uri( + path: '/test/patch', + queryParameters: {'key': 'val'}, + ), + headers: {'int': '42'}, + body: { + 'name': 'john', + 'surname': 'doe', + }, + ); + + expect(response.body, equals('ok')); + expect(response.statusCode, equals(200)); + + httpClient.close(); + }); + + test('unauthorized', () async { + final httpClient = MockClient((request) async { + expect( + request.url.toString(), + equals('$baseUrl/test/patch?key=val'), + ); + expect(request.method, equals('PATCH')); + expect(request.headers['foo'], equals('bar')); + expect(request.headers['int'], equals('42')); + expect( + request.body, + jsonEncode( + { + 'name': 'john', + 'surname': 'doe', + }, + ), + ); + + if (!authenticated) { + tested['unauthenticated'] = true; + authenticated = true; + + return http.Response('unauthorized', 401); + } else { + tested['authenticated'] = true; + expect(request.headers['authorization'], equals('some_fake_token')); + } + + return http.Response('ok', 200); + }); + + final chopper = buildClient(httpClient); + final response = await chopper.patch( + Uri( + path: '/test/patch', + queryParameters: {'key': 'val'}, + ), + headers: {'int': '42'}, + body: { + 'name': 'john', + 'surname': 'doe', + }, + ); + + expect(response.body, equals('ok')); + expect(response.statusCode, equals(200)); + expect(tested['authenticated'], equals(true)); + expect(tested['unauthenticated'], equals(true)); + + httpClient.close(); + }); + }); +} diff --git a/chopper/test/fake_authenticator.dart b/chopper/test/fake_authenticator.dart new file mode 100644 index 00000000..22de9356 --- /dev/null +++ b/chopper/test/fake_authenticator.dart @@ -0,0 +1,23 @@ +import 'dart:async' show FutureOr; + +import 'package:chopper/chopper.dart'; + +class FakeAuthenticator extends Authenticator { + @override + FutureOr authenticate( + Request request, + Response response, [ + Request? originalRequest, + ]) async { + if (response.statusCode == 401) { + return request.copyWith( + headers: { + ...request.headers, + 'authorization': 'some_fake_token', + }, + ); + } + + return null; + } +} From bdbe2102f5ef8fbe0a26097d2fd3977e34559be1 Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Sun, 11 Dec 2022 12:56:49 +0000 Subject: [PATCH 044/171] Add image upload test (#392) --- chopper/pubspec.yaml | 1 + chopper/test/multipart_test.dart | 24 ++++++++++++++++++++++++ chopper/test/test_service.chopper.dart | 19 +++++++++++++++++++ chopper/test/test_service.dart | 6 ++++++ 4 files changed, 50 insertions(+) diff --git a/chopper/pubspec.yaml b/chopper/pubspec.yaml index 51d7abcd..4578345c 100644 --- a/chopper/pubspec.yaml +++ b/chopper/pubspec.yaml @@ -21,5 +21,6 @@ dev_dependencies: http_parser: ^4.0.0 lints: ^2.0.0 test: ^1.16.4 + transparent_image: ^2.0.0 chopper_generator: path: ../chopper_generator diff --git a/chopper/test/multipart_test.dart b/chopper/test/multipart_test.dart index 9bffea50..ebb8af90 100644 --- a/chopper/test/multipart_test.dart +++ b/chopper/test/multipart_test.dart @@ -3,6 +3,7 @@ import 'package:http/http.dart' as http; import 'package:http/testing.dart'; import 'package:http_parser/http_parser.dart'; import 'package:test/test.dart'; +import 'package:transparent_image/transparent_image.dart'; import 'test_service.dart'; @@ -66,6 +67,29 @@ void main() { chopper.dispose(); }); + + test('image', () async { + final httpClient = MockClient((http.Request req) async { + final String body = String.fromCharCodes(req.bodyBytes); + + expect(req.headers['Content-Type'], contains('multipart/form-data;')); + expect(body, contains('content-type: application/octet-stream')); + expect(body, contains('content-disposition: form-data; name="image"')); + expect( + body, + contains(String.fromCharCodes(kTransparentImage)), + ); + + return http.Response('ok', 200); + }); + + final chopper = ChopperClient(client: httpClient); + final service = HttpTestService.create(chopper); + + await service.postImage(kTransparentImage); + + chopper.dispose(); + }); }); test('file with MultipartFile', () async { diff --git a/chopper/test/test_service.chopper.dart b/chopper/test/test_service.chopper.dart index 97ce5db3..f93398a5 100644 --- a/chopper/test/test_service.chopper.dart +++ b/chopper/test/test_service.chopper.dart @@ -411,6 +411,25 @@ class _$HttpTestService extends HttpTestService { return client.send($request); } + @override + Future> postImage(List imageData) { + final Uri $url = Uri.parse('/test/image'); + final List $parts = [ + PartValueFile>( + 'image', + imageData, + ) + ]; + final Request $request = Request( + 'POST', + $url, + client.baseUrl, + parts: $parts, + multipart: true, + ); + return client.send($request); + } + @override Future> postMultipartFile( MultipartFile file, { diff --git a/chopper/test/test_service.dart b/chopper/test/test_service.dart index 7d19418c..6257f43a 100644 --- a/chopper/test/test_service.dart +++ b/chopper/test/test_service.dart @@ -119,6 +119,12 @@ abstract class HttpTestService extends ChopperService { @PartFile('file') List bytes, ); + @Post(path: 'image') + @multipart + Future postImage( + @PartFile('image') List imageData, + ); + @Post(path: 'file') @multipart Future postMultipartFile( From b6ea960a523cf2599f13c940fc3e9bf98ec4661d Mon Sep 17 00:00:00 2001 From: Ivan Terekhin Date: Tue, 13 Dec 2022 16:09:53 +0300 Subject: [PATCH 045/171] Release 5.2.0 (#394) Co-authored-by: Ivan Terekhin <231950+JEuler@users.noreply.github.com> --- .gitignore | 3 ++- chopper/CHANGELOG.md | 4 ++++ chopper/pubspec.yaml | 2 +- chopper_generator/CHANGELOG.md | 3 +++ chopper_generator/pubspec.yaml | 2 +- example/pubspec.yaml | 2 +- 6 files changed, 12 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index 08fe361f..e752ffab 100644 --- a/.gitignore +++ b/.gitignore @@ -13,4 +13,5 @@ doc/api/ coverage chopper/doc/ .vscode/ -pubspec.temp.yaml \ No newline at end of file +pubspec.temp.yaml +.DS_Store diff --git a/chopper/CHANGELOG.md b/chopper/CHANGELOG.md index 56519d03..ff75d44a 100644 --- a/chopper/CHANGELOG.md +++ b/chopper/CHANGELOG.md @@ -1,4 +1,8 @@ # Changelog +## 5.2.0 + +- Replaced the String based path with Uri +- Fix for Authenticator body rewrite ## 5.1.0 diff --git a/chopper/pubspec.yaml b/chopper/pubspec.yaml index 4578345c..e9ffa8cf 100644 --- a/chopper/pubspec.yaml +++ b/chopper/pubspec.yaml @@ -1,6 +1,6 @@ name: chopper description: Chopper is an http client generator using source_gen, inspired by Retrofit -version: 5.1.0 +version: 5.2.0 documentation: https://hadrien-lejard.gitbook.io/chopper repository: https://github.com/lejard-h/chopper diff --git a/chopper_generator/CHANGELOG.md b/chopper_generator/CHANGELOG.md index 185ca6c8..4bdfebed 100644 --- a/chopper_generator/CHANGELOG.md +++ b/chopper_generator/CHANGELOG.md @@ -1,4 +1,7 @@ # Changelog +## 5.2.0 + +- Replaced the String based path with Uri ## 5.1.0 diff --git a/chopper_generator/pubspec.yaml b/chopper_generator/pubspec.yaml index a0fe86dc..0e34c7ec 100644 --- a/chopper_generator/pubspec.yaml +++ b/chopper_generator/pubspec.yaml @@ -1,6 +1,6 @@ name: chopper_generator description: Chopper is an http client generator using source_gen, inspired by Retrofit -version: 5.1.0 +version: 5.2.0 documentation: https://hadrien-lejard.gitbook.io/chopper repository: https://github.com/lejard-h/chopper diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 65be75a6..47051983 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -1,6 +1,6 @@ name: chopper_example description: Example usage of the Chopper package -version: 0.0.2 +version: 0.0.3 documentation: https://hadrien-lejard.gitbook.io/chopper/ #author: Hadrien Lejard From 1719c833b77df8cd9a1d0157f6cc27777800adb8 Mon Sep 17 00:00:00 2001 From: Ivan Terekhin Date: Wed, 14 Dec 2022 11:15:47 +0300 Subject: [PATCH 046/171] 6.0.0 (#397) --- chopper/CHANGELOG.md | 7 ++++++- chopper/pubspec.yaml | 2 +- chopper_generator/CHANGELOG.md | 7 ++++++- chopper_generator/pubspec.yaml | 4 ++-- 4 files changed, 15 insertions(+), 5 deletions(-) diff --git a/chopper/CHANGELOG.md b/chopper/CHANGELOG.md index ff75d44a..32f0af43 100644 --- a/chopper/CHANGELOG.md +++ b/chopper/CHANGELOG.md @@ -1,7 +1,12 @@ # Changelog +## 6.0.0 + +- Replaced the String based path with Uri (BREAKING CHANGE) +- Fix for Authenticator body rewrite + ## 5.2.0 -- Replaced the String based path with Uri +- Replaced the String based path with Uri (BREAKING CHANGE) - Fix for Authenticator body rewrite ## 5.1.0 diff --git a/chopper/pubspec.yaml b/chopper/pubspec.yaml index e9ffa8cf..ad1ac78f 100644 --- a/chopper/pubspec.yaml +++ b/chopper/pubspec.yaml @@ -1,6 +1,6 @@ name: chopper description: Chopper is an http client generator using source_gen, inspired by Retrofit -version: 5.2.0 +version: 6.0.0 documentation: https://hadrien-lejard.gitbook.io/chopper repository: https://github.com/lejard-h/chopper diff --git a/chopper_generator/CHANGELOG.md b/chopper_generator/CHANGELOG.md index 4bdfebed..39fc57cd 100644 --- a/chopper_generator/CHANGELOG.md +++ b/chopper_generator/CHANGELOG.md @@ -1,7 +1,12 @@ # Changelog + +## 6.0.0 + +- Replaced the String based path with Uri (BREAKING CHANGE) + ## 5.2.0 -- Replaced the String based path with Uri +- Replaced the String based path with Uri (BREAKING CHANGE) ## 5.1.0 diff --git a/chopper_generator/pubspec.yaml b/chopper_generator/pubspec.yaml index 0e34c7ec..c4ed6cfe 100644 --- a/chopper_generator/pubspec.yaml +++ b/chopper_generator/pubspec.yaml @@ -1,6 +1,6 @@ name: chopper_generator description: Chopper is an http client generator using source_gen, inspired by Retrofit -version: 5.2.0 +version: 6.0.0 documentation: https://hadrien-lejard.gitbook.io/chopper repository: https://github.com/lejard-h/chopper @@ -11,7 +11,7 @@ dependencies: analyzer: '>=4.4.0 <6.0.0' build: ^2.0.0 built_collection: ^5.0.0 - chopper: ^5.0.0 + chopper: ^6.0.0 code_builder: ^4.3.0 dart_style: ^2.0.0 logging: ^1.0.0 From 7453582a459b77682b3b3a11f17d9473e3951a1e Mon Sep 17 00:00:00 2001 From: Job Guldemeester Date: Fri, 6 Jan 2023 16:25:10 +0100 Subject: [PATCH 047/171] Configurable HttpLoggingInterceptor (#399) * Extracted the http logging interceptor Make http logging interceptor configurable (none, basic, headers, body) * Minor tweaks Updated documentation * Simplified onRequest body and startMessage logic. Made onResponse more consistent with onRequest. --- chopper/lib/chopper.dart | 1 + chopper/lib/src/http_logging_interceptor.dart | 173 ++++++++++ chopper/lib/src/interceptor.dart | 52 --- .../test/http_logging_interceptor_test.dart | 302 ++++++++++++++++++ chopper/test/interceptors_test.dart | 51 --- interceptors.md | 11 + 6 files changed, 487 insertions(+), 103 deletions(-) create mode 100644 chopper/lib/src/http_logging_interceptor.dart create mode 100644 chopper/test/http_logging_interceptor_test.dart diff --git a/chopper/lib/chopper.dart b/chopper/lib/chopper.dart index e7230361..2290febd 100644 --- a/chopper/lib/chopper.dart +++ b/chopper/lib/chopper.dart @@ -9,6 +9,7 @@ export 'src/base.dart'; export 'src/constants.dart'; export 'src/extensions.dart'; export 'src/interceptor.dart'; +export 'src/http_logging_interceptor.dart'; export 'src/request.dart'; export 'src/response.dart'; export 'src/utils.dart' hide mapToQuery; diff --git a/chopper/lib/src/http_logging_interceptor.dart b/chopper/lib/src/http_logging_interceptor.dart new file mode 100644 index 00000000..0be43965 --- /dev/null +++ b/chopper/lib/src/http_logging_interceptor.dart @@ -0,0 +1,173 @@ +import 'dart:async'; + +import 'package:chopper/src/interceptor.dart'; +import 'package:chopper/src/request.dart'; +import 'package:chopper/src/response.dart'; +import 'package:chopper/src/utils.dart'; +import 'package:http/http.dart' as http; +import 'package:meta/meta.dart'; + +enum Level { + /// No logs. + none, + + /// Logs request and response lines. + /// + /// Example: + /// ``` + /// --> POST https://foo.bar/greeting (3-byte body) + /// + /// <-- 200 OK POST https://foo.bar/greeting (6-byte body) + /// ``` + basic, + + /// Logs request and response lines and their respective headers. + /// + /// Example: + /// ``` + /// --> POST https://foo.bar/greeting + /// content-type: plain/text + /// content-length: 3 + /// --> END POST + /// + /// <-- 200 OK POST https://foo.bar/greeting + /// content-type: plain/text + /// content-length: 6 + /// <-- END HTTP + /// ``` + headers, + + /// Logs request and response lines and their respective headers and bodies (if present). + /// + /// Example: + /// ``` + /// --> POST https://foo.bar/greeting + /// content-type: plain/text + /// content-length: 3 + /// + /// Hi? + /// --> END POST https://foo.bar/greeting + /// + /// <-- 200 OK POST https://foo.bar/greeting + /// content-type: plain/text + /// content-length: 6 + /// + /// Hello! + /// <-- END HTTP + /// ``` + body, +} + +/// A [RequestInterceptor] and [ResponseInterceptor] implementation which logs +/// HTTP request and response data. +/// +/// Log levels can be set by applying [level] for more fine grained control +/// over amount of information being logged. +/// +/// **Warning:** Log messages written by this interceptor have the potential to +/// leak sensitive information, such as `Authorization` headers and user data +/// in response bodies. This interceptor should only be used in a controlled way +/// or in a non-production environment. +@immutable +class HttpLoggingInterceptor + implements RequestInterceptor, ResponseInterceptor { + const HttpLoggingInterceptor({this.level = Level.body}) + : _logBody = level == Level.body, + _logHeaders = level == Level.body || level == Level.headers; + + final Level level; + final bool _logBody; + final bool _logHeaders; + + @override + FutureOr onRequest(Request request) async { + if (level == Level.none) return request; + final http.BaseRequest base = await request.toBaseRequest(); + + String startRequestMessage = '--> ${base.method} ${base.url.toString()}'; + String bodyMessage = ''; + if (base is http.Request) { + if (base.body.isNotEmpty) { + bodyMessage = base.body; + + if (!_logHeaders) { + startRequestMessage += ' (${base.bodyBytes.length}-byte body)'; + } + } + } + + // Always start on a new line + chopperLogger.info(''); + chopperLogger.info(startRequestMessage); + + if (_logHeaders) { + base.headers.forEach((k, v) => chopperLogger.info('$k: $v')); + + if (base.contentLength != null && + base.headers['content-length'] == null) { + chopperLogger.info('content-length: ${base.contentLength}'); + } + } + + if (_logBody && bodyMessage.isNotEmpty) { + chopperLogger.info(''); + chopperLogger.info(bodyMessage); + } + + if (_logHeaders || _logBody) { + chopperLogger.info('--> END ${base.method}'); + } + + return request; + } + + @override + FutureOr onResponse(Response response) { + if (level == Level.none) return response; + final base = response.base; + + String bytes = ''; + String reasonPhrase = response.statusCode.toString(); + String bodyMessage = ''; + if (base is http.Response) { + if (base.reasonPhrase != null) { + reasonPhrase += + ' ${base.reasonPhrase != reasonPhrase ? base.reasonPhrase : ''}'; + } + + if (base.body.isNotEmpty) { + bodyMessage = base.body; + + if (!_logBody && !_logHeaders) { + bytes = ' (${response.bodyBytes.length}-byte body)'; + } + } + } + + // Always start on a new line + chopperLogger.info(''); + chopperLogger.info( + '<-- $reasonPhrase ${base.request?.method} ${base.request?.url.toString()}$bytes', + ); + + if (_logHeaders) { + base.headers.forEach((k, v) => chopperLogger.info('$k: $v')); + + if (base.contentLength != null && + base.headers['content-length'] == null) { + chopperLogger.info('content-length: ${base.contentLength}'); + } + } + + if (_logBody && bodyMessage.isNotEmpty) { + chopperLogger.info(''); + chopperLogger.info(bodyMessage); + } + + if (_logBody || _logHeaders) { + chopperLogger.info('<-- END HTTP'); + } + + return response; + } +} diff --git a/chopper/lib/src/interceptor.dart b/chopper/lib/src/interceptor.dart index e5f7bbe0..0e123237 100644 --- a/chopper/lib/src/interceptor.dart +++ b/chopper/lib/src/interceptor.dart @@ -158,58 +158,6 @@ class CurlInterceptor implements RequestInterceptor { } } -/// A [RequestInterceptor] and [ResponseInterceptor] implementation which logs -/// HTTP request and response data. -/// -/// **Warning:** Log messages written by this interceptor have the potential to -/// leak sensitive information, such as `Authorization` headers and user data -/// in response bodies. This interceptor should only be used in a controlled way -/// or in a non-production environment. -@immutable -class HttpLoggingInterceptor - implements RequestInterceptor, ResponseInterceptor { - @override - FutureOr onRequest(Request request) async { - final http.BaseRequest base = await request.toBaseRequest(); - chopperLogger.info('--> ${base.method} ${base.url.toString()}'); - base.headers.forEach((k, v) => chopperLogger.info('$k: $v')); - - String bytes = ''; - if (base is http.Request) { - final body = base.body; - if (body.isNotEmpty) { - chopperLogger.info(body); - bytes = ' (${base.bodyBytes.length}-byte body)'; - } - } - - chopperLogger.info('--> END ${base.method}$bytes'); - - return request; - } - - @override - FutureOr onResponse(Response response) { - final http.BaseRequest? base = response.base.request; - chopperLogger.info('<-- ${response.statusCode} ${base!.url.toString()}'); - - response.base.headers.forEach((k, v) => chopperLogger.info('$k: $v')); - - String bytes = ''; - if (response.base is http.Response) { - final resp = response.base as http.Response; - if (resp.body.isNotEmpty) { - chopperLogger.info(resp.body); - bytes = ' (${response.bodyBytes.length}-byte body)'; - } - } - - chopperLogger.info('--> END ${base.method}$bytes'); - - return response; - } -} - /// A [Converter] implementation that calls [json.encode] on [Request]s and /// [json.decode] on [Response]s using the [dart:convert](https://api.dart.dev/stable/2.10.3/dart-convert/dart-convert-library.html) /// package's [utf8] and [json] utilities. diff --git a/chopper/test/http_logging_interceptor_test.dart b/chopper/test/http_logging_interceptor_test.dart new file mode 100644 index 00000000..9a2d49f4 --- /dev/null +++ b/chopper/test/http_logging_interceptor_test.dart @@ -0,0 +1,302 @@ +import 'package:chopper/src/http_logging_interceptor.dart'; +import 'package:chopper/src/request.dart'; +import 'package:chopper/src/response.dart'; +import 'package:chopper/src/utils.dart'; +import 'package:http/http.dart' as http; +import 'package:test/test.dart'; + +void main() { + final fakeRequest = Request( + 'POST', + Uri.parse('/'), + Uri.parse('base'), + body: 'test', + headers: {'foo': 'bar'}, + ); + + group('http logging requests', () { + test('Http logger interceptor none level request', () async { + final logger = HttpLoggingInterceptor(level: Level.none); + + final logs = []; + chopperLogger.onRecord.listen((r) => logs.add(r.message)); + await logger.onRequest(fakeRequest); + + expect( + logs, + equals( + [], + ), + ); + }); + + test('Http logger interceptor basic level request', () async { + final logger = HttpLoggingInterceptor(level: Level.basic); + + final logs = []; + chopperLogger.onRecord.listen((r) => logs.add(r.message)); + await logger.onRequest(fakeRequest); + + expect( + logs, + equals( + [ + '', + '--> POST base/ (4-byte body)', + ], + ), + ); + }); + + test('Http logger interceptor basic level request', () async { + final logger = HttpLoggingInterceptor(level: Level.headers); + + final logs = []; + chopperLogger.onRecord.listen((r) => logs.add(r.message)); + await logger.onRequest(fakeRequest); + + expect( + logs, + equals( + [ + '', + '--> POST base/', + 'foo: bar', + 'content-type: text/plain; charset=utf-8', + 'content-length: 4', + '--> END POST', + ], + ), + ); + }); + + test('Http logger interceptor body level request', () async { + final logger = HttpLoggingInterceptor(level: Level.body); + + final logs = []; + chopperLogger.onRecord.listen((r) => logs.add(r.message)); + await logger.onRequest(fakeRequest); + + expect( + logs, + equals( + [ + '', + '--> POST base/', + 'foo: bar', + 'content-type: text/plain; charset=utf-8', + 'content-length: 4', + '', + 'test', + '--> END POST', + ], + ), + ); + }); + }); + + group('http logging interceptor response logging', () { + late Response fakeResponse; + + setUp(() async { + fakeResponse = Response( + http.Response( + 'responseBodyBase', + 200, + headers: {'foo': 'bar'}, + request: await fakeRequest.toBaseRequest(), + ), + 'responseBody', + ); + }); + + test('Http logger interceptor none level response', () async { + final logger = HttpLoggingInterceptor(level: Level.none); + + final logs = []; + chopperLogger.onRecord.listen((r) => logs.add(r.message)); + await logger.onResponse(fakeResponse); + + expect( + logs, + equals( + [], + ), + ); + }); + + test('Http logger interceptor basic level response', () async { + final logger = HttpLoggingInterceptor(level: Level.basic); + + final logs = []; + chopperLogger.onRecord.listen((r) => logs.add(r.message)); + await logger.onResponse(fakeResponse); + + expect( + logs, + equals( + [ + '', + '<-- 200 POST base/ (16-byte body)', + ], + ), + ); + }); + + test('Http logger interceptor headers level response', () async { + final logger = HttpLoggingInterceptor(level: Level.headers); + + final logs = []; + chopperLogger.onRecord.listen((r) => logs.add(r.message)); + await logger.onResponse(fakeResponse); + + expect( + logs, + equals( + [ + '', + '<-- 200 POST base/', + 'foo: bar', + 'content-length: 16', + '<-- END HTTP', + ], + ), + ); + }); + + test('Http logger interceptor body level response', () async { + final logger = HttpLoggingInterceptor(level: Level.body); + + final logs = []; + chopperLogger.onRecord.listen((r) => logs.add(r.message)); + await logger.onResponse(fakeResponse); + + expect( + logs, + equals( + [ + '', + '<-- 200 POST base/', + 'foo: bar', + 'content-length: 16', + '', + 'responseBodyBase', + '<-- END HTTP', + ], + ), + ); + }); + }); + + group('headers content-length not overridden', () { + late Response fakeResponse; + + setUp(() async { + fakeResponse = Response( + http.Response( + 'responseBodyBase', + 200, + headers: { + 'foo': 'bar', + 'content-length': '42', + }, + request: await fakeRequest.toBaseRequest(), + ), + 'responseBody', + ); + }); + + test('request header level content-length', () async { + final logger = HttpLoggingInterceptor(level: Level.headers); + + final logs = []; + chopperLogger.onRecord.listen((r) => logs.add(r.message)); + + await logger.onRequest(fakeRequest + .copyWith(headers: {...fakeRequest.headers, 'content-length': '42'})); + + expect( + logs, + equals( + [ + '', + '--> POST base/', + 'foo: bar', + 'content-length: 42', + 'content-type: text/plain; charset=utf-8', + '--> END POST', + ], + ), + ); + }); + + test('request body level content-length', () async { + final logger = HttpLoggingInterceptor(level: Level.body); + + final logs = []; + chopperLogger.onRecord.listen((r) => logs.add(r.message)); + + await logger.onRequest(fakeRequest + .copyWith(headers: {...fakeRequest.headers, 'content-length': '42'})); + + expect( + logs, + equals( + [ + '', + '--> POST base/', + 'foo: bar', + 'content-length: 42', + 'content-type: text/plain; charset=utf-8', + '', + 'test', + '--> END POST', + ], + ), + ); + }); + + test('response header level content-length', () async { + final logger = HttpLoggingInterceptor(level: Level.headers); + + final logs = []; + chopperLogger.onRecord.listen((r) => logs.add(r.message)); + await logger.onResponse(fakeResponse); + + expect( + logs, + equals( + [ + '', + '<-- 200 POST base/', + 'foo: bar', + 'content-length: 42', + '<-- END HTTP', + ], + ), + ); + }); + test('response body level content-length', () async { + final logger = HttpLoggingInterceptor(level: Level.body); + + final logs = []; + chopperLogger.onRecord.listen((r) => logs.add(r.message)); + await logger.onResponse(fakeResponse); + + expect( + logs, + equals( + [ + '', + '<-- 200 POST base/', + 'foo: bar', + 'content-length: 42', + '', + 'responseBodyBase', + '<-- END HTTP', + ], + ), + ); + }); + }); +} diff --git a/chopper/test/interceptors_test.dart b/chopper/test/interceptors_test.dart index bffcd040..0584dcc5 100644 --- a/chopper/test/interceptors_test.dart +++ b/chopper/test/interceptors_test.dart @@ -204,57 +204,6 @@ void main() { ), ); }); - - test('Http logger interceptor request', () async { - final logger = HttpLoggingInterceptor(); - - final logs = []; - chopperLogger.onRecord.listen((r) => logs.add(r.message)); - await logger.onRequest(fakeRequest); - - expect( - logs, - equals( - [ - '--> POST base/', - 'foo: bar', - 'content-type: text/plain; charset=utf-8', - 'test', - '--> END POST (4-byte body)', - ], - ), - ); - }); - - test('Http logger interceptor response', () async { - final logger = HttpLoggingInterceptor(); - - final fakeResponse = Response( - http.Response( - 'responseBodyBase', - 200, - headers: {'foo': 'bar'}, - request: await fakeRequest.toBaseRequest(), - ), - 'responseBody', - ); - - final logs = []; - chopperLogger.onRecord.listen((r) => logs.add(r.message)); - await logger.onResponse(fakeResponse); - - expect( - logs, - equals( - [ - '<-- 200 base/', - 'foo: bar', - 'responseBodyBase', - '--> END POST (16-byte body)', - ], - ), - ); - }); }); } diff --git a/interceptors.md b/interceptors.md index 2aadab1b..b8121023 100644 --- a/interceptors.md +++ b/interceptors.md @@ -35,3 +35,14 @@ final chopper = ChopperClient( * [CurlInterceptor](https://pub.dev/documentation/chopper/latest/chopper/CurlInterceptor-class.html) * [HttpLoggingInterceptor](https://pub.dev/documentation/chopper/latest/chopper/HttpLoggingInterceptor-class.html) +Both the `CurlInterceptor` and `HttpLoggingInterceptor` use the dart [logging package](https://pub.dev/packages/logging). +In order to see logging in console the logging package also needs to be added to your project and configured. + +For example: +```dart +Logger.root.level = Level.ALL; // defaults to Level.INFO +Logger.root.onRecord.listen((record) { + print('${record.level.name}: ${record.time}: ${record.message}'); +}); +``` + From fa73de364efde7b3cb27cf7ea8e65e3f31842614 Mon Sep 17 00:00:00 2001 From: Job Guldemeester Date: Sun, 8 Jan 2023 15:58:46 +0100 Subject: [PATCH 048/171] Made apply headers field name case insensitive. (#400) * Made applyHeaders field name case insensitive. Added tests for applyHeaders * fixed quotes lint check --- chopper/lib/src/utils.dart | 18 ++-- chopper/test/converter_test.dart | 14 +++ chopper/test/utils_test.dart | 164 +++++++++++++++++++++++++++++++ 3 files changed, 189 insertions(+), 7 deletions(-) diff --git a/chopper/lib/src/utils.dart b/chopper/lib/src/utils.dart index 299ddead..bd9ab4cc 100644 --- a/chopper/lib/src/utils.dart +++ b/chopper/lib/src/utils.dart @@ -1,3 +1,5 @@ +import 'dart:collection'; + import 'package:chopper/chopper.dart'; import 'package:logging/logging.dart'; @@ -39,13 +41,15 @@ Request applyHeaders( Map headers, { bool override = true, }) { - final Map headersCopy = {...request.headers}; - - for (String key in headers.keys) { - String? value = headers[key]; - if (value == null) continue; - if (!override && headersCopy.containsKey(key)) continue; - headersCopy[key] = value; + final LinkedHashMap headersCopy = LinkedHashMap( + equals: (a, b) => a.toLowerCase() == b.toLowerCase(), + hashCode: (e) => e.toLowerCase().hashCode, + ); + headersCopy.addAll(request.headers); + + for (final entry in headers.entries) { + if (!override && headersCopy.containsKey(entry.key)) continue; + headersCopy[entry.key] = entry.value; } return request.copyWith(headers: headersCopy); diff --git a/chopper/test/converter_test.dart b/chopper/test/converter_test.dart index 6b5d868c..d02d0a81 100644 --- a/chopper/test/converter_test.dart +++ b/chopper/test/converter_test.dart @@ -112,6 +112,20 @@ void main() { expect(converted.body, equals({'foo': 'bar'})); }); }); + + test('respects content-type headers', () { + final jsonConverter = JsonConverter(); + final testRequest = Request( + 'POST', + Uri.parse('foo'), + Uri.parse('bar'), + headers: {'Content-Type': 'application/vnd.api+json'}, + ); + + final result = jsonConverter.convertRequest(testRequest); + + expect(result.headers['content-type'], 'application/vnd.api+json'); + }); } class TestConverter implements Converter { diff --git a/chopper/test/utils_test.dart b/chopper/test/utils_test.dart index bf3f8e6f..63a35084 100644 --- a/chopper/test/utils_test.dart +++ b/chopper/test/utils_test.dart @@ -1,3 +1,4 @@ +import 'package:chopper/src/request.dart'; import 'package:chopper/src/utils.dart'; import 'package:test/test.dart'; @@ -563,4 +564,167 @@ void main() { ), ); }); + + Request createRequest(Map headers) => Request( + 'POST', + Uri.parse('foo'), + Uri.parse('bar'), + headers: headers, + ); + + group('applyHeader tests', () { + test('request apply single header', () { + final testRequest = createRequest({}); + + final result = applyHeader(testRequest, 'foo', 'bar'); + + expect(result.headers['foo'], 'bar'); + }); + + test('request apply single header overrides existing', () { + final testRequest = createRequest({'foo': 'bar'}); + + final result = applyHeader(testRequest, 'foo', 'whut'); + + expect(result.headers['foo'], 'whut'); + }); + + test( + 'request apply single header overrides existing field name case insensitive', + () { + final testRequest = createRequest({'Foo': 'bar'}); + + final result = applyHeader(testRequest, 'foo', 'whut'); + + expect(result.headers['foo'], 'whut'); + }, + ); + + test('request apply single header doesn\'t overrides existing', () { + final testRequest = createRequest({'foo': 'bar'}); + + final result = applyHeader(testRequest, 'foo', 'whut', override: false); + + expect(result.headers['foo'], 'bar'); + }); + + test( + 'request apply single header doesn\'t overrides existing field name case insensitive', + () { + final testRequest = createRequest({'Foo': 'bar'}); + + final result = applyHeader(testRequest, 'foo', 'whut', override: false); + + expect(result.headers['Foo'], 'bar'); + }, + ); + }); + + group('applyHeaders tests', () { + test('request apply headers', () { + final testRequest = createRequest({}); + + final result = applyHeaders(testRequest, {'foo': 'bar'}); + + expect(result.headers['foo'], 'bar'); + }); + + test('request apply headers overrides existing', () { + final testRequest = createRequest({'foo': 'bar'}); + + final result = applyHeaders(testRequest, {'foo': 'whut'}); + + expect(result.headers['foo'], 'whut'); + }); + + test( + 'request apply headers overrides existing field name case insensitive', + () { + final testRequest = createRequest({'Foo': 'bar'}); + + final result = applyHeaders(testRequest, {'foo': 'whut'}); + + expect(result.headers['foo'], 'whut'); + }, + ); + + test('request apply headers doesn\'t overrides existing', () { + final testRequest = createRequest({'foo': 'bar'}); + + final result = + applyHeaders(testRequest, {'foo': 'whut'}, override: false); + + expect(result.headers['foo'], 'bar'); + }); + + test( + 'request apply headers doesn\'t overrides existing field name case insensitive', + () { + final testRequest = createRequest({'Foo': 'bar'}); + + final result = + applyHeaders(testRequest, {'foo': 'whut'}, override: false); + + expect(result.headers['Foo'], 'bar'); + }, + ); + + test( + 'request apply headers multiple headers with override false', + () { + final testRequest = createRequest( + { + 'Foo': 'bar', + 'tomato': 'apple', + 'phone': 'tablet', + }, + ); + + final result = applyHeaders( + testRequest, + { + 'foo': 'whut', + 'phone': 'computer', + 'chair': 'table', + }, + override: false, + ); + + expect(result.headers['Foo'], 'bar'); + expect(result.headers['tomato'], 'apple'); + expect(result.headers['chair'], 'table'); + expect(result.headers['phone'], 'tablet'); + expect(result.headers.length, 4); + }, + ); + + test( + 'request apply headers multiple headers with override true', + () { + final testRequest = createRequest( + { + 'Foo': 'bar', + 'tomato': 'apple', + 'phone': 'tablet', + }, + ); + + final result = applyHeaders( + testRequest, + { + 'foo': 'whut', + 'phone': 'computer', + 'chair': 'table', + }, + override: true, + ); + + expect(result.headers['Foo'], 'whut'); + expect(result.headers['tomato'], 'apple'); + expect(result.headers['chair'], 'table'); + expect(result.headers['phone'], 'computer'); + expect(result.headers.length, 4); + }, + ); + }); } From bd3d7cc38ff9bb458bd29695ffaeaf4b7e914b18 Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Mon, 9 Jan 2023 09:43:49 +0100 Subject: [PATCH 049/171] :bulb: Clean up code in comments (#403) --- chopper/lib/src/annotations.dart | 6 ++---- chopper/lib/src/utils.dart | 4 ++-- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/chopper/lib/src/annotations.dart b/chopper/lib/src/annotations.dart index 5ddda966..e7fa787c 100644 --- a/chopper/lib/src/annotations.dart +++ b/chopper/lib/src/annotations.dart @@ -328,6 +328,7 @@ typedef ConvertResponse = FutureOr Function(Response response); /// ) /// Future> getTodo(@Path("id")); /// } +/// ``` @immutable class FactoryConverter { final ConvertRequest? request; @@ -365,7 +366,6 @@ class Field { /// @Post(path: '/something') /// Future fetch(@FieldMap List> query); /// ``` -/// @immutable class FieldMap { const FieldMap(); @@ -405,7 +405,6 @@ class Part { /// @Multipart /// Future fetch(@PartMap() List query); /// ``` -/// @immutable class PartMap { const PartMap(); @@ -413,7 +412,7 @@ class PartMap { /// Use [PartFile] to define a file field for a [Multipart] request. /// -/// ``` +/// ```dart /// @Post(path: 'file') /// @multipart /// Future postFile(@PartFile('file') List bytes); @@ -437,7 +436,6 @@ class PartFile { /// @Multipart /// Future fetch(@PartFileMap() List query); /// ``` -/// @immutable class PartFileMap { const PartFileMap(); diff --git a/chopper/lib/src/utils.dart b/chopper/lib/src/utils.dart index bd9ab4cc..656c637c 100644 --- a/chopper/lib/src/utils.dart +++ b/chopper/lib/src/utils.dart @@ -32,8 +32,8 @@ Request applyHeader( /// /// ```dart /// final newRequest = applyHeaders(request, { -/// 'Authorization': 'Bearer ', -/// 'Content-Type': 'application/json', +/// 'Authorization': 'Bearer ', +/// 'Content-Type': 'application/json', /// }); /// ``` Request applyHeaders( From a910ede73256e580fbcab71d78addea98846c90a Mon Sep 17 00:00:00 2001 From: Mark Asselin Date: Thu, 26 Jan 2023 01:20:35 -0800 Subject: [PATCH 050/171] Fix dead link on FAQs page (#404) --- faq.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/faq.md b/faq.md index a2f31633..796a1eac 100644 --- a/faq.md +++ b/faq.md @@ -80,8 +80,7 @@ You may need to change the base URL of your network calls during runtime, for ex Chopper is built on top of `http` package. -So, one can just use the mocking API of the HTTP package. -https://pub.dev/documentation/http/latest/testing/MockClient-class.html +So, one can just use the mocking API of the HTTP package. See the documentation for the [http.testing library](https://pub.dev/documentation/http/latest/http.testing/http.testing-library.html) and for the [MockClient class](https://pub.dev/documentation/http/latest/http.testing/MockClient-class.html). Also, you can follow this code by [ozburo](https://github.com/ozburo): From adfb1c5b1bd9e228ae21f89fa3620f4befb1a04d Mon Sep 17 00:00:00 2001 From: Job Guldemeester Date: Thu, 2 Feb 2023 16:22:29 +0100 Subject: [PATCH 051/171] Updated chopper version to 6.0.0 for chopper_built_value (#406) --- chopper_built_value/pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chopper_built_value/pubspec.yaml b/chopper_built_value/pubspec.yaml index 0bd63c23..2275d2de 100644 --- a/chopper_built_value/pubspec.yaml +++ b/chopper_built_value/pubspec.yaml @@ -10,7 +10,7 @@ environment: dependencies: built_value: ^8.0.0 built_collection: ^5.0.0 - chopper: ^5.0.0 + chopper: ^6.0.0 http: ^0.13.0 dev_dependencies: From 253d69a49ae0c5dd9ada9275a3188d2334e6cabf Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Wed, 22 Feb 2023 09:53:14 +0000 Subject: [PATCH 052/171] #290 implement Equatable (#410) * #290 implement Equatable * :heavy_plus_sign: add faker and data_fixture_dart as dev dependencies * :white_check_mark: add Request equatability test * :white_check_mark: add Response equatability test * :fire: Remove some obsolete equatables * :fire: Remove some obsolete equatables * :white_check_mark: add PartValue equatability test --- chopper/lib/src/request.dart | 25 +- chopper/lib/src/response.dart | 10 +- chopper/lib/src/utils.dart | 9 +- chopper/pubspec.yaml | 5 +- chopper/test/equatable_test.dart | 270 ++++++++++++++++++ .../test/fixtures/http_response_fixture.dart | 29 ++ chopper/test/fixtures/payload_fixture.dart | 19 ++ chopper/test/fixtures/request_fixture.dart | 36 +++ chopper/test/fixtures/response_fixture.dart | 29 ++ .../test/helpers/http_response_extension.dart | 21 ++ chopper/test/helpers/payload.dart | 27 ++ 11 files changed, 475 insertions(+), 5 deletions(-) create mode 100644 chopper/test/equatable_test.dart create mode 100644 chopper/test/fixtures/http_response_fixture.dart create mode 100644 chopper/test/fixtures/payload_fixture.dart create mode 100644 chopper/test/fixtures/request_fixture.dart create mode 100644 chopper/test/fixtures/response_fixture.dart create mode 100644 chopper/test/helpers/http_response_extension.dart create mode 100644 chopper/test/helpers/payload.dart diff --git a/chopper/lib/src/request.dart b/chopper/lib/src/request.dart index e418f2c2..e4ecfdb6 100644 --- a/chopper/lib/src/request.dart +++ b/chopper/lib/src/request.dart @@ -2,11 +2,12 @@ import 'dart:async'; import 'package:chopper/src/extensions.dart'; import 'package:chopper/src/utils.dart'; +import 'package:equatable/equatable.dart' show EquatableMixin; import 'package:http/http.dart' as http; import 'package:meta/meta.dart'; /// This class represents an HTTP request that can be made with Chopper. -class Request extends http.BaseRequest { +class Request extends http.BaseRequest with EquatableMixin { final Uri uri; final Uri baseUri; final dynamic body; @@ -207,11 +208,25 @@ class Request extends http.BaseRequest { return request; } + + @override + List get props => [ + method, + uri, + baseUri, + body, + parameters, + headers, + multipart, + parts, + useBrackets, + includeNullQueryVars, + ]; } /// Represents a part in a multipart request. @immutable -class PartValue { +class PartValue with EquatableMixin { final T value; final String name; @@ -227,6 +242,12 @@ class PartValue { name ?? this.name, value ?? this.value as NewType, ); + + @override + List get props => [ + name, + value, + ]; } /// Represents a file part in a multipart request. diff --git a/chopper/lib/src/response.dart b/chopper/lib/src/response.dart index 1fc29fac..8f44e1c5 100644 --- a/chopper/lib/src/response.dart +++ b/chopper/lib/src/response.dart @@ -1,5 +1,6 @@ import 'dart:typed_data'; +import 'package:equatable/equatable.dart' show EquatableMixin; import 'package:http/http.dart' as http; import 'package:meta/meta.dart'; @@ -15,7 +16,7 @@ import 'package:meta/meta.dart'; /// Future> fetchItem(); /// ``` @immutable -class Response { +class Response with EquatableMixin { /// The [http.BaseResponse] from `package:http` that this [Response] wraps. final http.BaseResponse base; @@ -65,4 +66,11 @@ class Response { /// call was successful, else this will be `null`. String get bodyString => base is http.Response ? (base as http.Response).body : ''; + + @override + List get props => [ + base, + body, + error, + ]; } diff --git a/chopper/lib/src/utils.dart b/chopper/lib/src/utils.dart index 656c637c..c8853170 100644 --- a/chopper/lib/src/utils.dart +++ b/chopper/lib/src/utils.dart @@ -1,6 +1,7 @@ import 'dart:collection'; import 'package:chopper/chopper.dart'; +import 'package:equatable/equatable.dart' show EquatableMixin; import 'package:logging/logging.dart'; /// Creates a new [Request] by copying [request] and adding a header with the @@ -130,7 +131,7 @@ Iterable<_Pair> _iterableToQuery( String _normalizeValue(value) => Uri.encodeComponent(value?.toString() ?? ''); -class _Pair { +class _Pair with EquatableMixin { final A first; final B second; final bool useBrackets; @@ -145,6 +146,12 @@ class _Pair { String toString() => useBrackets ? '$first${Uri.encodeQueryComponent('[]')}=$second' : '$first=$second'; + + @override + List get props => [ + first, + second, + ]; } bool isTypeOf() => _Instance() is _Instance; diff --git a/chopper/pubspec.yaml b/chopper/pubspec.yaml index ad1ac78f..815db4be 100644 --- a/chopper/pubspec.yaml +++ b/chopper/pubspec.yaml @@ -8,6 +8,7 @@ environment: sdk: ">=2.17.0 <3.0.0" dependencies: + equatable: ^2.0.5 http: ">=0.13.0 <1.0.0" logging: ^1.0.0 meta: ^1.3.0 @@ -17,7 +18,9 @@ dev_dependencies: build_test: ^2.0.0 collection: ^1.16.0 coverage: ^1.0.2 - dart_code_metrics: ^4.8.1 + dart_code_metrics: '>=4.8.1 <6.0.0' + data_fixture_dart: ^2.2.0 + faker: ^2.1.0 http_parser: ^4.0.0 lints: ^2.0.0 test: ^1.16.4 diff --git a/chopper/test/equatable_test.dart b/chopper/test/equatable_test.dart new file mode 100644 index 00000000..7bdebeae --- /dev/null +++ b/chopper/test/equatable_test.dart @@ -0,0 +1,270 @@ +import 'dart:convert' show jsonEncode; + +import 'package:chopper/chopper.dart'; +import 'package:faker/faker.dart'; +import 'package:http/http.dart' as http; +import 'package:test/test.dart'; + +import 'fixtures/http_response_fixture.dart' as http_fixture; +import 'fixtures/payload_fixture.dart'; +import 'fixtures/request_fixture.dart'; +import 'fixtures/response_fixture.dart'; +import 'helpers/payload.dart'; + +void main() { + final Faker faker = Faker(); + + group('Request', () { + final Uri baseUrl = Uri.parse(faker.internet.httpsUrl()); + late Request request; + + setUp(() { + request = RequestFixture.factory.makeSingle(); + }); + + test('should return true when comparing two identical objects', () { + expect( + Request( + 'GET', + Uri.parse('/foo'), + baseUrl, + headers: {'bar': 'baz'}, + ), + equals( + Request( + 'GET', + Uri.parse('/foo'), + baseUrl, + headers: {'bar': 'baz'}, + ), + ), + ); + }); + + test( + 'should return true when comparing original with copy', + () => expect( + request, + equals( + request.copyWith(), + ), + ), + ); + + test( + 'should return false when comparing two different objects', + () => expect( + request, + isNot( + equals( + RequestFixture.factory.makeSingle(), + ), + ), + ), + ); + + test( + 'should return false when comparing to null', + () => expect( + request, + isNot( + equals(null), + ), + ), + ); + + test( + 'should return false when comparing to an object of a different type', + () { + expect( + request, + isNot( + equals(faker.lorem.word()), + ), + ); + }, + ); + + test( + 'should return false when comparing to an object with different props', + () => expect( + request, + isNot( + equals( + request.copyWith( + headers: {'bar': 'bazzz'}, + ), + ), + ), + ), + ); + }); + + group('Response', () { + late Payload payload; + late Response response; + + setUp(() { + payload = PayloadFixture.factory.makeSingle(); + response = ResponseFixture.factory() + .redefine(ResponseFixture.factory().body(payload)) + .makeSingle(); + }); + + test('should return true when comparing two identical objects', () { + final http.Response base = http_fixture.ResponseFixture.factory + .redefine( + http_fixture.ResponseFixture.factory.body( + jsonEncode(payload), + ), + ) + .makeSingle(); + + expect( + Response(base, payload), + equals( + Response(base, payload), + ), + ); + }); + + test( + 'should return true when comparing original with copy', + () => expect( + response, + equals( + response.copyWith(), + ), + ), + ); + + test( + 'should return false when comparing two different objects', + () => expect( + response, + isNot( + equals( + ResponseFixture.factory() + .redefine(ResponseFixture.factory() + .body(PayloadFixture.factory.makeSingle())) + .makeSingle(), + ), + ), + ), + ); + + test( + 'should return false when comparing to null', + () => expect( + response, + isNot( + equals(null), + ), + ), + ); + + test( + 'should return false when comparing to an object of a different type', + () { + expect( + response, + isNot( + equals(faker.lorem.word()), + ), + ); + }, + ); + + test( + 'should return false when comparing to an object with different props', + () => expect( + response, + isNot( + equals( + response.copyWith( + body: PayloadFixture.factory.makeSingle(), + ), + ), + ), + ), + ); + }); + + group('PartValue', () { + late PartValue partValue; + + setUp(() { + partValue = PartValue( + faker.lorem.word(), + faker.lorem.word(), + ); + }); + + test('should return true when comparing two identical objects', () { + expect( + PartValue('foo', 'bar'), + equals( + PartValue('foo', 'bar'), + ), + ); + }); + + test( + 'should return true when comparing original with copy', + () => expect( + partValue, + equals( + partValue.copyWith(), + ), + ), + ); + + test( + 'should return false when comparing two different objects', + () => expect( + partValue, + isNot( + equals( + PartValue('bar', 'baz'), + ), + ), + ), + ); + + test( + 'should return false when comparing to null', + () => expect( + partValue, + isNot( + equals(null), + ), + ), + ); + + test( + 'should return false when comparing to an object of a different type', + () { + expect( + partValue, + isNot( + equals(faker.lorem.word()), + ), + ); + }, + ); + + test( + 'should return false when comparing to an object with different props', + () => expect( + partValue, + isNot( + equals( + partValue.copyWith( + value: 'bar', + ), + ), + ), + ), + ); + }); +} diff --git a/chopper/test/fixtures/http_response_fixture.dart b/chopper/test/fixtures/http_response_fixture.dart new file mode 100644 index 00000000..6b6b6071 --- /dev/null +++ b/chopper/test/fixtures/http_response_fixture.dart @@ -0,0 +1,29 @@ +import 'dart:convert' show jsonEncode; + +import 'package:data_fixture_dart/data_fixture_dart.dart'; +import 'package:http/http.dart' as http; +import 'package:meta/meta.dart'; + +import '../helpers/http_response_extension.dart'; +import 'payload_fixture.dart'; + +extension ResponseFixture on http.Response { + static ResponseFactory get factory => ResponseFactory(); +} + +@internal +class ResponseFactory extends FixtureFactory { + @override + FixtureDefinition definition() => define( + (Faker faker) => http.Response( + jsonEncode(PayloadFixture.factory.makeSingle().toJson()), + 200, + ), + ); + + FixtureRedefinitionBuilder body(String? body) => + (http.Response response) => response.copyWith(body: body); + + FixtureRedefinitionBuilder statusCode(int? statusCode) => + (http.Response response) => response.copyWith(statusCode: statusCode); +} diff --git a/chopper/test/fixtures/payload_fixture.dart b/chopper/test/fixtures/payload_fixture.dart new file mode 100644 index 00000000..435883cf --- /dev/null +++ b/chopper/test/fixtures/payload_fixture.dart @@ -0,0 +1,19 @@ +import 'package:data_fixture_dart/data_fixture_dart.dart'; +import 'package:meta/meta.dart'; + +import '../helpers/payload.dart'; + +extension PayloadFixture on Payload { + static PayloadFactory get factory => PayloadFactory(); +} + +@internal +class PayloadFactory extends FixtureFactory { + @override + FixtureDefinition definition() => define( + (Faker faker) => Payload( + statusCode: 200, + message: faker.lorem.sentence(), + ), + ); +} diff --git a/chopper/test/fixtures/request_fixture.dart b/chopper/test/fixtures/request_fixture.dart new file mode 100644 index 00000000..6d422cf4 --- /dev/null +++ b/chopper/test/fixtures/request_fixture.dart @@ -0,0 +1,36 @@ +import 'dart:convert' show jsonEncode; + +import 'package:chopper/chopper.dart' show Request; +import 'package:data_fixture_dart/data_fixture_dart.dart'; +import 'package:meta/meta.dart'; + +extension RequestFixture on Request { + static RequestFixtureFactory get factory => RequestFixtureFactory(); +} + +@internal +class RequestFixtureFactory extends FixtureFactory { + @override + FixtureDefinition definition() { + final String method = + faker.randomGenerator.element(['GET', 'POST', 'PUT', 'DELETE']); + + return define( + (Faker faker) => Request( + method, + Uri.parse('/${faker.lorem.word()}'), + Uri.https(faker.internet.domainName()), + headers: faker.randomGenerator.boolean() + ? {'x-${faker.lorem.word()}': faker.lorem.word()} + : {}, + parameters: faker.randomGenerator.boolean() + ? {faker.lorem.word(): faker.lorem.word()} + : null, + body: + faker.randomGenerator.boolean() && ['POST', 'PUT'].contains(method) + ? jsonEncode({faker.lorem.word(): faker.lorem.sentences(10)}) + : null, + ), + ); + } +} diff --git a/chopper/test/fixtures/response_fixture.dart b/chopper/test/fixtures/response_fixture.dart new file mode 100644 index 00000000..cd604e4f --- /dev/null +++ b/chopper/test/fixtures/response_fixture.dart @@ -0,0 +1,29 @@ +import 'package:chopper/chopper.dart' show Response; +import 'package:data_fixture_dart/data_fixture_dart.dart'; +import 'package:http/http.dart' as http; +import 'package:meta/meta.dart'; + +import 'http_response_fixture.dart' as http_fixture; + +extension ResponseFixture on Response { + static ResponseFixtureFactory factory() => ResponseFixtureFactory(); +} + +@internal +class ResponseFixtureFactory extends FixtureFactory> { + @override + FixtureDefinition> definition() { + final http.Response base = + http_fixture.ResponseFixture.factory.makeSingle(); + + return define( + (Faker faker) => Response(base, null), + ); + } + + FixtureRedefinitionBuilder> body(T? body) => + (Response response) => response.copyWith(body: body); + + FixtureRedefinitionBuilder> error(Object? value) => + (Response response) => response.copyWith(bodyError: value); +} diff --git a/chopper/test/helpers/http_response_extension.dart b/chopper/test/helpers/http_response_extension.dart new file mode 100644 index 00000000..17094acf --- /dev/null +++ b/chopper/test/helpers/http_response_extension.dart @@ -0,0 +1,21 @@ +import 'package:http/http.dart' as http; + +extension HttpResponseExtension on http.Response { + http.Response copyWith({ + String? body, + int? statusCode, + Map? headers, + bool? isRedirect, + bool? persistentConnection, + String? reasonPhrase, + }) => + http.Response( + body ?? this.body, + statusCode ?? this.statusCode, + request: request, + headers: headers ?? this.headers, + reasonPhrase: reasonPhrase ?? this.reasonPhrase, + isRedirect: isRedirect ?? this.isRedirect, + persistentConnection: persistentConnection ?? this.persistentConnection, + ); +} diff --git a/chopper/test/helpers/payload.dart b/chopper/test/helpers/payload.dart new file mode 100644 index 00000000..4c4ea8a3 --- /dev/null +++ b/chopper/test/helpers/payload.dart @@ -0,0 +1,27 @@ +import 'package:equatable/equatable.dart'; + +class Payload with EquatableMixin { + const Payload({ + this.statusCode = 200, + this.message = 'OK', + }); + + final int statusCode; + final String message; + + factory Payload.fromJson(Map json) => Payload( + statusCode: json['statusCode'] as int? ?? 200, + message: json['message'] as String? ?? 'OK', + ); + + Map toJson() => { + 'statusCode': statusCode, + 'message': message, + }; + + @override + List get props => [ + statusCode, + message, + ]; +} From 99767520e55704ac7d054a8deace4a950c63d629 Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Mon, 13 Mar 2023 08:41:09 +0000 Subject: [PATCH 053/171] Add Feature request Github issue template (#414) --- .github/ISSUE_TEMPLATE/feature_request.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 00000000..6532412f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: enhancement +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. \ No newline at end of file From 617f3f399cee06c76f2e605139693e8f4d596494 Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Wed, 29 Mar 2023 20:48:52 +0100 Subject: [PATCH 054/171] :construction_worker: add CODEOWNERS (#415) --- .github/CODEOWNERS | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .github/CODEOWNERS diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 00000000..1b115426 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,5 @@ +# These owners will be the default owners for everything in +# the repo. Unless a later match takes precedence, +# @global-owner1 and @global-owner2 will be requested for +# review when someone opens a pull request. +* @Guldem @JEuler @lejard-h @meysam1717 @pixeltoast @stewemetal @techouse \ No newline at end of file From d83ad405be18b3655bffedc09094aa80e76c27f6 Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Fri, 12 May 2023 08:30:07 +0100 Subject: [PATCH 055/171] :arrow_up: Bump Dart SDK constraint to ">=2.17.0 <4.0.0" (#417) * :arrow_up: Bump Dart constraint to ">=2.17.0 <3.0.0" * :arrow_up: Bump mono_repo Yaml * :rotating_light: Ignore Element.enclosingElement3 warning --- .github/workflows/dart.yml | 98 ++++++++++--------- chopper/pubspec.yaml | 4 +- chopper_built_value/pubspec.yaml | 6 +- chopper_built_value/test/data.g.dart | 13 ++- chopper_built_value/test/serializers.g.dart | 4 +- chopper_generator/lib/src/generator.dart | 4 + chopper_generator/pubspec.yaml | 6 +- example/lib/built_value_resource.chopper.dart | 2 +- example/lib/built_value_resource.g.dart | 16 ++- example/lib/built_value_serializers.g.dart | 4 +- .../lib/json_decode_service.activator.g.dart | 2 +- example/lib/json_decode_service.vm.g.dart | 4 +- example/lib/json_decode_service.worker.g.dart | 12 +-- example/pubspec.yaml | 10 +- tool/ci.sh | 2 +- 15 files changed, 102 insertions(+), 85 deletions(-) diff --git a/.github/workflows/dart.yml b/.github/workflows/dart.yml index 25ae548e..8c57149d 100644 --- a/.github/workflows/dart.yml +++ b/.github/workflows/dart.yml @@ -1,4 +1,4 @@ -# Created with package:mono_repo v6.4.2 +# Created with package:mono_repo v6.5.5 name: Dart CI on: push: @@ -22,7 +22,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Cache Pub hosted dependencies - uses: actions/cache@9b0c1fce7a93df8e3bb8926b0d6e9d89e92f20a7 + uses: actions/cache@88522ab9f39a2ea568f7027eddc7d8d8bc9d59c8 with: path: "~/.pub-cache/hosted" key: "os:ubuntu-latest;pub-cache-hosted;sdk:stable" @@ -30,22 +30,58 @@ jobs: os:ubuntu-latest;pub-cache-hosted os:ubuntu-latest - name: Setup Dart SDK - uses: dart-lang/setup-dart@6a218f2413a3e78e9087f638a238f6b40893203d + uses: dart-lang/setup-dart@d6a63dab3335f427404425de0fbfed4686d93c4f with: sdk: stable - id: checkout name: Checkout repository - uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 + uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab - name: mono_repo self validate - run: dart pub global activate mono_repo 6.4.2 + run: dart pub global activate mono_repo 6.5.5 - name: mono_repo self validate run: dart pub global run mono_repo generate --validate job_002: + name: "analyze_and_format; PKG: chopper; `dart format --output=none --set-exit-if-changed .`, `dart analyze --fatal-infos .`" + runs-on: ubuntu-latest + steps: + - name: Cache Pub hosted dependencies + uses: actions/cache@88522ab9f39a2ea568f7027eddc7d8d8bc9d59c8 + with: + path: "~/.pub-cache/hosted" + key: "os:ubuntu-latest;pub-cache-hosted;sdk:stable;packages:chopper;commands:format-analyze" + restore-keys: | + os:ubuntu-latest;pub-cache-hosted;sdk:stable;packages:chopper + os:ubuntu-latest;pub-cache-hosted;sdk:stable + os:ubuntu-latest;pub-cache-hosted + os:ubuntu-latest + - name: Setup Dart SDK + uses: dart-lang/setup-dart@d6a63dab3335f427404425de0fbfed4686d93c4f + with: + sdk: stable + - id: checkout + name: Checkout repository + uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab + - id: chopper_pub_upgrade + name: chopper; dart pub upgrade + run: dart pub upgrade + if: "always() && steps.checkout.conclusion == 'success'" + working-directory: chopper + - name: "chopper; dart format --output=none --set-exit-if-changed ." + run: "dart format --output=none --set-exit-if-changed ." + if: "always() && steps.chopper_pub_upgrade.conclusion == 'success'" + working-directory: chopper + - name: "chopper; dart analyze --fatal-infos ." + run: dart analyze --fatal-infos . + if: "always() && steps.chopper_pub_upgrade.conclusion == 'success'" + working-directory: chopper + needs: + - job_001 + job_003: name: "analyzer_and_format; PKGS: chopper_built_value, chopper_generator; `dart format --output=none --set-exit-if-changed .`, `dart analyze --fatal-infos .`" runs-on: ubuntu-latest steps: - name: Cache Pub hosted dependencies - uses: actions/cache@9b0c1fce7a93df8e3bb8926b0d6e9d89e92f20a7 + uses: actions/cache@88522ab9f39a2ea568f7027eddc7d8d8bc9d59c8 with: path: "~/.pub-cache/hosted" key: "os:ubuntu-latest;pub-cache-hosted;sdk:stable;packages:chopper_built_value-chopper_generator;commands:format-analyze" @@ -55,12 +91,12 @@ jobs: os:ubuntu-latest;pub-cache-hosted os:ubuntu-latest - name: Setup Dart SDK - uses: dart-lang/setup-dart@6a218f2413a3e78e9087f638a238f6b40893203d + uses: dart-lang/setup-dart@d6a63dab3335f427404425de0fbfed4686d93c4f with: sdk: stable - id: checkout name: Checkout repository - uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 + uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab - id: chopper_built_value_pub_upgrade name: chopper_built_value; dart pub upgrade run: dart pub upgrade @@ -87,40 +123,6 @@ jobs: run: dart analyze --fatal-infos . if: "always() && steps.chopper_generator_pub_upgrade.conclusion == 'success'" working-directory: chopper_generator - job_003: - name: "analyze_and_format; PKG: chopper; `dart format --output=none --set-exit-if-changed .`, `dart analyze --fatal-infos .`" - runs-on: ubuntu-latest - steps: - - name: Cache Pub hosted dependencies - uses: actions/cache@9b0c1fce7a93df8e3bb8926b0d6e9d89e92f20a7 - with: - path: "~/.pub-cache/hosted" - key: "os:ubuntu-latest;pub-cache-hosted;sdk:stable;packages:chopper;commands:format-analyze" - restore-keys: | - os:ubuntu-latest;pub-cache-hosted;sdk:stable;packages:chopper - os:ubuntu-latest;pub-cache-hosted;sdk:stable - os:ubuntu-latest;pub-cache-hosted - os:ubuntu-latest - - name: Setup Dart SDK - uses: dart-lang/setup-dart@6a218f2413a3e78e9087f638a238f6b40893203d - with: - sdk: stable - - id: checkout - name: Checkout repository - uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 - - id: chopper_pub_upgrade - name: chopper; dart pub upgrade - run: dart pub upgrade - if: "always() && steps.checkout.conclusion == 'success'" - working-directory: chopper - - name: "chopper; dart format --output=none --set-exit-if-changed ." - run: "dart format --output=none --set-exit-if-changed ." - if: "always() && steps.chopper_pub_upgrade.conclusion == 'success'" - working-directory: chopper - - name: "chopper; dart analyze --fatal-infos ." - run: dart analyze --fatal-infos . - if: "always() && steps.chopper_pub_upgrade.conclusion == 'success'" - working-directory: chopper needs: - job_001 - job_002 @@ -129,7 +131,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Cache Pub hosted dependencies - uses: actions/cache@9b0c1fce7a93df8e3bb8926b0d6e9d89e92f20a7 + uses: actions/cache@88522ab9f39a2ea568f7027eddc7d8d8bc9d59c8 with: path: "~/.pub-cache/hosted" key: "os:ubuntu-latest;pub-cache-hosted;sdk:stable;packages:chopper-chopper_built_value;commands:test_1" @@ -139,12 +141,12 @@ jobs: os:ubuntu-latest;pub-cache-hosted os:ubuntu-latest - name: Setup Dart SDK - uses: dart-lang/setup-dart@6a218f2413a3e78e9087f638a238f6b40893203d + uses: dart-lang/setup-dart@d6a63dab3335f427404425de0fbfed4686d93c4f with: sdk: stable - id: checkout name: Checkout repository - uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 + uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab - id: chopper_pub_upgrade name: chopper; dart pub upgrade run: dart pub upgrade @@ -172,7 +174,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Cache Pub hosted dependencies - uses: actions/cache@9b0c1fce7a93df8e3bb8926b0d6e9d89e92f20a7 + uses: actions/cache@88522ab9f39a2ea568f7027eddc7d8d8bc9d59c8 with: path: "~/.pub-cache/hosted" key: "os:ubuntu-latest;pub-cache-hosted;sdk:stable;packages:chopper-chopper_built_value;commands:test_0" @@ -182,12 +184,12 @@ jobs: os:ubuntu-latest;pub-cache-hosted os:ubuntu-latest - name: Setup Dart SDK - uses: dart-lang/setup-dart@6a218f2413a3e78e9087f638a238f6b40893203d + uses: dart-lang/setup-dart@d6a63dab3335f427404425de0fbfed4686d93c4f with: sdk: stable - id: checkout name: Checkout repository - uses: actions/checkout@93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8 + uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab - id: chopper_pub_upgrade name: chopper; dart pub upgrade run: dart pub upgrade diff --git a/chopper/pubspec.yaml b/chopper/pubspec.yaml index 815db4be..f6cfea6a 100644 --- a/chopper/pubspec.yaml +++ b/chopper/pubspec.yaml @@ -1,11 +1,11 @@ name: chopper description: Chopper is an http client generator using source_gen, inspired by Retrofit -version: 6.0.0 +version: 6.1.2 documentation: https://hadrien-lejard.gitbook.io/chopper repository: https://github.com/lejard-h/chopper environment: - sdk: ">=2.17.0 <3.0.0" + sdk: ">=2.17.0 <4.0.0" dependencies: equatable: ^2.0.5 diff --git a/chopper_built_value/pubspec.yaml b/chopper_built_value/pubspec.yaml index 2275d2de..1f536048 100644 --- a/chopper_built_value/pubspec.yaml +++ b/chopper_built_value/pubspec.yaml @@ -1,11 +1,11 @@ name: chopper_built_value description: A built_value based Converter for Chopper. -version: 1.1.0 +version: 1.2.1 documentation: https://hadrien-lejard.gitbook.io/chopper/converters/built-value-converter repository: https://github.com/lejard-h/chopper environment: - sdk: ">=2.17.0 <3.0.0" + sdk: ">=2.17.0 <4.0.0" dependencies: built_value: ^8.0.0 @@ -18,7 +18,7 @@ dev_dependencies: build_runner: ^2.0.0 build_test: ^2.0.0 built_value_generator: ^8.0.6 - dart_code_metrics: ^4.8.1 + dart_code_metrics: '>=4.8.1 <6.0.0' lints: ^2.0.0 dependency_overrides: diff --git a/chopper_built_value/test/data.g.dart b/chopper_built_value/test/data.g.dart index d9413768..82eb32ea 100644 --- a/chopper_built_value/test/data.g.dart +++ b/chopper_built_value/test/data.g.dart @@ -123,7 +123,11 @@ class _$DataModel extends DataModel { @override int get hashCode { - return $jf($jc($jc(0, id.hashCode), name.hashCode)); + var _$hash = 0; + _$hash = $jc(_$hash, id.hashCode); + _$hash = $jc(_$hash, name.hashCode); + _$hash = $jf(_$hash); + return _$hash; } @override @@ -209,7 +213,10 @@ class _$ErrorModel extends ErrorModel { @override int get hashCode { - return $jf($jc(0, message.hashCode)); + var _$hash = 0; + _$hash = $jc(_$hash, message.hashCode); + _$hash = $jf(_$hash); + return _$hash; } @override @@ -261,4 +268,4 @@ class ErrorModelBuilder implements Builder { } } -// ignore_for_file: always_put_control_body_on_new_line,always_specify_types,annotate_overrides,avoid_annotating_with_dynamic,avoid_as,avoid_catches_without_on_clauses,avoid_returning_this,deprecated_member_use_from_same_package,lines_longer_than_80_chars,no_leading_underscores_for_local_identifiers,omit_local_variable_types,prefer_expression_function_bodies,sort_constructors_first,test_types_in_equals,unnecessary_const,unnecessary_new,unnecessary_lambdas +// ignore_for_file: deprecated_member_use_from_same_package,type=lint diff --git a/chopper_built_value/test/serializers.g.dart b/chopper_built_value/test/serializers.g.dart index 55d2a7d3..8fc6a12d 100644 --- a/chopper_built_value/test/serializers.g.dart +++ b/chopper_built_value/test/serializers.g.dart @@ -1,6 +1,6 @@ // GENERATED CODE - DO NOT MODIFY BY HAND -part of serializers; +part of 'serializers.dart'; // ************************************************************************** // BuiltValueGenerator @@ -11,4 +11,4 @@ Serializers _$serializers = (new Serializers().toBuilder() ..add(ErrorModel.serializer)) .build(); -// ignore_for_file: always_put_control_body_on_new_line,always_specify_types,annotate_overrides,avoid_annotating_with_dynamic,avoid_as,avoid_catches_without_on_clauses,avoid_returning_this,deprecated_member_use_from_same_package,lines_longer_than_80_chars,no_leading_underscores_for_local_identifiers,omit_local_variable_types,prefer_expression_function_bodies,sort_constructors_first,test_types_in_equals,unnecessary_const,unnecessary_new,unnecessary_lambdas +// ignore_for_file: deprecated_member_use_from_same_package,type=lint diff --git a/chopper_generator/lib/src/generator.dart b/chopper_generator/lib/src/generator.dart index 67fb905f..cce54040 100644 --- a/chopper_generator/lib/src/generator.dart +++ b/chopper_generator/lib/src/generator.dart @@ -364,8 +364,12 @@ class ChopperGenerator extends GeneratorForAnnotation { }); } + /// TODO: Upgrade to `Element.enclosingElement` when analyzer 6.0.0 is released; in the mean time ignore the deprecation warning + /// https://github.com/dart-lang/sdk/blob/main/pkg/analyzer/CHANGELOG.md#520 String _factoryForFunction(FunctionTypedElement function) => + // ignore: deprecated_member_use function.enclosingElement3 is ClassElement + // ignore: deprecated_member_use ? '${function.enclosingElement3!.name}.${function.name}' : function.name!; diff --git a/chopper_generator/pubspec.yaml b/chopper_generator/pubspec.yaml index c4ed6cfe..716f5332 100644 --- a/chopper_generator/pubspec.yaml +++ b/chopper_generator/pubspec.yaml @@ -1,11 +1,11 @@ name: chopper_generator description: Chopper is an http client generator using source_gen, inspired by Retrofit -version: 6.0.0 +version: 6.0.1 documentation: https://hadrien-lejard.gitbook.io/chopper repository: https://github.com/lejard-h/chopper environment: - sdk: ">=2.17.0 <3.0.0" + sdk: ">=2.17.0 <4.0.0" dependencies: analyzer: '>=4.4.0 <6.0.0' @@ -20,7 +20,7 @@ dependencies: dev_dependencies: test: ^1.16.4 - dart_code_metrics: ^4.8.1 + dart_code_metrics: '>=4.8.1 <6.0.0' lints: ^2.0.0 dependency_overrides: diff --git a/example/lib/built_value_resource.chopper.dart b/example/lib/built_value_resource.chopper.dart index 9b83b3fb..e30a468b 100644 --- a/example/lib/built_value_resource.chopper.dart +++ b/example/lib/built_value_resource.chopper.dart @@ -1,6 +1,6 @@ // GENERATED CODE - DO NOT MODIFY BY HAND -part of resource; +part of 'built_value_resource.dart'; // ************************************************************************** // ChopperGenerator diff --git a/example/lib/built_value_resource.g.dart b/example/lib/built_value_resource.g.dart index bc969e05..525a0594 100644 --- a/example/lib/built_value_resource.g.dart +++ b/example/lib/built_value_resource.g.dart @@ -1,6 +1,6 @@ // GENERATED CODE - DO NOT MODIFY BY HAND -part of resource; +part of 'built_value_resource.dart'; // ************************************************************************** // BuiltValueGenerator @@ -131,7 +131,11 @@ class _$Resource extends Resource { @override int get hashCode { - return $jf($jc($jc(0, id.hashCode), name.hashCode)); + var _$hash = 0; + _$hash = $jc(_$hash, id.hashCode); + _$hash = $jc(_$hash, name.hashCode); + _$hash = $jf(_$hash); + return _$hash; } @override @@ -222,7 +226,11 @@ class _$ResourceError extends ResourceError { @override int get hashCode { - return $jf($jc($jc(0, type.hashCode), message.hashCode)); + var _$hash = 0; + _$hash = $jc(_$hash, type.hashCode); + _$hash = $jc(_$hash, message.hashCode); + _$hash = $jf(_$hash); + return _$hash; } @override @@ -284,4 +292,4 @@ class ResourceErrorBuilder } } -// ignore_for_file: always_put_control_body_on_new_line,always_specify_types,annotate_overrides,avoid_annotating_with_dynamic,avoid_as,avoid_catches_without_on_clauses,avoid_returning_this,deprecated_member_use_from_same_package,lines_longer_than_80_chars,no_leading_underscores_for_local_identifiers,omit_local_variable_types,prefer_expression_function_bodies,sort_constructors_first,test_types_in_equals,unnecessary_const,unnecessary_new,unnecessary_lambdas +// ignore_for_file: deprecated_member_use_from_same_package,type=lint diff --git a/example/lib/built_value_serializers.g.dart b/example/lib/built_value_serializers.g.dart index 699b3f08..1dd64f7a 100644 --- a/example/lib/built_value_serializers.g.dart +++ b/example/lib/built_value_serializers.g.dart @@ -1,6 +1,6 @@ // GENERATED CODE - DO NOT MODIFY BY HAND -part of serializers; +part of 'built_value_serializers.dart'; // ************************************************************************** // BuiltValueGenerator @@ -11,4 +11,4 @@ Serializers _$serializers = (new Serializers().toBuilder() ..add(ResourceError.serializer)) .build(); -// ignore_for_file: always_put_control_body_on_new_line,always_specify_types,annotate_overrides,avoid_annotating_with_dynamic,avoid_as,avoid_catches_without_on_clauses,avoid_returning_this,deprecated_member_use_from_same_package,lines_longer_than_80_chars,no_leading_underscores_for_local_identifiers,omit_local_variable_types,prefer_expression_function_bodies,sort_constructors_first,test_types_in_equals,unnecessary_const,unnecessary_new,unnecessary_lambdas +// ignore_for_file: deprecated_member_use_from_same_package,type=lint diff --git a/example/lib/json_decode_service.activator.g.dart b/example/lib/json_decode_service.activator.g.dart index 1756d3d3..1e151e3c 100644 --- a/example/lib/json_decode_service.activator.g.dart +++ b/example/lib/json_decode_service.activator.g.dart @@ -1,7 +1,7 @@ // GENERATED CODE - DO NOT MODIFY BY HAND // ************************************************************************** -// SquadronWorkerGenerator +// Generated by: WorkerGenerator // ************************************************************************** import 'json_decode_service.vm.g.dart'; diff --git a/example/lib/json_decode_service.vm.g.dart b/example/lib/json_decode_service.vm.g.dart index 7ad9a226..e798c22e 100644 --- a/example/lib/json_decode_service.vm.g.dart +++ b/example/lib/json_decode_service.vm.g.dart @@ -1,13 +1,13 @@ // GENERATED CODE - DO NOT MODIFY BY HAND // ************************************************************************** -// SquadronWorkerGenerator +// Generated by: WorkerGenerator // ************************************************************************** import 'package:squadron/squadron_service.dart'; import 'json_decode_service.dart'; // VM entry point -void _start(Map command) => run($JsonDecodeServiceInitializer, command); +void _start(Map command) => run($JsonDecodeServiceInitializer, command, null); dynamic $getJsonDecodeServiceActivator() => _start; diff --git a/example/lib/json_decode_service.worker.g.dart b/example/lib/json_decode_service.worker.g.dart index d50372ab..0119d302 100644 --- a/example/lib/json_decode_service.worker.g.dart +++ b/example/lib/json_decode_service.worker.g.dart @@ -3,7 +3,7 @@ part of 'json_decode_service.dart'; // ************************************************************************** -// SquadronWorkerGenerator +// WorkerGenerator // ************************************************************************** // Operations map for JsonDecodeService @@ -14,9 +14,8 @@ mixin $JsonDecodeServiceOperations on WorkerService { static const int _$jsonDecodeId = 1; - static Map _getOperations(JsonDecodeService svc) => { - _$jsonDecodeId: (r) => svc.jsonDecode(r.args[0]), - }; + static Map _getOperations(JsonDecodeService svc) => + {_$jsonDecodeId: (req) => svc.jsonDecode(req.args[0])}; } // Service initializer @@ -33,9 +32,6 @@ class JsonDecodeServiceWorker extends Worker Future jsonDecode(String source) => send( $JsonDecodeServiceOperations._$jsonDecodeId, args: [source], - token: null, - inspectRequest: false, - inspectResponse: false, ); @override @@ -52,7 +48,7 @@ class JsonDecodeServiceWorkerPool extends WorkerPool @override Future jsonDecode(String source) => - execute((w) => w.jsonDecode(source)); + execute(($w) => $w.jsonDecode(source)); @override Map get operations => WorkerService.noOperations; diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 47051983..963f23c9 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -1,11 +1,11 @@ name: chopper_example description: Example usage of the Chopper package -version: 0.0.3 +version: 0.0.4 documentation: https://hadrien-lejard.gitbook.io/chopper/ #author: Hadrien Lejard environment: - sdk: '>=2.17.0 <3.0.0' + sdk: '>=2.17.0 <4.0.0' dependencies: chopper: @@ -14,16 +14,16 @@ dependencies: analyzer: http: built_collection: - squadron: ^4.3.0 + squadron: ^4.3.8 dev_dependencies: build_runner: chopper_generator: json_serializable: built_value_generator: - dart_code_metrics: ^4.8.1 + dart_code_metrics: '>=4.8.1 <6.0.0' lints: ^2.0.0 - squadron_builder: ^0.9.0 + squadron_builder: ^2.0.0 dependency_overrides: chopper: diff --git a/tool/ci.sh b/tool/ci.sh index 372d5024..ca07f3a6 100755 --- a/tool/ci.sh +++ b/tool/ci.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Created with package:mono_repo v6.4.2 +# Created with package:mono_repo v6.5.5 # Support built in commands on windows out of the box. # When it is a flutter repo (check the pubspec.yaml for "sdk: flutter") From 83f8c259a9eec9e51b87fc0f3579d200ecaabc34 Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Sat, 13 May 2023 19:03:02 +0100 Subject: [PATCH 056/171] :green_heart: Fix CI publish (#419) --- .github/workflows/publish.yml | 63 ++++++++++++++++---------- .github/workflows/publish_dry_run.yml | 47 +++++++++++++++++++ chopper/.DS_Store | Bin 6148 -> 0 bytes chopper/CHANGELOG.md | 12 +++++ chopper_built_value/CHANGELOG.md | 8 ++++ chopper_generator/CHANGELOG.md | 4 ++ tool/publish.sh | 11 +++-- 7 files changed, 115 insertions(+), 30 deletions(-) create mode 100644 .github/workflows/publish_dry_run.yml delete mode 100644 chopper/.DS_Store diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index aea02d47..0eab9e8e 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -2,45 +2,58 @@ name: Publish packages on: push: - branches: ['master'] + branches: + - master +defaults: + run: + shell: bash +env: + PUB_ENVIRONMENT: bot.github +permissions: read-all jobs: publish_chopper: - name: "Publish chopper" - runs-on: ubuntu-latest - steps: - - uses: dart-lang/setup-dart@v1.3 + name: "Publish chopper" + runs-on: ubuntu-latest + steps: + - uses: dart-lang/setup-dart@v1 with: sdk: stable - id: checkout uses: actions/checkout@v3 + - id: credentials + run: | + mkdir -p $XDG_CONFIG_HOME/dart + echo '${{ secrets.PUB_CREDENTIALS }}' > "$XDG_CONFIG_HOME/dart/pub-credentials.json" - id: publish run: bash tool/publish.sh chopper - env: - CREDENTIAL_JSON: ${{ secrets.CREDENTIAL_JSON }} publish_chopper_generator: name: "Publish chopper_generator" runs-on: ubuntu-latest steps: - - uses: dart-lang/setup-dart@v1.3 - with: - sdk: stable - - id: checkout - uses: actions/checkout@v3 - - id: publish - run: bash tool/publish.sh chopper_generator - env: - CREDENTIAL_JSON: ${{ secrets.CREDENTIAL_JSON }} + - uses: dart-lang/setup-dart@v1 + with: + sdk: stable + - id: checkout + uses: actions/checkout@v3 + - id: credentials + run: | + mkdir -p $XDG_CONFIG_HOME/dart + echo '${{ secrets.PUB_CREDENTIALS }}' > "$XDG_CONFIG_HOME/dart/pub-credentials.json" + - id: publish + run: bash tool/publish.sh chopper_generator publish_chopper_built_value: name: "Publish chopper_built_value" runs-on: ubuntu-latest steps: - - uses: dart-lang/setup-dart@v1.3 - with: - sdk: stable - - id: checkout - uses: actions/checkout@v3 - - id: publish - run: bash tool/publish.sh chopper_built_value - env: - CREDENTIAL_JSON: ${{ secrets.CREDENTIAL_JSON }} \ No newline at end of file + - uses: dart-lang/setup-dart@v1 + with: + sdk: stable + - id: checkout + uses: actions/checkout@v3 + - id: credentials + run: | + mkdir -p $XDG_CONFIG_HOME/dart + echo '${{ secrets.PUB_CREDENTIALS }}' > "$XDG_CONFIG_HOME/dart/pub-credentials.json" + - id: publish + run: bash tool/publish.sh chopper_built_value \ No newline at end of file diff --git a/.github/workflows/publish_dry_run.yml b/.github/workflows/publish_dry_run.yml new file mode 100644 index 00000000..4e7fca02 --- /dev/null +++ b/.github/workflows/publish_dry_run.yml @@ -0,0 +1,47 @@ +name: Publish packages (dry run) + +on: + pull_request: + branches: + - master +defaults: + run: + shell: bash +env: + PUB_ENVIRONMENT: bot.github +permissions: read-all + +jobs: + publish_chopper: + name: "Publish chopper (dry run)" + runs-on: ubuntu-latest + steps: + - uses: dart-lang/setup-dart@v1 + with: + sdk: stable + - id: checkout + uses: actions/checkout@v3 + - id: publish_dry_run + run: bash tool/publish.sh chopper --dry-run + publish_chopper_generator: + name: "Publish chopper_generator (dry run)" + runs-on: ubuntu-latest + steps: + - uses: dart-lang/setup-dart@v1 + with: + sdk: stable + - id: checkout + uses: actions/checkout@v3 + - id: publish_dry_run + run: bash tool/publish.sh chopper_generator --dry-run + publish_chopper_built_value: + name: "Publish chopper_built_value (dry run)" + runs-on: ubuntu-latest + steps: + - uses: dart-lang/setup-dart@v1 + with: + sdk: stable + - id: checkout + uses: actions/checkout@v3 + - id: publish_dry_run + run: bash tool/publish.sh chopper_built_value --dry-run \ No newline at end of file diff --git a/chopper/.DS_Store b/chopper/.DS_Store deleted file mode 100644 index e8a2b199799712c46a4fa61772f920ea4f1144d0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHK%}T>S5dPK{+F}ny5IyGTso>d5S{t){ES`W4GT=EeayD3 zb>J^DAbWR=0iLM|vimp3l6yJh%mPi$4Df`co6LrBGK=XYugnGY?Ar=^KpmI3Mhkbi zLWCRK6PxrK75qL6en%KG(i&sDFwTV7N6g;H_M6OLjY)nD^Ilk^La$ew<(!p_^VT-g zk4&Z|+Ds7_tR%0jRfRQlSyRM{dWiDY=A>X<#~dr>y&#TR(H_>!SHZ7_2S#Y2gInS* zzR0twX0k7HZ^n$X%x47VpR;2i6=gsfPzH7|z%yHl#L#2vkRufR5wJ99qYV5i13&D&iRl0U diff --git a/chopper/CHANGELOG.md b/chopper/CHANGELOG.md index 32f0af43..8efd695f 100644 --- a/chopper/CHANGELOG.md +++ b/chopper/CHANGELOG.md @@ -1,4 +1,16 @@ # Changelog + +## 6.1.2 +- Packages upgrade, constraints upgrade + +## 6.1.1 +- EquatableMixin for Request, Response and PartValue + +## 6.1.0 + +- HttpLogging interceptor more configurable +- Apply headers field name case insensitive. + ## 6.0.0 - Replaced the String based path with Uri (BREAKING CHANGE) diff --git a/chopper_built_value/CHANGELOG.md b/chopper_built_value/CHANGELOG.md index e3a476bc..c78c9e07 100644 --- a/chopper_built_value/CHANGELOG.md +++ b/chopper_built_value/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## 1.2.1 + +- Packages upgrade, constraints upgrade + +## 1.2.0 + +- Chopper upgraded + ## 1.1.0 - Chopper upgraded diff --git a/chopper_generator/CHANGELOG.md b/chopper_generator/CHANGELOG.md index 39fc57cd..4fa0cf97 100644 --- a/chopper_generator/CHANGELOG.md +++ b/chopper_generator/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 6.0.1 + +- Packages upgrade, constraints upgrade + ## 6.0.0 - Replaced the String based path with Uri (BREAKING CHANGE) diff --git a/tool/publish.sh b/tool/publish.sh index 26340f52..f5b81db2 100644 --- a/tool/publish.sh +++ b/tool/publish.sh @@ -6,13 +6,14 @@ PKG=$1 echo -e "\033[1mPKG: ${PKG}\033[22m" pushd "${PKG}" -mkdir -p ~/.pub-cache - -echo $CREDENTIAL_JSON > ~/.pub-cache/credentials.json - sed '/Comment before publish$/,+2 d' pubspec.yaml > pubspec.temp.yaml rm pubspec.yaml mv pubspec.temp.yaml pubspec.yaml -dart pub publish -f +if [ "$2" == "--dry-run" ]; then + dart pub publish --dry-run +else + dart pub publish --force +fi + popd \ No newline at end of file From 64bd220ed52c6fcad01344318f8185a5ee5cd68a Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Sun, 14 May 2023 07:38:48 +0100 Subject: [PATCH 057/171] :green_heart: Fix CI publish credentials (#421) --- .github/workflows/publish.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 0eab9e8e..1ed7dbc0 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -24,7 +24,7 @@ jobs: - id: credentials run: | mkdir -p $XDG_CONFIG_HOME/dart - echo '${{ secrets.PUB_CREDENTIALS }}' > "$XDG_CONFIG_HOME/dart/pub-credentials.json" + echo '${{ secrets.CREDENTIAL_JSON }}' > "$XDG_CONFIG_HOME/dart/pub-credentials.json" - id: publish run: bash tool/publish.sh chopper publish_chopper_generator: @@ -39,7 +39,7 @@ jobs: - id: credentials run: | mkdir -p $XDG_CONFIG_HOME/dart - echo '${{ secrets.PUB_CREDENTIALS }}' > "$XDG_CONFIG_HOME/dart/pub-credentials.json" + echo '${{ secrets.CREDENTIAL_JSON }}' > "$XDG_CONFIG_HOME/dart/pub-credentials.json" - id: publish run: bash tool/publish.sh chopper_generator publish_chopper_built_value: @@ -54,6 +54,6 @@ jobs: - id: credentials run: | mkdir -p $XDG_CONFIG_HOME/dart - echo '${{ secrets.PUB_CREDENTIALS }}' > "$XDG_CONFIG_HOME/dart/pub-credentials.json" + echo '${{ secrets.CREDENTIAL_JSON }}' > "$XDG_CONFIG_HOME/dart/pub-credentials.json" - id: publish run: bash tool/publish.sh chopper_built_value \ No newline at end of file From 09774f1524f10069cd8dc91fbbbd6273bbaf0bea Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Sun, 14 May 2023 17:51:36 +0100 Subject: [PATCH 058/171] :green_heart: Fix CI coverage reporting (#423) --- .github/workflows/dart.yml | 59 ++++++++++++++----------------- chopper/mono_pkg.yaml | 2 +- chopper_built_value/mono_pkg.yaml | 2 +- mono_repo.yaml | 30 +++++----------- tool/ci.sh | 10 +++--- tool/coverage.sh | 11 ------ 6 files changed, 42 insertions(+), 72 deletions(-) delete mode 100755 tool/coverage.sh diff --git a/.github/workflows/dart.yml b/.github/workflows/dart.yml index 8c57149d..444801b6 100644 --- a/.github/workflows/dart.yml +++ b/.github/workflows/dart.yml @@ -127,14 +127,14 @@ jobs: - job_001 - job_002 job_004: - name: "unit_test; PKGS: chopper, chopper_built_value; `dart test -p chrome`" + name: "unit_test; PKGS: chopper, chopper_built_value; `dart pub global run coverage:test_with_coverage`" runs-on: ubuntu-latest steps: - name: Cache Pub hosted dependencies uses: actions/cache@88522ab9f39a2ea568f7027eddc7d8d8bc9d59c8 with: path: "~/.pub-cache/hosted" - key: "os:ubuntu-latest;pub-cache-hosted;sdk:stable;packages:chopper-chopper_built_value;commands:test_1" + key: "os:ubuntu-latest;pub-cache-hosted;sdk:stable;packages:chopper-chopper_built_value;commands:test_with_coverage" restore-keys: | os:ubuntu-latest;pub-cache-hosted;sdk:stable;packages:chopper-chopper_built_value os:ubuntu-latest;pub-cache-hosted;sdk:stable @@ -144,6 +144,8 @@ jobs: uses: dart-lang/setup-dart@d6a63dab3335f427404425de0fbfed4686d93c4f with: sdk: stable + - name: "Activate package:coverage" + run: "dart pub global activate coverage '>=1.5.0'" - id: checkout name: Checkout repository uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab @@ -152,32 +154,44 @@ jobs: run: dart pub upgrade if: "always() && steps.checkout.conclusion == 'success'" working-directory: chopper - - name: "chopper; dart test -p chrome" - run: dart test -p chrome + - name: "chopper; dart pub global run coverage:test_with_coverage" + run: "dart pub global run coverage:test_with_coverage" if: "always() && steps.chopper_pub_upgrade.conclusion == 'success'" working-directory: chopper + - name: Upload coverage to codecov.io + uses: codecov/codecov-action@main + with: + files: chopper/coverage/lcov.info + fail_ci_if_error: true + name: coverage_00 - id: chopper_built_value_pub_upgrade name: chopper_built_value; dart pub upgrade run: dart pub upgrade if: "always() && steps.checkout.conclusion == 'success'" working-directory: chopper_built_value - - name: "chopper_built_value; dart test -p chrome" - run: dart test -p chrome + - name: "chopper_built_value; dart pub global run coverage:test_with_coverage" + run: "dart pub global run coverage:test_with_coverage" if: "always() && steps.chopper_built_value_pub_upgrade.conclusion == 'success'" working-directory: chopper_built_value + - name: Upload coverage to codecov.io + uses: codecov/codecov-action@main + with: + files: chopper_built_value/coverage/lcov.info + fail_ci_if_error: true + name: coverage_01 needs: - job_001 - job_002 - job_003 job_005: - name: "unit_test; PKGS: chopper, chopper_built_value; `dart test`" + name: "unit_test; PKGS: chopper, chopper_built_value; `dart test -p chrome`" runs-on: ubuntu-latest steps: - name: Cache Pub hosted dependencies uses: actions/cache@88522ab9f39a2ea568f7027eddc7d8d8bc9d59c8 with: path: "~/.pub-cache/hosted" - key: "os:ubuntu-latest;pub-cache-hosted;sdk:stable;packages:chopper-chopper_built_value;commands:test_0" + key: "os:ubuntu-latest;pub-cache-hosted;sdk:stable;packages:chopper-chopper_built_value;commands:test" restore-keys: | os:ubuntu-latest;pub-cache-hosted;sdk:stable;packages:chopper-chopper_built_value os:ubuntu-latest;pub-cache-hosted;sdk:stable @@ -195,8 +209,8 @@ jobs: run: dart pub upgrade if: "always() && steps.checkout.conclusion == 'success'" working-directory: chopper - - name: chopper; dart test - run: dart test + - name: "chopper; dart test -p chrome" + run: dart test -p chrome if: "always() && steps.chopper_pub_upgrade.conclusion == 'success'" working-directory: chopper - id: chopper_built_value_pub_upgrade @@ -204,32 +218,11 @@ jobs: run: dart pub upgrade if: "always() && steps.checkout.conclusion == 'success'" working-directory: chopper_built_value - - name: chopper_built_value; dart test - run: dart test + - name: "chopper_built_value; dart test -p chrome" + run: dart test -p chrome if: "always() && steps.chopper_built_value_pub_upgrade.conclusion == 'success'" working-directory: chopper_built_value needs: - job_001 - job_002 - job_003 - job_006: - name: Coverage - runs-on: ubuntu-latest - steps: - - uses: dart-lang/setup-dart@v1.3 - with: - sdk: stable - - id: checkout - uses: actions/checkout@v3 - - id: upload_coverage - name: chopper; tool/coverage.sh - run: bash tool/coverage.sh - if: "always() && steps.checkout.conclusion == 'success'" - env: - CODECOV_TOKEN: "${{ secrets.CODECOV_TOKEN }}" - needs: - - job_001 - - job_002 - - job_003 - - job_004 - - job_005 diff --git a/chopper/mono_pkg.yaml b/chopper/mono_pkg.yaml index ed853726..8cce50b9 100644 --- a/chopper/mono_pkg.yaml +++ b/chopper/mono_pkg.yaml @@ -7,7 +7,7 @@ stages: - format - analyze: --fatal-infos . - unit_test: - - test: + - test_with_coverage: - test: -p chrome cache: diff --git a/chopper_built_value/mono_pkg.yaml b/chopper_built_value/mono_pkg.yaml index 3d4d539a..ae7b5f25 100644 --- a/chopper_built_value/mono_pkg.yaml +++ b/chopper_built_value/mono_pkg.yaml @@ -7,7 +7,7 @@ stages: - format - analyze: --fatal-infos . - unit_test: - - test: + - test_with_coverage: - test: -p chrome cache: diff --git a/mono_repo.yaml b/mono_repo.yaml index 08251824..28e29828 100644 --- a/mono_repo.yaml +++ b/mono_repo.yaml @@ -4,28 +4,16 @@ github: on: push: branches: - - master - - develop + - master + - develop pull_request: branches: - - master - - develop - on_completion: - - name: "Coverage" - runs-on: ubuntu-latest - steps: - - uses: dart-lang/setup-dart@v1.3 - with: - sdk: stable - - id: checkout - uses: actions/checkout@v3 - - id: upload_coverage - name: "chopper; tool/coverage.sh" - if: "always() && steps.checkout.conclusion == 'success'" - run: bash tool/coverage.sh - env: - CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + - master + - develop merge_stages: -- analyzer_and_format -- unit_test \ No newline at end of file + - analyzer_and_format + - unit_test + +coverage_service: + - codecov \ No newline at end of file diff --git a/tool/ci.sh b/tool/ci.sh index ca07f3a6..7449806d 100755 --- a/tool/ci.sh +++ b/tool/ci.sh @@ -75,14 +75,14 @@ for PKG in ${PKGS}; do echo 'dart format --output=none --set-exit-if-changed .' dart format --output=none --set-exit-if-changed . || EXIT_CODE=$? ;; - test_0) - echo 'dart test' - dart test || EXIT_CODE=$? - ;; - test_1) + test) echo 'dart test -p chrome' dart test -p chrome || EXIT_CODE=$? ;; + test_with_coverage) + echo 'dart pub global run coverage:test_with_coverage' + dart pub global run coverage:test_with_coverage || EXIT_CODE=$? + ;; *) echo -e "\033[31mUnknown TASK '${TASK}' - TERMINATING JOB\033[0m" exit 64 diff --git a/tool/coverage.sh b/tool/coverage.sh deleted file mode 100755 index 8fb7f288..00000000 --- a/tool/coverage.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/bash - -dart pub get -dart run test --coverage=coverage -dart run coverage:format_coverage --lcov \ - --in=coverage \ - --out=coverage/coverage.lcov \ - --packages=.packages \ - --report-on=lib - -curl -s https://codecov.io/bash | bash \ No newline at end of file From 01450641b67d16dc4654e87e55b13dd7686f23a4 Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Mon, 15 May 2023 06:18:19 +0100 Subject: [PATCH 059/171] :construction_worker: only run CI publish / --dry-run when a package's version changes (#424) --- .github/workflows/publish.yml | 77 +++++++++++++++++---------- .github/workflows/publish_dry_run.yml | 64 +++++++++++++++------- tool/compare_versions.dart | 38 +++++++++++++ tool/pubspec.yaml | 12 +++++ 4 files changed, 146 insertions(+), 45 deletions(-) create mode 100644 tool/compare_versions.dart create mode 100644 tool/pubspec.yaml diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 1ed7dbc0..5d582264 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1,7 +1,7 @@ name: Publish packages on: - push: + pull_request: branches: - master defaults: @@ -12,48 +12,71 @@ env: permissions: read-all jobs: - publish_chopper: - name: "Publish chopper" + get_base_version: + name: "Get base version" runs-on: ubuntu-latest + strategy: + matrix: + package: [ chopper, chopper_generator, chopper_built_value ] + outputs: + BASE_VERSION_chopper: ${{ steps.load_base_version.outputs.BASE_VERSION_chopper }} + BASE_VERSION_chopper_generator: ${{ steps.load_base_version.outputs.BASE_VERSION_chopper_generator }} + BASE_VERSION_chopper_built_value: ${{ steps.load_base_version.outputs.BASE_VERSION_chopper_built_value }} steps: - uses: dart-lang/setup-dart@v1 with: sdk: stable - id: checkout uses: actions/checkout@v3 - - id: credentials - run: | - mkdir -p $XDG_CONFIG_HOME/dart - echo '${{ secrets.CREDENTIAL_JSON }}' > "$XDG_CONFIG_HOME/dart/pub-credentials.json" - - id: publish - run: bash tool/publish.sh chopper - publish_chopper_generator: - name: "Publish chopper_generator" - runs-on: ubuntu-latest - steps: - - uses: dart-lang/setup-dart@v1 with: - sdk: stable - - id: checkout - uses: actions/checkout@v3 - - id: credentials + ref: ${{ github.event.pull_request.base.ref }} + - name: Load base version + id: load_base_version run: | - mkdir -p $XDG_CONFIG_HOME/dart - echo '${{ secrets.CREDENTIAL_JSON }}' > "$XDG_CONFIG_HOME/dart/pub-credentials.json" - - id: publish - run: bash tool/publish.sh chopper_generator - publish_chopper_built_value: - name: "Publish chopper_built_value" + set -e + echo "BASE_VERSION_${{ matrix.package }}=$(awk '/^version: / {print $2}' ${{ matrix.package }}/pubspec.yaml)" >> $GITHUB_OUTPUT + publish: + name: "Publish" + needs: get_base_version runs-on: ubuntu-latest + strategy: + matrix: + package: [ chopper, chopper_generator, chopper_built_value ] steps: - uses: dart-lang/setup-dart@v1 with: sdk: stable - id: checkout uses: actions/checkout@v3 - - id: credentials + - name: Load this version + id: load_this_version + run: | + set -e + echo "THIS_VERSION=$(awk '/^version: / {print $2}' ${{ matrix.package }}/pubspec.yaml)" >> $GITHUB_ENV + - name: Compare versions + id: compare_versions + env: + BASE_VERSION_chopper: ${{ needs.get_base_version.outputs.BASE_VERSION_chopper }} + BASE_VERSION_chopper_generator: ${{ needs.get_base_version.outputs.BASE_VERSION_chopper_generator }} + BASE_VERSION_chopper_built_value: ${{ needs.get_base_version.outputs.BASE_VERSION_chopper_built_value }} + run: | + set -e + pushd tool || exit + dart pub get + echo "IS_VERSION_GREATER=$(dart run compare_versions.dart $THIS_VERSION $BASE_VERSION_${{ matrix.package }})" >> $GITHUB_ENV + popd || exit + - name: Set up pub credentials + id: credentials + if: ${{ env.IS_VERSION_GREATER == 1 }} run: | + set -e mkdir -p $XDG_CONFIG_HOME/dart echo '${{ secrets.CREDENTIAL_JSON }}' > "$XDG_CONFIG_HOME/dart/pub-credentials.json" - - id: publish - run: bash tool/publish.sh chopper_built_value \ No newline at end of file + - name: Publish + id: publish + if: ${{ env.IS_VERSION_GREATER == 1 }} + run: bash tool/publish.sh ${{ matrix.package }} + - name: Skip publish + id: skip_publish + if: ${{ env.IS_VERSION_GREATER == 0 }} + run: echo "Skipping publish for ${{ matrix.package }} because the version is not greater than the one on pub.dev" \ No newline at end of file diff --git a/.github/workflows/publish_dry_run.yml b/.github/workflows/publish_dry_run.yml index 4e7fca02..b16ef582 100644 --- a/.github/workflows/publish_dry_run.yml +++ b/.github/workflows/publish_dry_run.yml @@ -12,36 +12,64 @@ env: permissions: read-all jobs: - publish_chopper: - name: "Publish chopper (dry run)" + get_base_version: + name: "Get base version" runs-on: ubuntu-latest + strategy: + matrix: + package: [ chopper, chopper_generator, chopper_built_value ] + outputs: + BASE_VERSION_chopper: ${{ steps.load_base_version.outputs.BASE_VERSION_chopper }} + BASE_VERSION_chopper_generator: ${{ steps.load_base_version.outputs.BASE_VERSION_chopper_generator }} + BASE_VERSION_chopper_built_value: ${{ steps.load_base_version.outputs.BASE_VERSION_chopper_built_value }} steps: - uses: dart-lang/setup-dart@v1 with: sdk: stable - id: checkout uses: actions/checkout@v3 - - id: publish_dry_run - run: bash tool/publish.sh chopper --dry-run - publish_chopper_generator: - name: "Publish chopper_generator (dry run)" - runs-on: ubuntu-latest - steps: - - uses: dart-lang/setup-dart@v1 with: - sdk: stable - - id: checkout - uses: actions/checkout@v3 - - id: publish_dry_run - run: bash tool/publish.sh chopper_generator --dry-run - publish_chopper_built_value: - name: "Publish chopper_built_value (dry run)" + ref: ${{ github.event.pull_request.base.ref }} + - name: Load base version + id: load_base_version + run: | + set -e + echo "BASE_VERSION_${{ matrix.package }}=$(awk '/^version: / {print $2}' ${{ matrix.package }}/pubspec.yaml)" >> $GITHUB_OUTPUT + publish_dry_run: + name: "Publish DRY RUN" + needs: get_base_version runs-on: ubuntu-latest + strategy: + matrix: + package: [ chopper, chopper_generator, chopper_built_value ] steps: - uses: dart-lang/setup-dart@v1 with: sdk: stable - id: checkout uses: actions/checkout@v3 - - id: publish_dry_run - run: bash tool/publish.sh chopper_built_value --dry-run \ No newline at end of file + - name: Load this version + id: load_this_version + run: | + set -e + echo "THIS_VERSION=$(awk '/^version: / {print $2}' ${{ matrix.package }}/pubspec.yaml)" >> $GITHUB_ENV + - name: Compare versions + id: compare_versions + env: + BASE_VERSION_chopper: ${{ needs.get_base_version.outputs.BASE_VERSION_chopper }} + BASE_VERSION_chopper_generator: ${{ needs.get_base_version.outputs.BASE_VERSION_chopper_generator }} + BASE_VERSION_chopper_built_value: ${{ needs.get_base_version.outputs.BASE_VERSION_chopper_built_value }} + run: | + set -e + pushd tool || exit + dart pub get + echo "IS_VERSION_GREATER=$(dart run compare_versions.dart $THIS_VERSION $BASE_VERSION_${{ matrix.package }})" >> $GITHUB_ENV + popd || exit + - name: Publish (dry run) + id: publish_dry_run + if: ${{ env.IS_VERSION_GREATER == 1 }} + run: bash tool/publish.sh ${{ matrix.package }} --dry-run + - name: Skip publish (dry run) + id: skip_publish_dry_run + if: ${{ env.IS_VERSION_GREATER == 0 }} + run: echo "Skipping publish (dry run) for ${{ matrix.package }} because the version is not greater than the one on pub.dev" \ No newline at end of file diff --git a/tool/compare_versions.dart b/tool/compare_versions.dart new file mode 100644 index 00000000..1f4d03c8 --- /dev/null +++ b/tool/compare_versions.dart @@ -0,0 +1,38 @@ +import 'dart:io' show exitCode, stderr, stdout; +import 'package:cli_script/cli_script.dart' show wrapMain; +import 'package:pub_semver/pub_semver.dart' show Version; + +void main(List args) { + wrapMain(() { + exitCode = 0; + + if (args.length != 2) { + stderr.write( + 'Please provide two arguments!\n\nExample usage:\ndart run compare_versions.dart 2.0.0+1 1.9.0+5\n', + ); + exitCode = 1; + return; + } + + late final Version v1; + late final Version v2; + + try { + v1 = Version.parse(args[0]); + } on FormatException catch (e) { + stderr.write('Error parsing version 1: ${e.message}'); + exitCode = 1; + return; + } + + try { + v2 = Version.parse(args[1]); + } on FormatException catch (e) { + stderr.write('Error parsing version 2: ${e.message}'); + exitCode = 1; + return; + } + + stdout.write(v1 > v2 ? 1 : 0); + }); +} diff --git a/tool/pubspec.yaml b/tool/pubspec.yaml new file mode 100644 index 00000000..5ad9b46a --- /dev/null +++ b/tool/pubspec.yaml @@ -0,0 +1,12 @@ +name: compare_versions + +publish_to: 'none' + +version: 1.0.0 + +environment: + sdk: ">=2.17.0 <4.0.0" + +dependencies: + cli_script: ^0.3.1 + pub_semver: ^2.1.4 \ No newline at end of file From ef2fbf59dca8e79eb39c13f16743c66aef4f4c9e Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Mon, 15 May 2023 06:48:14 +0100 Subject: [PATCH 060/171] :green_heart: fix publish CI to run only on push (#425) --- .github/workflows/publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 5d582264..8591699a 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1,7 +1,7 @@ name: Publish packages on: - pull_request: + push: branches: - master defaults: From 08781c999565ec5f1e562f74c47fcdb0acd256e2 Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Sat, 20 May 2023 12:52:00 +0100 Subject: [PATCH 061/171] :green_heart: fix publish workflow (#427) * :green_heart: fix publish workflow * :rotating_light: silence deprecation warning --- .github/workflows/publish.yml | 3 ++- chopper_generator/lib/src/generator.dart | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 8591699a..e29d5e21 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -29,7 +29,8 @@ jobs: - id: checkout uses: actions/checkout@v3 with: - ref: ${{ github.event.pull_request.base.ref }} + fetch-depth: 2 + - run: git checkout HEAD^ - name: Load base version id: load_base_version run: | diff --git a/chopper_generator/lib/src/generator.dart b/chopper_generator/lib/src/generator.dart index cce54040..72635a75 100644 --- a/chopper_generator/lib/src/generator.dart +++ b/chopper_generator/lib/src/generator.dart @@ -457,6 +457,7 @@ class ChopperGenerator extends GeneratorForAnnotation { _typeChecker(Map).isExactlyType(type) || _typeChecker(BuiltMap).isExactlyType(type)) return type; + // ignore: deprecated_member_use if (generic.isDynamic) return null; if (_typeChecker(List).isExactlyType(type) || From 485ef8fdfd04638e4ab617724d00ca606857f90a Mon Sep 17 00:00:00 2001 From: Joran Dob Date: Fri, 26 May 2023 11:23:22 +0200 Subject: [PATCH 062/171] Adding follow redirects to toHttpRequest (#430) --- chopper/lib/src/request.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/chopper/lib/src/request.dart b/chopper/lib/src/request.dart index e4ecfdb6..7e0b21f2 100644 --- a/chopper/lib/src/request.dart +++ b/chopper/lib/src/request.dart @@ -138,6 +138,7 @@ class Request extends http.BaseRequest with EquatableMixin { @visibleForTesting http.Request toHttpRequest() { final http.Request request = http.Request(method, url) + ..followRedirects = followRedirects ..headers.addAll(headers); if (body != null) { From a88aa3a495009261c0abd01b0fe4b22efd26d66a Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Sat, 27 May 2023 09:03:18 +0100 Subject: [PATCH 063/171] :arrow_up: update squadron example (#432) --- example/build.yaml | 6 +- example/lib/json_decode_service.vm.g.dart | 2 +- example/lib/json_decode_service.worker.g.dart | 226 +++++++++++++++++- example/pubspec.yaml | 4 +- 4 files changed, 230 insertions(+), 8 deletions(-) diff --git a/example/build.yaml b/example/build.yaml index d0fb37db..b8099626 100644 --- a/example/build.yaml +++ b/example/build.yaml @@ -13,4 +13,8 @@ targets: any_map: false checked: false explicit_to_json: true - create_to_json: true \ No newline at end of file + create_to_json: true + squadron_builder:worker_builder: + options: + with_finalizers: true + serialization_type: List \ No newline at end of file diff --git a/example/lib/json_decode_service.vm.g.dart b/example/lib/json_decode_service.vm.g.dart index e798c22e..656bcef1 100644 --- a/example/lib/json_decode_service.vm.g.dart +++ b/example/lib/json_decode_service.vm.g.dart @@ -8,6 +8,6 @@ import 'package:squadron/squadron_service.dart'; import 'json_decode_service.dart'; // VM entry point -void _start(Map command) => run($JsonDecodeServiceInitializer, command, null); +void _start(List command) => run($JsonDecodeServiceInitializer, command, null); dynamic $getJsonDecodeServiceActivator() => _start; diff --git a/example/lib/json_decode_service.worker.g.dart b/example/lib/json_decode_service.worker.g.dart index 0119d302..0cd306ce 100644 --- a/example/lib/json_decode_service.worker.g.dart +++ b/example/lib/json_decode_service.worker.g.dart @@ -23,10 +23,10 @@ JsonDecodeService $JsonDecodeServiceInitializer(WorkerRequest startRequest) => JsonDecodeService(); // Worker for JsonDecodeService -class JsonDecodeServiceWorker extends Worker +class _JsonDecodeServiceWorker extends Worker with $JsonDecodeServiceOperations implements JsonDecodeService { - JsonDecodeServiceWorker() : super($JsonDecodeServiceActivator); + _JsonDecodeServiceWorker() : super($JsonDecodeServiceActivator); @override Future jsonDecode(String source) => send( @@ -36,13 +36,109 @@ class JsonDecodeServiceWorker extends Worker @override Map get operations => WorkerService.noOperations; + + final Object _detachToken = Object(); +} + +// Finalizable worker wrapper for JsonDecodeService +class JsonDecodeServiceWorker implements _JsonDecodeServiceWorker { + JsonDecodeServiceWorker() : _worker = _JsonDecodeServiceWorker() { + _finalizer.attach(this, _worker, detach: _worker._detachToken); + } + + final _JsonDecodeServiceWorker _worker; + + static final Finalizer<_JsonDecodeServiceWorker> _finalizer = + Finalizer<_JsonDecodeServiceWorker>((w) { + try { + _finalizer.detach(w._detachToken); + w.stop(); + } catch (ex) { + // finalizers must not throw + } + }); + + @override + Future jsonDecode(String source) => _worker.jsonDecode(source); + + @override + Map get operations => _worker.operations; + + @override + List get args => _worker.args; + + @override + Channel? get channel => _worker.channel; + + @override + Duration get idleTime => _worker.idleTime; + + @override + bool get isStopped => _worker.isStopped; + + @override + int get maxWorkload => _worker.maxWorkload; + + @override + WorkerStat get stats => _worker.stats; + + @override + String get status => _worker.status; + + @override + int get totalErrors => _worker.totalErrors; + + @override + int get totalWorkload => _worker.totalWorkload; + + @override + Duration get upTime => _worker.upTime; + + @override + String get workerId => _worker.workerId; + + @override + int get workload => _worker.workload; + + @override + Future start() => _worker.start(); + + @override + void stop() => _worker.stop(); + + @override + Future send(int command, + {List args = const [], + CancellationToken? token, + bool inspectRequest = false, + bool inspectResponse = false}) => + _worker.send(command, + args: args, + token: token, + inspectRequest: inspectRequest, + inspectResponse: inspectResponse); + + @override + Stream stream(int command, + {List args = const [], + CancellationToken? token, + bool inspectRequest = false, + bool inspectResponse = false}) => + _worker.stream(command, + args: args, + token: token, + inspectRequest: inspectRequest, + inspectResponse: inspectResponse); + + @override + Object get _detachToken => _worker._detachToken; } // Worker pool for JsonDecodeService -class JsonDecodeServiceWorkerPool extends WorkerPool +class _JsonDecodeServiceWorkerPool extends WorkerPool with $JsonDecodeServiceOperations implements JsonDecodeService { - JsonDecodeServiceWorkerPool({ConcurrencySettings? concurrencySettings}) + _JsonDecodeServiceWorkerPool({ConcurrencySettings? concurrencySettings}) : super(() => JsonDecodeServiceWorker(), concurrencySettings: concurrencySettings); @@ -52,4 +148,126 @@ class JsonDecodeServiceWorkerPool extends WorkerPool @override Map get operations => WorkerService.noOperations; + + final Object _detachToken = Object(); +} + +// Finalizable worker pool wrapper for JsonDecodeService +class JsonDecodeServiceWorkerPool implements _JsonDecodeServiceWorkerPool { + JsonDecodeServiceWorkerPool({ConcurrencySettings? concurrencySettings}) + : _pool = _JsonDecodeServiceWorkerPool( + concurrencySettings: concurrencySettings) { + _finalizer.attach(this, _pool, detach: _pool._detachToken); + } + + final _JsonDecodeServiceWorkerPool _pool; + + static final Finalizer<_JsonDecodeServiceWorkerPool> _finalizer = + Finalizer<_JsonDecodeServiceWorkerPool>((p) { + try { + _finalizer.detach(p._detachToken); + p.stop(); + } catch (ex) { + // finalizers must not throw + } + }); + + @override + Future jsonDecode(String source) => _pool.jsonDecode(source); + + @override + Map get operations => _pool.operations; + + @override + ConcurrencySettings get concurrencySettings => _pool.concurrencySettings; + + @override + Iterable get fullStats => _pool.fullStats; + + @override + int get maxConcurrency => _pool.maxConcurrency; + + @override + int get maxParallel => _pool.maxParallel; + + @override + int get maxSize => _pool.maxSize; + + @override + int get maxWorkers => _pool.maxWorkers; + + @override + int get maxWorkload => _pool.maxWorkload; + + @override + int get minWorkers => _pool.minWorkers; + + @override + int get pendingWorkload => _pool.pendingWorkload; + + @override + int get size => _pool.size; + + @override + Iterable get stats => _pool.stats; + + @override + bool get stopped => _pool.stopped; + + @override + int get totalErrors => _pool.totalErrors; + + @override + int get totalWorkload => _pool.totalWorkload; + + @override + int get workload => _pool.workload; + + @override + void cancel([Task? task, String? message]) => _pool.cancel(task, message); + + @override + FutureOr start() => _pool.start(); + + @override + int stop([bool Function(JsonDecodeServiceWorker worker)? predicate]) => + _pool.stop(predicate); + + @override + Object registerWorkerPoolListener( + void Function(JsonDecodeServiceWorker worker, bool removed) + listener) => + _pool.registerWorkerPoolListener(listener); + + @override + void unregisterWorkerPoolListener( + {void Function(JsonDecodeServiceWorker worker, bool removed)? + listener, + Object? token}) => + _pool.unregisterWorkerPoolListener(listener: listener, token: token); + + @override + Future execute(Future Function(JsonDecodeServiceWorker worker) task, + {PerfCounter? counter}) => + _pool.execute(task, counter: counter); + + @override + StreamTask scheduleStream( + Stream Function(JsonDecodeServiceWorker worker) task, + {PerfCounter? counter}) => + _pool.scheduleStream(task, counter: counter); + + @override + ValueTask scheduleTask( + Future Function(JsonDecodeServiceWorker worker) task, + {PerfCounter? counter}) => + _pool.scheduleTask(task, counter: counter); + + @override + Stream stream(Stream Function(JsonDecodeServiceWorker worker) task, + {PerfCounter? counter}) => + _pool.stream(task, counter: counter); + + @override + Object get _detachToken => _pool._detachToken; } diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 963f23c9..5a1e090d 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -14,7 +14,7 @@ dependencies: analyzer: http: built_collection: - squadron: ^4.3.8 + squadron: ^5.0.0 dev_dependencies: build_runner: @@ -23,7 +23,7 @@ dev_dependencies: built_value_generator: dart_code_metrics: '>=4.8.1 <6.0.0' lints: ^2.0.0 - squadron_builder: ^2.0.0 + squadron_builder: ^2.1.2 dependency_overrides: chopper: From 05464527a2835fbc52d268ac8e8ff50a9e2b052e Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Sat, 27 May 2023 09:07:34 +0100 Subject: [PATCH 064/171] :arrow_up: update http constraint to ">=0.13.0 <2.0.0" (#431) --- chopper/pubspec.yaml | 2 +- chopper_built_value/pubspec.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/chopper/pubspec.yaml b/chopper/pubspec.yaml index f6cfea6a..6ef71aad 100644 --- a/chopper/pubspec.yaml +++ b/chopper/pubspec.yaml @@ -9,7 +9,7 @@ environment: dependencies: equatable: ^2.0.5 - http: ">=0.13.0 <1.0.0" + http: ">=0.13.0 <2.0.0" logging: ^1.0.0 meta: ^1.3.0 diff --git a/chopper_built_value/pubspec.yaml b/chopper_built_value/pubspec.yaml index 1f536048..9d9a208e 100644 --- a/chopper_built_value/pubspec.yaml +++ b/chopper_built_value/pubspec.yaml @@ -11,7 +11,7 @@ dependencies: built_value: ^8.0.0 built_collection: ^5.0.0 chopper: ^6.0.0 - http: ^0.13.0 + http: ">=0.13.0 <2.0.0" dev_dependencies: test: ^1.16.4 From ae8bff5ec2aa819f8b3ed86d8ffaa0df1195ae8c Mon Sep 17 00:00:00 2001 From: "Joseph, NamKung" Date: Sun, 28 May 2023 08:16:40 +0900 Subject: [PATCH 065/171] Add MultipartRequest log to CurlInterceptor (#435) --- chopper/lib/src/interceptor.dart | 10 ++++++++++ chopper/test/interceptors_test.dart | 29 +++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/chopper/lib/src/interceptor.dart b/chopper/lib/src/interceptor.dart index 0e123237..b8595cea 100644 --- a/chopper/lib/src/interceptor.dart +++ b/chopper/lib/src/interceptor.dart @@ -151,6 +151,16 @@ class CurlInterceptor implements RequestInterceptor { curl += ' -d \'$body\''; } } + if (baseRequest is http.MultipartRequest) { + final fields = baseRequest.fields; + final files = baseRequest.files; + fields.forEach((k, v) { + curl += ' -f \'$k: $v\''; + }); + for (var file in files) { + curl += ' -f \'${file.field}: ${file.filename ?? ''}\''; + } + } curl += ' "$url"'; chopperLogger.info(curl); diff --git a/chopper/test/interceptors_test.dart b/chopper/test/interceptors_test.dart index 0584dcc5..3ee651d0 100644 --- a/chopper/test/interceptors_test.dart +++ b/chopper/test/interceptors_test.dart @@ -204,6 +204,35 @@ void main() { ), ); }); + + final fakeRequestMultipart = Request( + 'POST', + Uri.parse('/'), + Uri.parse('base'), + headers: {'foo': 'bar'}, + parts: [ + PartValue('p1', 123), + PartValueFile( + 'p2', + http.MultipartFile.fromBytes('file', [0], filename: 'filename'), + ), + ], + multipart: true, + ); + + test('Curl interceptors Multipart', () async { + final curl = CurlInterceptor(); + var log = ''; + chopperLogger.onRecord.listen((r) => log = r.message); + await curl.onRequest(fakeRequestMultipart); + + expect( + log, + equals( + "curl -v -X POST -H 'foo: bar' -f 'p1: 123' -f 'file: filename' \"base/\"", + ), + ); + }); }); } From d824ded15a9e5f83960bb57f438af53e2e9b5869 Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Sun, 28 May 2023 08:55:54 +0100 Subject: [PATCH 066/171] :green_heart: add cleanup step to publish workflow (#434) --- .github/workflows/publish.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index e29d5e21..664fde42 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -80,4 +80,9 @@ jobs: - name: Skip publish id: skip_publish if: ${{ env.IS_VERSION_GREATER == 0 }} - run: echo "Skipping publish for ${{ matrix.package }} because the version is not greater than the one on pub.dev" \ No newline at end of file + run: echo "Skipping publish for ${{ matrix.package }} because the version is not greater than the one on pub.dev" + - name: Cleanup + id: cleanup + if: ${{ always() }} + run: | + rm -rf "$XDG_CONFIG_HOME/dart/pub-credentials.json" \ No newline at end of file From 829d5b0edd4986e3079fa81e67ebe9fbc836b6c2 Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Sun, 28 May 2023 10:21:41 +0100 Subject: [PATCH 067/171] :twisted_rightwards_arrows: post release branch sync (#437) --- chopper/CHANGELOG.md | 57 +++++++++++++++++++------------- chopper/pubspec.yaml | 2 +- chopper_built_value/CHANGELOG.md | 4 +++ chopper_built_value/pubspec.yaml | 2 +- chopper_generator/CHANGELOG.md | 49 ++++++++++++++------------- example/pubspec.yaml | 2 +- 6 files changed, 67 insertions(+), 49 deletions(-) diff --git a/chopper/CHANGELOG.md b/chopper/CHANGELOG.md index 8efd695f..e14d3af5 100644 --- a/chopper/CHANGELOG.md +++ b/chopper/CHANGELOG.md @@ -1,9 +1,17 @@ # Changelog +## 6.1.3 + +- Add follow redirects to toHttpRequest ([#430](https://github.com/lejard-h/chopper/pull/430)) +- Update http constraint to ">=0.13.0 <2.0.0" ([#431](https://github.com/lejard-h/chopper/pull/431)) +- Add MultipartRequest log to CurlInterceptor ([#435](https://github.com/lejard-h/chopper/pull/435)) + ## 6.1.2 + - Packages upgrade, constraints upgrade ## 6.1.1 + - EquatableMixin for Request, Response and PartValue ## 6.1.0 @@ -37,6 +45,7 @@ ## 4.0.1 - Fix for the null safety support + ## 4.0.0 - **Null safety support** @@ -73,15 +82,15 @@ **Breaking change** New way to handle errors - if (response.isSuccessful) { - final body = response.body; - } else { - final error = response.error; - } +if (response.isSuccessful) { +final body = response.body; +} else { +final error = response.error; +} + - Fix error handling by introducing `Response.error` getter - Remove `onError` since every response are available via `onResponse` stream - ## 2.5.0 - Unsuccessful response are not throw anymore, use `Response.isSuccessful` getter or `statusCode` instead @@ -90,8 +99,8 @@ New way to handle errors ## 2.4.2 - Fix on JsonConverter - If content type header overrided using @Post(headers: {'content-type': '...'}) - The converter won't add json header and won't apply json.encode if content type is not JSON + If content type header overrided using @Post(headers: {'content-type': '...'}) + The converter won't add json header and won't apply json.encode if content type is not JSON - add `bool override` on `applyHeader(s)` functions, true by default @@ -107,8 +116,9 @@ New way to handle errors `Response.base` is now a `BaseRequest` instead of a `Request`, which means that you can't do base.body now. Please use Response.bodyBytes or Response.bodyString instead for non streaming case. - Now supports streams ! - - You can pass `Stream>` as a body to a request - - You can also use `Stream>` as the BodyType for the response, in this case the returned response will contain a stream in `body`. + - You can pass `Stream>` as a body to a request + - You can also use `Stream>` as the BodyType for the response, in this case the returned response will + contain a stream in `body`. - Support passing `MutlipartFile` (from packages:http) directly to `@FileField` annotation ## 2.3.2 @@ -138,12 +148,12 @@ New way to handle errors ## 2.2.0 - Fix converter issue on List - - ***Breaking Change*** - on `Converter.convertResponse(response)`, - it take a new generic type => `Converter.convertResponse(response)` + - ***Breaking Change*** + on `Converter.convertResponse(response)`, + it take a new generic type => `Converter.convertResponse(response)` - deprecated `Chopper.service(Type)`, use `Chopper.getservice()` instead -thanks to @MichaelDark + thanks to @MichaelDark ## 2.1.0 @@ -159,30 +169,31 @@ thanks to @MichaelDark - Request is now containing baseUrl - Can call `Request.toHttpRequest()` direclty to get the `http.BaseRequest` will receive -- If a full url is specified in the `path` (ex: @Get(path: 'https://...')), it won't be concaten with the baseUrl of the ChopperClient and the ChopperAPI +- If a full url is specified in the `path` (ex: @Get(path: 'https://...')), it won't be concaten with the baseUrl of the + ChopperClient and the ChopperAPI - Add `CurlInterceptor` thanks @edwardaux - Add `HttpLoggingInterceptor` - Add `FactoryConverter` annotation `@FactoryConverter(request: convertRequest, response: convertResponse)` - ***BreakingChange*** - - Method.url renamed to path - - `Converter.encode` and `Converter.decode` removed, implement `Converter.convertResponse` and Converter.convertRequest` instead - - `ChopperClient.jsonApi` deprecated, use a `JsonConverter` instead - - `ChopperClient.formUrlEncodedApi`, use `FormUrlEncodedConverter` instead - - remove `JsonEncoded` annotation, use `FactoryConverter` instead + - Method.url renamed to path + - `Converter.encode` and `Converter.decode` removed, implement `Converter.convertResponse` and + Converter.convertRequest` instead + - `ChopperClient.jsonApi` deprecated, use a `JsonConverter` instead + - `ChopperClient.formUrlEncodedApi`, use `FormUrlEncodedConverter` instead + - remove `JsonEncoded` annotation, use `FactoryConverter` instead ## 1.1.0 - ***BreakingChange*** - Removed `name` parameter on `ChopperApi` - New way to instanciate a service + Removed `name` parameter on `ChopperApi` + New way to instanciate a service @ChopperApi() abstract class MyService extends ChopperService { static MyService create([ChopperClient client]) => _$MyService(client); } - ## 1.0.0 - Multipart request diff --git a/chopper/pubspec.yaml b/chopper/pubspec.yaml index 6ef71aad..322108e1 100644 --- a/chopper/pubspec.yaml +++ b/chopper/pubspec.yaml @@ -1,6 +1,6 @@ name: chopper description: Chopper is an http client generator using source_gen, inspired by Retrofit -version: 6.1.2 +version: 6.1.3 documentation: https://hadrien-lejard.gitbook.io/chopper repository: https://github.com/lejard-h/chopper diff --git a/chopper_built_value/CHANGELOG.md b/chopper_built_value/CHANGELOG.md index c78c9e07..5fc1f1f7 100644 --- a/chopper_built_value/CHANGELOG.md +++ b/chopper_built_value/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 1.2.2 + +- Update http constraint to ">=0.13.0 <2.0.0" ([#431](https://github.com/lejard-h/chopper/pull/431)) + ## 1.2.1 - Packages upgrade, constraints upgrade diff --git a/chopper_built_value/pubspec.yaml b/chopper_built_value/pubspec.yaml index 9d9a208e..1eed9725 100644 --- a/chopper_built_value/pubspec.yaml +++ b/chopper_built_value/pubspec.yaml @@ -1,6 +1,6 @@ name: chopper_built_value description: A built_value based Converter for Chopper. -version: 1.2.1 +version: 1.2.2 documentation: https://hadrien-lejard.gitbook.io/chopper/converters/built-value-converter repository: https://github.com/lejard-h/chopper diff --git a/chopper_generator/CHANGELOG.md b/chopper_generator/CHANGELOG.md index 4fa0cf97..2c58007d 100644 --- a/chopper_generator/CHANGELOG.md +++ b/chopper_generator/CHANGELOG.md @@ -36,13 +36,13 @@ ## 4.0.1 - Fix for the null safety support + ## 4.0.0 - **Null safety support** - Fix `@Header` annotation not generating null safe code - Respect `required` keyword in functions - ## 3.0.5 - Packages upgrade @@ -66,7 +66,8 @@ ## 3.0.0 -- Maintenance release to support last version of `chopper` package (3.0.0) that introduced a breaking change on error handling +- Maintenance release to support last version of `chopper` package (3.0.0) that introduced a breaking change on error + handling ## 2.5.0 @@ -76,8 +77,8 @@ ## 2.4.2 - Fix on JsonConverter - If content type header overrided using @Post(headers: {'content-type': '...'}) - The converter won't add json header and won't apply json.encode if content type is not JSON + If content type header overrided using @Post(headers: {'content-type': '...'}) + The converter won't add json header and won't apply json.encode if content type is not JSON - add `bool override` on `applyHeader(s)` functions, true by default @@ -94,7 +95,7 @@ ## 2.3.4 - fix trailing slash when empty path +fix trailing slash when empty path ## 2.3.3 @@ -127,12 +128,12 @@ ## 2.2.0 - Fix converter issue on List - - ***Breaking Change*** - on `Converter.convertResponse(response)`, - it take a new generic type => `Converter.convertResponse(response)` + - ***Breaking Change*** + on `Converter.convertResponse(response)`, + it take a new generic type => `Converter.convertResponse(response)` - deprecated `Chopper.service(Type)`, use `Chopper.getservice()` instead -thanks to @MichaelDark + thanks to @MichaelDark ## 2.1.0 @@ -142,29 +143,31 @@ thanks to @MichaelDark - Request is now containing baseUrl - Can call `Request.toHttpRequest()` direclty to get the `http.BaseRequest` will receive -- If a full url is specified in the `path` (ex: @Get(path: 'https://...')), it won't be concaten with the baseUrl of the ChopperClient and the ChopperAPI +- If a full url is specified in the `path` (ex: @Get(path: 'https://...')), it won't be concaten with the baseUrl of the + ChopperClient and the ChopperAPI - Add `CurlInterceptor` thanks @edwardaux - Add `HttpLoggingInterceptor` - Add `FactoryConverter` annotation `@FactoryConverter(request: convertRequest, response: convertResponse)` - ***BreakingChange*** - - Method.url renamed to path - - `Converter.encode` and `Converter.decode` removed, implement `Converter.convertResponse` and Converter.convertRequest` instead - - `ChopperClient.jsonApi` deprecated, use a `JsonConverter` instead - - `ChopperClient.formUrlEncodedApi`, use `FormUrlEncodedConverter` instead - - remove `JsonEncoded` annotation, use `FactoryConverter` instead + - Method.url renamed to path + - `Converter.encode` and `Converter.decode` removed, implement `Converter.convertResponse` and + Converter.convertRequest` instead + - `ChopperClient.jsonApi` deprecated, use a `JsonConverter` instead + - `ChopperClient.formUrlEncodedApi`, use `FormUrlEncodedConverter` instead + - remove `JsonEncoded` annotation, use `FactoryConverter` instead ## 1.1.0 - ***BreakingChange*** - Removed `name` parameter on `ChopperApi` - New way to instanciate a service - ```dart - @ChopperApi() - abstract class MyService extends ChopperService { - static MyService create([ChopperClient client]) => _$MyService(client); - } - ``` + Removed `name` parameter on `ChopperApi` + New way to instanciate a service + ```dart + @ChopperApi() + abstract class MyService extends ChopperService { + static MyService create([ChopperClient client]) => _$MyService(client); + } + ``` ## 1.0.1 diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 5a1e090d..d88b0adf 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -1,6 +1,6 @@ name: chopper_example description: Example usage of the Chopper package -version: 0.0.4 +version: 0.0.5 documentation: https://hadrien-lejard.gitbook.io/chopper/ #author: Hadrien Lejard From 455cd343923880817a46af2eba556dd4af4d7f8c Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Sat, 3 Jun 2023 10:07:39 +0100 Subject: [PATCH 068/171] :bug: fix Multipart for List and List (#439) --- chopper/lib/src/request.dart | 17 +- chopper/test/multipart_test.dart | 558 ++++++++++++++++--------- chopper/test/test_service.chopper.dart | 36 ++ chopper/test/test_service.dart | 9 + 4 files changed, 409 insertions(+), 211 deletions(-) diff --git a/chopper/lib/src/request.dart b/chopper/lib/src/request.dart index 7e0b21f2..736126ee 100644 --- a/chopper/lib/src/request.dart +++ b/chopper/lib/src/request.dart @@ -180,13 +180,18 @@ class Request extends http.BaseRequest with EquatableMixin { ); } else { throw ArgumentError( - 'Type ${part.value.runtimeType} is not a supported type for PartFile' - 'Please use one of the following types' - ' - List' - ' - String (path of your file) ' - ' - MultipartFile (from package:http)', + 'Type ${part.value.runtimeType} is not a supported type for PartFile. ' + 'Please use one of the following types:\n' + '- List\n' + '- String (path of your file)\n' + '- MultipartFile (from package:http)', ); } + } else if (part.value is Iterable) { + request.fields.addAll({ + for (int i = 0; i < part.value.length; i++) + '${part.name}[$i]': part.value.elementAt(i).toString(), + }); } else { request.fields[part.name] = part.value.toString(); } @@ -251,7 +256,7 @@ class PartValue with EquatableMixin { ]; } -/// Represents a file part in a multipart request. +/// Represents a file [PartValue] in a multipart request. @immutable class PartValueFile extends PartValue { const PartValueFile(super.name, super.value); diff --git a/chopper/test/multipart_test.dart b/chopper/test/multipart_test.dart index ebb8af90..8943a412 100644 --- a/chopper/test/multipart_test.dart +++ b/chopper/test/multipart_test.dart @@ -90,267 +90,415 @@ void main() { chopper.dispose(); }); - }); - test('file with MultipartFile', () async { - final httpClient = MockClient((http.Request req) async { - expect(req.headers['Content-Type'], contains('multipart/form-data;')); - expect( - req.body, - contains('content-type: application/octet-stream'), - ); + test('file with MultipartFile', () async { + final httpClient = MockClient((http.Request req) async { + expect(req.headers['Content-Type'], contains('multipart/form-data;')); + expect( + req.body, + contains('content-type: application/octet-stream'), + ); - expect( - req.body, - isNot(contains('content-disposition: form-data; name="id"')), - ); - expect( - req.body, - contains( - 'content-disposition: form-data; name="file_field"; filename="file_name"', - ), - ); - expect( - req.body, - contains(String.fromCharCodes([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])), + expect( + req.body, + isNot(contains('content-disposition: form-data; name="id"')), + ); + expect( + req.body, + contains( + 'content-disposition: form-data; name="file_field"; filename="file_name"', + ), + ); + expect( + req.body, + contains(String.fromCharCodes([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])), + ); + + return http.Response('ok', 200); + }); + + final chopper = ChopperClient(client: httpClient); + final service = HttpTestService.create(chopper); + + final file = http.MultipartFile.fromBytes( + 'file_field', + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + filename: 'file_name', + contentType: MediaType.parse('application/octet-stream'), ); - return http.Response('ok', 200); + await service.postMultipartFile(file); + + chopper.dispose(); }); - final chopper = ChopperClient(client: httpClient); - final service = HttpTestService.create(chopper); + test('MultipartFile with other Part', () async { + final httpClient = MockClient((http.Request req) async { + expect(req.headers['Content-Type'], contains('multipart/form-data;')); - final file = http.MultipartFile.fromBytes( - 'file_field', - [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], - filename: 'file_name', - contentType: MediaType.parse('application/octet-stream'), - ); + expect( + req.body, + contains( + 'content-disposition: form-data; name="id"\r\n\r\n42\r\n', + ), + ); - await service.postMultipartFile(file); + expect( + req.body, + contains('content-type: application/octet-stream'), + ); + expect( + req.body, + contains( + 'content-disposition: form-data; name="file_field"; filename="file_name"', + ), + ); + expect( + req.body, + contains(String.fromCharCodes([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])), + ); - chopper.dispose(); - }); + return http.Response('ok', 200); + }); - test('MultipartFile with other Part', () async { - final httpClient = MockClient((http.Request req) async { - expect(req.headers['Content-Type'], contains('multipart/form-data;')); + final chopper = ChopperClient(client: httpClient); + final service = HttpTestService.create(chopper); - expect( - req.body, - contains( - 'content-disposition: form-data; name="id"\r\n\r\n42\r\n', - ), + final file = http.MultipartFile.fromBytes( + 'file_field', + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], + filename: 'file_name', + contentType: MediaType.parse('application/octet-stream'), ); - expect( - req.body, - contains('content-type: application/octet-stream'), - ); - expect( - req.body, - contains( - 'content-disposition: form-data; name="file_field"; filename="file_name"', - ), - ); - expect( - req.body, - contains(String.fromCharCodes([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])), - ); + await service.postMultipartFile(file, id: '42'); - return http.Response('ok', 200); + chopper.dispose(); }); - final chopper = ChopperClient(client: httpClient); - final service = HttpTestService.create(chopper); + test('support List', () async { + final httpClient = MockClient((http.Request req) async { + expect(req.headers['Content-Type'], contains('multipart/form-data;')); - final file = http.MultipartFile.fromBytes( - 'file_field', - [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], - filename: 'file_name', - contentType: MediaType.parse('application/octet-stream'), - ); + expect( + req.body, + contains( + 'content-type: application/octet-stream\r\n' + 'content-disposition: form-data; name="file_1"; filename="file_name_1"\r\n' + '\r\n' + 'Hello', + ), + ); - await service.postMultipartFile(file, id: '42'); + expect( + req.body, + contains( + 'content-type: application/octet-stream\r\n' + 'content-disposition: form-data; name="file_2"; filename="file_name_2"\r\n' + '\r\n' + 'World', + ), + ); - chopper.dispose(); - }); + return http.Response('ok', 200); + }); - test('support List', () async { - final httpClient = MockClient((http.Request req) async { - expect(req.headers['Content-Type'], contains('multipart/form-data;')); + final chopper = ChopperClient(client: httpClient); + final service = HttpTestService.create(chopper); - expect( - req.body, - contains( - 'content-type: application/octet-stream\r\n' - 'content-disposition: form-data; name="file_1"; filename="file_name_1"\r\n' - '\r\n' - 'Hello', - ), + final file1 = http.MultipartFile.fromBytes( + 'file_1', + 'Hello'.codeUnits, + filename: 'file_name_1', + contentType: MediaType.parse('application/octet-stream'), ); - expect( - req.body, - contains( - 'content-type: application/octet-stream\r\n' - 'content-disposition: form-data; name="file_2"; filename="file_name_2"\r\n' - '\r\n' - 'World', - ), + final file2 = http.MultipartFile.fromBytes( + 'file_2', + 'World'.codeUnits, + filename: 'file_name_2', + contentType: MediaType.parse('application/octet-stream'), ); - return http.Response('ok', 200); + await service.postListFiles([file1, file2]); + + chopper.dispose(); }); - final chopper = ChopperClient(client: httpClient); - final service = HttpTestService.create(chopper); + test('PartValue', () async { + final req = await Request( + HttpMethod.Post, + Uri.parse('https://foo/'), + Uri.parse(''), + parts: [ + PartValue('foo', 'bar'), + PartValue('int', 42), + ], + ).toMultipartRequest(); - final file1 = http.MultipartFile.fromBytes( - 'file_1', - 'Hello'.codeUnits, - filename: 'file_name_1', - contentType: MediaType.parse('application/octet-stream'), - ); + expect(req.fields['foo'], equals('bar')); + expect(req.fields['int'], equals('42')); + }); + + test( + 'PartFile', + () async { + final req = await Request( + HttpMethod.Post, + Uri.parse('https://foo/'), + Uri.parse(''), + parts: [ + PartValueFile('foo', 'test/multipart_test.dart'), + PartValueFile>('int', [1, 2]), + ], + ).toMultipartRequest(); - final file2 = http.MultipartFile.fromBytes( - 'file_2', - 'World'.codeUnits, - filename: 'file_name_2', - contentType: MediaType.parse('application/octet-stream'), + expect( + req.files.firstWhere((f) => f.field == 'foo').filename, + equals('multipart_test.dart'), + ); + final bytes = await req.files + .firstWhere((f) => f.field == 'int') + .finalize() + .first; + expect(bytes, equals([1, 2])); + }, + testOn: 'vm', ); - await service.postListFiles([file1, file2]); + test('PartValue.replace', () { + dynamic part = PartValue('foo', 'bar'); - chopper.dispose(); - }); + expect(part.name, equals('foo')); + expect(part.value, equals('bar')); - test('PartValue', () async { - final req = await Request( - HttpMethod.Post, - Uri.parse('https://foo/'), - Uri.parse(''), - parts: [ - PartValue('foo', 'bar'), - PartValue('int', 42), - ], - ).toMultipartRequest(); - - expect(req.fields['foo'], equals('bar')); - expect(req.fields['int'], equals('42')); - }); + part = part.copyWith(value: 42); + + expect(part.name, equals('foo')); + expect(part.value, equals(42)); - test( - 'PartFile', - () async { + part = part.copyWith(name: 'int'); + + expect(part.name, equals('int')); + expect(part.value, equals(42)); + }); + + test('Multipart request non nullable', () async { final req = await Request( HttpMethod.Post, Uri.parse('https://foo/'), Uri.parse(''), parts: [ - PartValueFile('foo', 'test/multipart_test.dart'), - PartValueFile>('int', [1, 2]), + PartValue('int', 42), + PartValueFile>('list int', [1, 2]), + PartValue('null value', null), + PartValueFile('null file', null), ], ).toMultipartRequest(); + expect(req.fields.length, equals(1)); + expect(req.fields['int'], equals('42')); + expect(req.files.length, equals(1)); + final bytes = await req.files.first.finalize().first; + expect(bytes, equals([1, 2])); + }); + + test('PartValue with MultipartFile directly', () async { + final req = await Request( + HttpMethod.Post, + Uri.parse('https://foo/'), + Uri.parse(''), + parts: [ + PartValue( + '', + http.MultipartFile.fromBytes( + 'first', + [1, 2], + filename: 'list int 1', + ), + ), + PartValueFile( + '', + http.MultipartFile.fromBytes( + 'second', + [2, 1], + filename: 'list int 2', + ), + ), + ], + ).toMultipartRequest(); + + final first = req.files[0]; + final second = req.files[1]; + + expect(first.filename, equals('list int 1')); + expect(second.filename, equals('list int 2')); + + var bytes = await first.finalize().first; + expect(bytes, equals([1, 2])); + + bytes = await second.finalize().first; + expect(bytes, equals([2, 1])); + }); + + test('Throw exception', () async { expect( - req.files.firstWhere((f) => f.field == 'foo').filename, - equals('multipart_test.dart'), + () async => await Request( + HttpMethod.Post, + Uri.parse('https://foo/'), + Uri.parse(''), + parts: [ + PartValueFile('', 123), + ], + ).toMultipartRequest(), + throwsA(isA()), ); - final bytes = - await req.files.firstWhere((f) => f.field == 'int').finalize().first; - expect(bytes, equals([1, 2])); - }, - testOn: 'vm', - ); + }); - test('PartValue.replace', () { - dynamic part = PartValue('foo', 'bar'); + test('Multipart request List', () async { + const List ints = [1, 2, 3]; + const List doubles = [1.23, -1.23, 0.0, 0.12324, 3 / 4]; + const List nums = [ + 1.23443534678, + 0.00000000001, + -34251, + 0.0, + 3 / 4, + ]; + const List strings = [ + 'lorem', + 'ipsum', + 'dolor', + '''r237tw78re ei[04o2 ]de[qwlr;,mgrrt9ie0owp[ld;s,a.vfe[plre'q/sd;poeรŸลกฤ‘ฤฤ‡ลพ''', + ]; - expect(part.name, equals('foo')); - expect(part.value, equals('bar')); + final req = await Request( + HttpMethod.Post, + Uri.parse('https://foo/'), + Uri.parse(''), + parts: [ + PartValue>('ints', ints), + PartValue>('doubles', doubles), + PartValue>('nums', nums), + PartValue>('strings', strings), + ], + ).toMultipartRequest(); - part = part.copyWith(value: 42); + expect( + req.fields.length, + equals(ints.length + doubles.length + nums.length + strings.length), + ); - expect(part.name, equals('foo')); - expect(part.value, equals(42)); + for (var i = 0; i < ints.length; i++) { + expect(req.fields['ints[$i]'], equals(ints[i].toString())); + } - part = part.copyWith(name: 'int'); + for (var i = 0; i < doubles.length; i++) { + expect(req.fields['doubles[$i]'], equals(doubles[i].toString())); + } - expect(part.name, equals('int')); - expect(part.value, equals(42)); - }); + for (var i = 0; i < nums.length; i++) { + expect(req.fields['nums[$i]'], equals(nums[i].toString())); + } - test('Multipart request non nullable', () async { - final req = await Request( - HttpMethod.Post, - Uri.parse('https://foo/'), - Uri.parse(''), - parts: [ - PartValue('int', 42), - PartValueFile>('list int', [1, 2]), - PartValue('null value', null), - PartValueFile('null file', null), - ], - ).toMultipartRequest(); - - expect(req.fields.length, equals(1)); - expect(req.fields['int'], equals('42')); - expect(req.files.length, equals(1)); - final bytes = await req.files.first.finalize().first; - expect(bytes, equals([1, 2])); - }); + for (var i = 0; i < strings.length; i++) { + expect(req.fields['strings[$i]'], equals(strings[i])); + } + }); - test('PartValue with MultipartFile directly', () async { - final req = await Request( - HttpMethod.Post, - Uri.parse('https://foo/'), - Uri.parse(''), - parts: [ - PartValue( - '', - http.MultipartFile.fromBytes( - 'first', - [1, 2], - filename: 'list int 1', - ), - ), - PartValueFile( - '', - http.MultipartFile.fromBytes( - 'second', - [2, 1], - filename: 'list int 2', - ), - ), - ], - ).toMultipartRequest(); + test('Multipart lists', () async { + const List ints = [1, 2, 3]; + const List doubles = [1.23, -1.23, 0.0, 0.12324, 3 / 4]; + const List nums = [ + 1.23443534678, + 0.00000000001, + -34251, + 0.0, + 3 / 4, + ]; + const String utf8String = + '''r237tw78re ei[04o2 ]de[qwlr;,mgrrt9ie0owp[ld;s,a.vfe[plre'q/sd;poeรŸลกฤ‘ฤฤ‡ลพ'''; + const List strings = [ + 'lorem', + 'ipsum', + 'dolor', + utf8String, + ]; - final first = req.files[0]; - final second = req.files[1]; + final httpClient = MockClient((http.Request req) async { + expect(req.headers['Content-Type'], contains('multipart/form-data;')); - expect(first.filename, equals('list int 1')); - expect(second.filename, equals('list int 2')); + for (var i = 0; i < ints.length; i++) { + expect( + req.body, + contains( + 'content-disposition: form-data; name="ints[$i]"\r\n' + '\r\n' + '${ints[i]}\r\n', + ), + ); + } + + for (var i = 0; i < doubles.length; i++) { + expect( + req.body, + contains( + 'content-disposition: form-data; name="doubles[$i]"\r\n' + '\r\n' + '${doubles[i]}\r\n', + ), + ); + } + + for (var i = 0; i < nums.length; i++) { + expect( + req.body, + contains( + 'content-disposition: form-data; name="nums[$i]"\r\n' + '\r\n' + '${nums[i]}\r\n', + ), + ); + } + + for (var i = 0; i < strings.length; i++) { + if (strings[i] == utf8String) { + expect( + req.body, + contains( + 'content-disposition: form-data; name="strings[$i]"\r\n' + 'content-type: text/plain; charset=utf-8\r\n' + 'content-transfer-encoding: binary\r\n' + '\r\n' + '${strings[i]}\r\n', + ), + ); + } else { + expect( + req.body, + contains( + 'content-disposition: form-data; name="strings[$i]"\r\n' + '\r\n' + '${strings[i]}\r\n', + ), + ); + } + } - var bytes = await first.finalize().first; - expect(bytes, equals([1, 2])); + return http.Response('ok', 200); + }); - bytes = await second.finalize().first; - expect(bytes, equals([2, 1])); - }); + final chopper = ChopperClient(client: httpClient); + final service = HttpTestService.create(chopper); - test('Throw exception', () async { - expect( - () async => await Request( - HttpMethod.Post, - Uri.parse('https://foo/'), - Uri.parse(''), - parts: [ - PartValueFile('', 123), - ], - ).toMultipartRequest(), - throwsA(isA()), - ); + await service.postMultipartList( + ints: ints, + doubles: doubles, + nums: nums, + strings: strings, + ); + + chopper.dispose(); + }); }); } diff --git a/chopper/test/test_service.chopper.dart b/chopper/test/test_service.chopper.dart index f93398a5..45e1d737 100644 --- a/chopper/test/test_service.chopper.dart +++ b/chopper/test/test_service.chopper.dart @@ -475,6 +475,42 @@ class _$HttpTestService extends HttpTestService { return client.send($request); } + @override + Future> postMultipartList({ + required List ints, + required List doubles, + required List nums, + required List strings, + }) { + final Uri $url = Uri.parse('/test/multipart_list'); + final List $parts = [ + PartValue>( + 'ints', + ints, + ), + PartValue>( + 'doubles', + doubles, + ), + PartValue>( + 'nums', + nums, + ), + PartValue>( + 'strings', + strings, + ), + ]; + final Request $request = Request( + 'POST', + $url, + client.baseUrl, + parts: $parts, + multipart: true, + ); + return client.send($request); + } + @override Future fullUrl() { final Uri $url = Uri.parse('https://test.com'); diff --git a/chopper/test/test_service.dart b/chopper/test/test_service.dart index 6257f43a..c0baf1a4 100644 --- a/chopper/test/test_service.dart +++ b/chopper/test/test_service.dart @@ -136,6 +136,15 @@ abstract class HttpTestService extends ChopperService { @multipart Future postListFiles(@PartFile() List files); + @Post(path: 'multipart_list') + @multipart + Future postMultipartList({ + @Part('ints') required List ints, + @Part('doubles') required List doubles, + @Part('nums') required List nums, + @Part('strings') required List strings, + }); + @Get(path: 'https://test.com') Future fullUrl(); From 1f8c59630c5342310b138e4585cc38fad97f22a5 Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Sun, 4 Jun 2023 09:04:54 +0100 Subject: [PATCH 069/171] :twisted_rightwards_arrows: post release branch sync (#441) --- chopper/CHANGELOG.md | 4 ++++ chopper/pubspec.yaml | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/chopper/CHANGELOG.md b/chopper/CHANGELOG.md index e14d3af5..d8a56152 100644 --- a/chopper/CHANGELOG.md +++ b/chopper/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 6.1.4 + +- Fix Multipart for List and List ([#439](https://github.com/lejard-h/chopper/pull/439)) + ## 6.1.3 - Add follow redirects to toHttpRequest ([#430](https://github.com/lejard-h/chopper/pull/430)) diff --git a/chopper/pubspec.yaml b/chopper/pubspec.yaml index 322108e1..4fd47d0a 100644 --- a/chopper/pubspec.yaml +++ b/chopper/pubspec.yaml @@ -1,6 +1,6 @@ name: chopper description: Chopper is an http client generator using source_gen, inspired by Retrofit -version: 6.1.3 +version: 6.1.4 documentation: https://hadrien-lejard.gitbook.io/chopper repository: https://github.com/lejard-h/chopper From 917a463541078df5eb25622cc1daaeb70e959542 Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Mon, 19 Jun 2023 16:40:34 +0100 Subject: [PATCH 070/171] :sparkles: Generating files in different directories (#444) --- chopper_generator/lib/chopper_generator.dart | 2 +- chopper_generator/lib/src/generator.dart | 13 +++++++++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/chopper_generator/lib/chopper_generator.dart b/chopper_generator/lib/chopper_generator.dart index 74355389..84099889 100644 --- a/chopper_generator/lib/chopper_generator.dart +++ b/chopper_generator/lib/chopper_generator.dart @@ -5,4 +5,4 @@ import 'package:build/build.dart'; import 'src/generator.dart'; Builder chopperGeneratorFactory(BuilderOptions options) => - chopperGeneratorFactoryBuilder(header: options.config['header']); + chopperGeneratorFactoryBuilder(options); diff --git a/chopper_generator/lib/src/generator.dart b/chopper_generator/lib/src/generator.dart index 72635a75..cb3b4f2f 100644 --- a/chopper_generator/lib/src/generator.dart +++ b/chopper_generator/lib/src/generator.dart @@ -634,10 +634,19 @@ class ChopperGenerator extends GeneratorForAnnotation { } } -Builder chopperGeneratorFactoryBuilder({String? header}) => PartBuilder( +Builder chopperGeneratorFactoryBuilder(BuilderOptions options) => PartBuilder( [ChopperGenerator()], '.chopper.dart', - header: header, + header: options.config['header'], + formatOutput: + PartBuilder([ChopperGenerator()], '.chopper.dart').formatOutput, + options: !options.config.containsKey('build_extensions') + ? options.overrideWith( + BuilderOptions({ + 'build_extensions': {'.dart': '.chopper.dart'}, + }), + ) + : options, ); bool getMethodOptionalBody(ConstantReader method) => From ac907839cb79a187046bffc56fcf3fd834290534 Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Mon, 19 Jun 2023 17:15:01 +0100 Subject: [PATCH 071/171] :twisted_rightwards_arrows: post release branch sync (#447) --- chopper_generator/CHANGELOG.md | 4 ++++ chopper_generator/pubspec.yaml | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/chopper_generator/CHANGELOG.md b/chopper_generator/CHANGELOG.md index 2c58007d..881ab59e 100644 --- a/chopper_generator/CHANGELOG.md +++ b/chopper_generator/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 6.0.2 + +- Add support for generating files in different directories ([#444](https://github.com/lejard-h/chopper/pull/444)) + ## 6.0.1 - Packages upgrade, constraints upgrade diff --git a/chopper_generator/pubspec.yaml b/chopper_generator/pubspec.yaml index 716f5332..ee10e5de 100644 --- a/chopper_generator/pubspec.yaml +++ b/chopper_generator/pubspec.yaml @@ -1,6 +1,6 @@ name: chopper_generator description: Chopper is an http client generator using source_gen, inspired by Retrofit -version: 6.0.1 +version: 6.0.2 documentation: https://hadrien-lejard.gitbook.io/chopper repository: https://github.com/lejard-h/chopper From 97959f0773d40e0e3ce21fd87a6e025e283012ad Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Tue, 4 Jul 2023 22:01:47 +0100 Subject: [PATCH 072/171] :recycle: refactor chopper_generator (#448) --- .github/workflows/dart.yml | 21 +- chopper/pubspec.yaml | 1 + chopper/test/ensure_build_test.dart | 16 + chopper_generator/Makefile | 4 + chopper_generator/README.md | 9 + chopper_generator/lib/chopper_generator.dart | 7 +- .../lib/src/builder_factory.dart | 23 + chopper_generator/lib/src/extensions.dart | 6 + chopper_generator/lib/src/generator.dart | 380 +++++------ chopper_generator/lib/src/utils.dart | 61 ++ chopper_generator/lib/src/vars.dart | 17 + chopper_generator/mono_pkg.yaml | 2 + chopper_generator/pubspec.yaml | 5 +- chopper_generator/test/ensure_build_test.dart | 16 + .../test/test_service.chopper.dart | 639 ++++++++++++++++++ chopper_generator/test/test_service.dart | 218 ++++++ 16 files changed, 1191 insertions(+), 234 deletions(-) create mode 100644 chopper/test/ensure_build_test.dart create mode 100644 chopper_generator/lib/src/builder_factory.dart create mode 100644 chopper_generator/lib/src/extensions.dart create mode 100644 chopper_generator/lib/src/utils.dart create mode 100644 chopper_generator/lib/src/vars.dart create mode 100644 chopper_generator/test/ensure_build_test.dart create mode 100644 chopper_generator/test/test_service.chopper.dart create mode 100644 chopper_generator/test/test_service.dart diff --git a/.github/workflows/dart.yml b/.github/workflows/dart.yml index 444801b6..ee10d7f9 100644 --- a/.github/workflows/dart.yml +++ b/.github/workflows/dart.yml @@ -127,16 +127,16 @@ jobs: - job_001 - job_002 job_004: - name: "unit_test; PKGS: chopper, chopper_built_value; `dart pub global run coverage:test_with_coverage`" + name: "unit_test; PKGS: chopper, chopper_built_value, chopper_generator; `dart pub global run coverage:test_with_coverage`" runs-on: ubuntu-latest steps: - name: Cache Pub hosted dependencies uses: actions/cache@88522ab9f39a2ea568f7027eddc7d8d8bc9d59c8 with: path: "~/.pub-cache/hosted" - key: "os:ubuntu-latest;pub-cache-hosted;sdk:stable;packages:chopper-chopper_built_value;commands:test_with_coverage" + key: "os:ubuntu-latest;pub-cache-hosted;sdk:stable;packages:chopper-chopper_built_value-chopper_generator;commands:test_with_coverage" restore-keys: | - os:ubuntu-latest;pub-cache-hosted;sdk:stable;packages:chopper-chopper_built_value + os:ubuntu-latest;pub-cache-hosted;sdk:stable;packages:chopper-chopper_built_value-chopper_generator os:ubuntu-latest;pub-cache-hosted;sdk:stable os:ubuntu-latest;pub-cache-hosted os:ubuntu-latest @@ -179,6 +179,21 @@ jobs: files: chopper_built_value/coverage/lcov.info fail_ci_if_error: true name: coverage_01 + - id: chopper_generator_pub_upgrade + name: chopper_generator; dart pub upgrade + run: dart pub upgrade + if: "always() && steps.checkout.conclusion == 'success'" + working-directory: chopper_generator + - name: "chopper_generator; dart pub global run coverage:test_with_coverage" + run: "dart pub global run coverage:test_with_coverage" + if: "always() && steps.chopper_generator_pub_upgrade.conclusion == 'success'" + working-directory: chopper_generator + - name: Upload coverage to codecov.io + uses: codecov/codecov-action@main + with: + files: chopper_generator/coverage/lcov.info + fail_ci_if_error: true + name: coverage_02 needs: - job_001 - job_002 diff --git a/chopper/pubspec.yaml b/chopper/pubspec.yaml index 4fd47d0a..2af5cfbf 100644 --- a/chopper/pubspec.yaml +++ b/chopper/pubspec.yaml @@ -16,6 +16,7 @@ dependencies: dev_dependencies: build_runner: ^2.0.0 build_test: ^2.0.0 + build_verify: ^3.1.0 collection: ^1.16.0 coverage: ^1.0.2 dart_code_metrics: '>=4.8.1 <6.0.0' diff --git a/chopper/test/ensure_build_test.dart b/chopper/test/ensure_build_test.dart new file mode 100644 index 00000000..73f46337 --- /dev/null +++ b/chopper/test/ensure_build_test.dart @@ -0,0 +1,16 @@ +@TestOn('vm') +@Timeout(Duration(seconds: 120)) +import 'package:build_verify/build_verify.dart'; +import 'package:test/test.dart'; + +void main() { + test( + 'ensure_build', + () => expectBuildClean( + packageRelativeDirectory: 'chopper', + gitDiffPathArguments: [ + 'test/test_service.chopper.dart', + ], + ), + ); +} diff --git a/chopper_generator/Makefile b/chopper_generator/Makefile index dee4bcf7..444451d2 100644 --- a/chopper_generator/Makefile +++ b/chopper_generator/Makefile @@ -41,6 +41,10 @@ install: @# Help: Install all the project's packages dart pub get +tests: + @# Help: Run Dart unit and widget tests for the current project. + dart test + upgrade: @# Help: Upgrade all the project's packages. dart pub upgrade \ No newline at end of file diff --git a/chopper_generator/README.md b/chopper_generator/README.md index 63f60659..81a0f90f 100644 --- a/chopper_generator/README.md +++ b/chopper_generator/README.md @@ -1 +1,10 @@ +# chopper_generator + +[![pub package](https://img.shields.io/pub/v/chopper_generator.svg)](https://pub.dartlang.org/packages/chopper_generator) + This package provides the code generator for the [Chopper](https://github.com/lejard-h/chopper) package. + +## Usage + +For examples please refer to the main [Chopper](https://github.com/lejard-h/chopper) package and/or read the +[documentation](https://hadrien-lejard.gitbook.io/chopper). \ No newline at end of file diff --git a/chopper_generator/lib/chopper_generator.dart b/chopper_generator/lib/chopper_generator.dart index 84099889..df3d5954 100644 --- a/chopper_generator/lib/chopper_generator.dart +++ b/chopper_generator/lib/chopper_generator.dart @@ -1,8 +1,3 @@ library chopper_generator.dart; -import 'package:build/build.dart'; - -import 'src/generator.dart'; - -Builder chopperGeneratorFactory(BuilderOptions options) => - chopperGeneratorFactoryBuilder(options); +export 'src/builder_factory.dart'; diff --git a/chopper_generator/lib/src/builder_factory.dart b/chopper_generator/lib/src/builder_factory.dart new file mode 100644 index 00000000..f5ff89d1 --- /dev/null +++ b/chopper_generator/lib/src/builder_factory.dart @@ -0,0 +1,23 @@ +import 'package:build/build.dart'; +import 'package:chopper/chopper.dart' show ChopperApi; +import 'package:source_gen/source_gen.dart'; + +import 'generator.dart'; + +/// Creates a [PartBuilder] used to generate code for [ChopperApi] annotated +/// classes. The [options] are provided by Dart's build system and read from the +/// `build.yaml` file. +Builder chopperGeneratorFactory(BuilderOptions options) => PartBuilder( + [const ChopperGenerator()], + '.chopper.dart', + header: options.config['header'], + formatOutput: + PartBuilder([const ChopperGenerator()], '.chopper.dart').formatOutput, + options: !options.config.containsKey('build_extensions') + ? options.overrideWith( + BuilderOptions({ + 'build_extensions': {'.dart': '.chopper.dart'}, + }), + ) + : options, + ); diff --git a/chopper_generator/lib/src/extensions.dart b/chopper_generator/lib/src/extensions.dart new file mode 100644 index 00000000..8361baf6 --- /dev/null +++ b/chopper_generator/lib/src/extensions.dart @@ -0,0 +1,6 @@ +import 'package:analyzer/dart/element/nullability_suffix.dart'; +import 'package:analyzer/dart/element/type.dart'; + +extension DartTypeExtension on DartType { + bool get isNullable => nullabilitySuffix != NullabilitySuffix.none; +} diff --git a/chopper_generator/lib/src/generator.dart b/chopper_generator/lib/src/generator.dart index cb3b4f2f..39353006 100644 --- a/chopper_generator/lib/src/generator.dart +++ b/chopper_generator/lib/src/generator.dart @@ -1,30 +1,25 @@ -///@nodoc -import 'dart:async'; +import 'dart:async' show FutureOr; import 'package:analyzer/dart/constant/value.dart'; import 'package:analyzer/dart/element/element.dart'; -import 'package:analyzer/dart/element/nullability_suffix.dart'; import 'package:analyzer/dart/element/type.dart'; import 'package:build/build.dart'; import 'package:built_collection/built_collection.dart'; import 'package:chopper/chopper.dart' as chopper; +import 'package:chopper_generator/src/extensions.dart'; +import 'package:chopper_generator/src/utils.dart'; +import 'package:chopper_generator/src/vars.dart'; import 'package:code_builder/code_builder.dart'; import 'package:dart_style/dart_style.dart'; import 'package:logging/logging.dart'; import 'package:source_gen/source_gen.dart'; -const String _clientVar = 'client'; -const String _baseUrlVar = 'baseUrl'; -const String _parametersVar = r'$params'; -const String _headersVar = r'$headers'; -const String _requestVar = r'$request'; -const String _bodyVar = r'$body'; -const String _partsVar = r'$parts'; -const String _urlVar = r'$url'; +/// Code generator for [chopper.ChopperApi] annotated classes. +class ChopperGenerator extends GeneratorForAnnotation { + const ChopperGenerator(); -final Logger _logger = Logger('Chopper Generator'); + static final Logger _logger = Logger('Chopper Generator'); -class ChopperGenerator extends GeneratorForAnnotation { @override FutureOr generateForAnnotatedElement( Element element, @@ -32,20 +27,20 @@ class ChopperGenerator extends GeneratorForAnnotation { BuildStep buildStep, ) { if (element is! ClassElement) { - final String friendlyName = element.displayName; throw InvalidGenerationSourceError( - 'Generator cannot target `$friendlyName`.', - todo: 'Remove the [ChopperApi] annotation from `$friendlyName`.', + 'Generator cannot target `${element.displayName}`.', + todo: + 'Remove the [ChopperApi] annotation from `${element.displayName}`.', ); } return _buildChopperApiImplementationClass(annotation, element); } - bool _extendsChopperService(InterfaceType type) => + static bool _extendsChopperService(InterfaceType type) => _typeChecker(chopper.ChopperService).isExactlyType(type); - Field _buildDefinitionTypeMethod(String superType) => Field( + static Field _buildDefinitionTypeMethod(String superType) => Field( (method) => method ..annotations.add(refer('override')) ..name = 'definitionType' @@ -53,7 +48,7 @@ class ChopperGenerator extends GeneratorForAnnotation { ..assignment = Code(superType), ); - String _buildChopperApiImplementationClass( + static String _buildChopperApiImplementationClass( ConstantReader annotation, ClassElement element, ) { @@ -67,7 +62,8 @@ class ChopperGenerator extends GeneratorForAnnotation { final String friendlyName = element.name; final String name = '_\$$friendlyName'; - final String baseUrl = annotation.peek(_baseUrlVar)?.stringValue ?? ''; + final String baseUrl = + annotation.peek(Vars.baseUrl.toString())?.stringValue ?? ''; final Class classBuilder = Class((builder) { builder @@ -78,29 +74,33 @@ class ChopperGenerator extends GeneratorForAnnotation { ..methods.addAll(_parseMethods(element, baseUrl)); }); - final String ignore = - '// ignore_for_file: always_put_control_body_on_new_line, always_specify_types, prefer_const_declarations, unnecessary_brace_in_string_interps'; + const String ignore = '// ignore_for_file: ' + 'always_put_control_body_on_new_line, ' + 'always_specify_types, ' + 'prefer_const_declarations, ' + 'unnecessary_brace_in_string_interps'; final DartEmitter emitter = DartEmitter(); return DartFormatter().format('$ignore\n${classBuilder.accept(emitter)}'); } - Constructor _generateConstructor() => Constructor( + static Constructor _generateConstructor() => Constructor( (ConstructorBuilder constructorBuilder) { constructorBuilder.optionalParameters.add( Parameter((paramBuilder) { - paramBuilder.name = _clientVar; + paramBuilder.name = Vars.client.toString(); paramBuilder.type = refer('${chopper.ChopperClient}?'); }), ); constructorBuilder.body = Code( - 'if ($_clientVar == null) return;\nthis.$_clientVar = $_clientVar;', + 'if (${Vars.client} == null) return;\n' + 'this.${Vars.client} = ${Vars.client};', ); }, ); - Iterable _parseMethods(ClassElement element, String baseUrl) => + static Iterable _parseMethods(ClassElement element, String baseUrl) => element.methods .where( (MethodElement method) => @@ -110,7 +110,7 @@ class ChopperGenerator extends GeneratorForAnnotation { ) .map((MethodElement m) => _generateMethod(m, baseUrl)); - Method _generateMethod(MethodElement m, String baseUrl) { + static Method _generateMethod(MethodElement m, String baseUrl) { final ConstantReader? method = _getMethodAnnotation(m); final bool multipart = _hasAnnotation(m, chopper.Multipart); final ConstantReader? factoryConverter = _getFactoryConverterAnnotation(m); @@ -145,12 +145,12 @@ class ChopperGenerator extends GeneratorForAnnotation { b.annotations.add(refer('override')); b.name = m.displayName; - // We don't support returning null Type + /// We don't support returning null Type b.returns = Reference( m.returnType.getDisplayString(withNullability: false), ); - // And null Typed parameters + /// And null Typed parameters b.types.addAll( m.typeParameters.map( (t) => Reference(t.getDisplayString(withNullability: false)), @@ -160,32 +160,35 @@ class ChopperGenerator extends GeneratorForAnnotation { b.requiredParameters.addAll( m.parameters .where((p) => p.isRequiredPositional) - .map(buildRequiredPositionalParam), + .map(Utils.buildRequiredPositionalParam), ); b.optionalParameters.addAll( m.parameters .where((p) => p.isOptionalPositional) - .map(buildOptionalPositionalParam), + .map(Utils.buildOptionalPositionalParam), ); b.optionalParameters.addAll( - m.parameters.where((p) => p.isNamed).map(buildNamedParam), + m.parameters.where((p) => p.isNamed).map(Utils.buildNamedParam), ); final List blocks = [ - declareFinal(_urlVar, type: refer('Uri')).assign(url).statement, + declareFinal(Vars.url.toString(), type: refer('Uri')) + .assign(url) + .statement, ]; if (queries.isNotEmpty) { blocks.add( - declareFinal(_parametersVar, type: refer('Map')) - .assign(_generateMap(queries)) - .statement, + declareFinal( + Vars.parameters.toString(), + type: refer('Map'), + ).assign(_generateMap(queries)).statement, ); } - // Build an iterable of all the parameters that are nullable + /// Build an iterable of all the parameters that are nullable final Iterable optionalNullableParameters = [ ...m.parameters.where((p) => p.isOptionalPositional), ...m.parameters.where((p) => p.isNamed), @@ -194,9 +197,9 @@ class ChopperGenerator extends GeneratorForAnnotation { final bool hasQueryMap = queryMap.isNotEmpty; if (hasQueryMap) { if (queries.isNotEmpty) { - blocks.add(refer('$_parametersVar.addAll').call( + blocks.add(refer('${Vars.parameters}.addAll').call( [ - // Check if the parameter is nullable + /// Check if the parameter is nullable optionalNullableParameters.contains(queryMap.keys.first) ? refer(queryMap.keys.first).ifNullThen(refer('const {}')) : refer(queryMap.keys.first), @@ -204,9 +207,12 @@ class ChopperGenerator extends GeneratorForAnnotation { ).statement); } else { blocks.add( - declareFinal(_parametersVar, type: refer('Map')) + declareFinal( + Vars.parameters.toString(), + type: refer('Map'), + ) .assign( - // Check if the parameter is nullable + /// Check if the parameter is nullable optionalNullableParameters.contains(queryMap.keys.first) ? refer(queryMap.keys.first).ifNullThen(refer('const {}')) : refer(queryMap.keys.first), @@ -222,18 +228,22 @@ class ChopperGenerator extends GeneratorForAnnotation { blocks.add(headers); } - final bool methodOptionalBody = getMethodOptionalBody(method); - final String methodName = getMethodName(method); - final String methodUrl = getMethodPath(method); + final bool methodOptionalBody = Utils.getMethodOptionalBody(method); + final String methodName = Utils.getMethodName(method); + final String methodUrl = Utils.getMethodPath(method); bool hasBody = body.isNotEmpty || fields.isNotEmpty; if (hasBody) { if (body.isNotEmpty) { blocks.add( - declareFinal(_bodyVar).assign(refer(body.keys.first)).statement, + declareFinal(Vars.body.toString()) + .assign(refer(body.keys.first)) + .statement, ); } else { blocks.add( - declareFinal(_bodyVar).assign(_generateMap(fields)).statement, + declareFinal(Vars.body.toString()) + .assign(_generateMap(fields)) + .statement, ); } } @@ -241,12 +251,14 @@ class ChopperGenerator extends GeneratorForAnnotation { final bool hasFieldMap = fieldMap.isNotEmpty; if (hasFieldMap) { if (hasBody) { - blocks.add(refer('$_bodyVar.addAll').call( + blocks.add(refer('${Vars.body}.addAll').call( [refer(fieldMap.keys.first)], ).statement); } else { blocks.add( - declareFinal(_bodyVar).assign(refer(fieldMap.keys.first)).statement, + declareFinal(Vars.body.toString()) + .assign(refer(fieldMap.keys.first)) + .statement, ); } } @@ -256,7 +268,7 @@ class ChopperGenerator extends GeneratorForAnnotation { bool hasParts = multipart && (parts.isNotEmpty || fileFields.isNotEmpty); if (hasParts) { blocks.add( - declareFinal(_partsVar, type: refer('List')) + declareFinal(Vars.parts.toString(), type: refer('List')) .assign(_generateList(parts, fileFields)) .statement, ); @@ -266,13 +278,13 @@ class ChopperGenerator extends GeneratorForAnnotation { if (hasPartMap) { if (hasParts) { blocks.add( - refer('$_partsVar.addAll').call( + refer('${Vars.parts}.addAll').call( [refer(partMap.keys.first)], ).statement, ); } else { blocks.add( - declareFinal(_partsVar, type: refer('List')) + declareFinal(Vars.parts.toString(), type: refer('List')) .assign(refer(partMap.keys.first)) .statement, ); @@ -283,13 +295,13 @@ class ChopperGenerator extends GeneratorForAnnotation { if (hasFileFilesMap) { if (hasParts || hasPartMap) { blocks.add( - refer('$_partsVar.addAll').call( + refer('${Vars.parts}.addAll').call( [refer(fileFieldMap.keys.first)], ).statement, ); } else { blocks.add( - declareFinal(_partsVar, type: refer('List')) + declareFinal(Vars.parts.toString(), type: refer('List')) .assign(refer(fileFieldMap.keys.first)) .statement, ); @@ -309,12 +321,12 @@ class ChopperGenerator extends GeneratorForAnnotation { ); } - final bool useBrackets = getUseBrackets(method); + final bool useBrackets = Utils.getUseBrackets(method); - final bool includeNullQueryVars = getIncludeNullQueryVars(method); + final bool includeNullQueryVars = Utils.getIncludeNullQueryVars(method); blocks.add( - declareFinal(_requestVar, type: refer('Request')) + declareFinal(Vars.request.toString(), type: refer('Request')) .assign( _generateRequest( method, @@ -355,8 +367,8 @@ class ChopperGenerator extends GeneratorForAnnotation { ); } - blocks.add(refer('$_clientVar.send') - .call([refer(_requestVar)], namedArguments, typeArguments) + blocks.add(refer('${Vars.client}.send') + .call([refer(Vars.request.toString())], namedArguments, typeArguments) .returned .statement); @@ -366,19 +378,22 @@ class ChopperGenerator extends GeneratorForAnnotation { /// TODO: Upgrade to `Element.enclosingElement` when analyzer 6.0.0 is released; in the mean time ignore the deprecation warning /// https://github.com/dart-lang/sdk/blob/main/pkg/analyzer/CHANGELOG.md#520 - String _factoryForFunction(FunctionTypedElement function) => + static String _factoryForFunction(FunctionTypedElement function) => // ignore: deprecated_member_use function.enclosingElement3 is ClassElement // ignore: deprecated_member_use ? '${function.enclosingElement3!.name}.${function.name}' : function.name!; - Map _getAnnotation(MethodElement method, Type type) { + static Map _getAnnotation( + MethodElement method, + Type type, + ) { DartObject? annotation; String name = ''; for (final ParameterElement p in method.parameters) { - DartObject? a = _typeChecker(type).firstAnnotationOf(p); + final DartObject? a = _typeChecker(type).firstAnnotationOf(p); if (annotation != null && a != null) { throw Exception( 'Too many $type annotation for \'${method.displayName}\'', @@ -392,25 +407,20 @@ class ChopperGenerator extends GeneratorForAnnotation { return annotation == null ? {} : {name: ConstantReader(annotation)}; } - Map _getAnnotations( + static Map _getAnnotations( MethodElement m, Type type, - ) { - Map annotation = {}; - for (final ParameterElement p in m.parameters) { - final DartObject? a = _typeChecker(type).firstAnnotationOf(p); - if (a != null) { - annotation[p] = ConstantReader(a); - } - } - - return annotation; - } + ) => + { + for (final ParameterElement p in m.parameters) + if (_typeChecker(type).hasAnnotationOf(p)) + p: ConstantReader(_typeChecker(type).firstAnnotationOf(p)), + }; - TypeChecker _typeChecker(Type type) => TypeChecker.fromRuntime(type); + static TypeChecker _typeChecker(Type type) => TypeChecker.fromRuntime(type); - ConstantReader? _getMethodAnnotation(MethodElement method) { - for (final type in _methodsAnnotations) { + static ConstantReader? _getMethodAnnotation(MethodElement method) { + for (final Type type in _methodsAnnotations) { final DartObject? annotation = _typeChecker(type) .firstAnnotationOf(method, throwOnUnresolved: false); if (annotation != null) { @@ -421,18 +431,18 @@ class ChopperGenerator extends GeneratorForAnnotation { return null; } - ConstantReader? _getFactoryConverterAnnotation(MethodElement method) { + static ConstantReader? _getFactoryConverterAnnotation(MethodElement method) { final DartObject? annotation = _typeChecker(chopper.FactoryConverter) .firstAnnotationOf(method, throwOnUnresolved: false); return annotation != null ? ConstantReader(annotation) : null; } - bool _hasAnnotation(MethodElement method, Type type) => + static bool _hasAnnotation(MethodElement method, Type type) => _typeChecker(type).firstAnnotationOf(method, throwOnUnresolved: false) != null; - final List _methodsAnnotations = const [ + static const List _methodsAnnotations = [ chopper.Get, chopper.Post, chopper.Delete, @@ -443,14 +453,15 @@ class ChopperGenerator extends GeneratorForAnnotation { chopper.Options, ]; - DartType? _genericOf(DartType? type) => + static DartType? _genericOf(DartType? type) => type is InterfaceType && type.typeArguments.isNotEmpty ? type.typeArguments.first : null; - DartType? _getResponseType(DartType type) => _genericOf(_genericOf(type)); + static DartType? _getResponseType(DartType type) => + _genericOf(_genericOf(type)); - DartType? _getResponseInnerType(DartType type) { + static DartType? _getResponseInnerType(DartType type) { final DartType? generic = _genericOf(type); if (generic == null || @@ -466,41 +477,41 @@ class ChopperGenerator extends GeneratorForAnnotation { return _getResponseInnerType(generic); } - Expression _generateUrl( + static Expression _generateUrl( ConstantReader method, Map paths, String baseUrl, ) { - String path = getMethodPath(method); + String path = Utils.getMethodPath(method); paths.forEach((p, ConstantReader r) { final String name = r.peek('name')?.stringValue ?? p.displayName; path = path.replaceFirst('{$name}', '\${${p.displayName}}'); }); if (path.startsWith('http://') || path.startsWith('https://')) { - // if the request's url is already a fully qualified URL, we can use - // as-is and ignore the baseUrl + /// if the request's url is already a fully qualified URL, we can use + /// as-is and ignore the baseUrl return _generateUri(path); - } else if (path.isEmpty && baseUrl.isEmpty) { + } + + if (path.isEmpty && baseUrl.isEmpty) { return _generateUri(''); - } else { - if (path.isNotEmpty && - baseUrl.isNotEmpty && - !baseUrl.endsWith('/') && - !path.startsWith('/')) { - return _generateUri('$baseUrl/$path'); - } + } - return _generateUri('$baseUrl$path'); + if (path.isNotEmpty && + baseUrl.isNotEmpty && + !baseUrl.endsWith('/') && + !path.startsWith('/')) { + return _generateUri('$baseUrl/$path'); } + + return _generateUri('$baseUrl$path'); } - Expression _generateUri(String url) => refer('Uri').newInstanceNamed( - 'parse', - [literal(url)], - ); + static Expression _generateUri(String url) => + refer('Uri').newInstanceNamed('parse', [literal(url)]); - Expression _generateRequest( + static Expression _generateRequest( ConstantReader method, { bool hasBody = false, bool hasParts = false, @@ -508,58 +519,47 @@ class ChopperGenerator extends GeneratorForAnnotation { bool useHeaders = false, bool useBrackets = false, bool includeNullQueryVars = false, - }) { - final List params = [ - literal(getMethodName(method)), - refer(_urlVar), - refer('$_clientVar.$_baseUrlVar'), - ]; - - final Map namedParams = {}; - - if (hasBody) { - namedParams['body'] = refer(_bodyVar); - } - - if (hasParts) { - namedParams['parts'] = refer(_partsVar); - namedParams['multipart'] = literalBool(true); - } - - if (useQueries) { - namedParams['parameters'] = refer(_parametersVar); - } - - if (useHeaders) { - namedParams['headers'] = refer(_headersVar); - } - - if (useBrackets) { - namedParams['useBrackets'] = literalBool(useBrackets); - } - - if (includeNullQueryVars) { - namedParams['includeNullQueryVars'] = literalBool(includeNullQueryVars); - } - - return refer('Request').newInstance(params, namedParams); - } - - Expression _generateMap(Map queries) { - final Map map = {}; - queries.forEach((ParameterElement p, ConstantReader r) { - final String name = r.peek('name')?.stringValue ?? p.displayName; - map[literal(name)] = refer(p.displayName); - }); + }) => + refer('Request').newInstance( + [ + literal(Utils.getMethodName(method)), + refer(Vars.url.toString()), + refer('${Vars.client}.${Vars.baseUrl}'), + ], + { + if (hasBody) 'body': refer(Vars.body.toString()), + if (hasParts) ...{ + 'parts': refer(Vars.parts.toString()), + 'multipart': literalBool(true), + }, + if (useQueries) 'parameters': refer(Vars.parameters.toString()), + if (useHeaders) 'headers': refer(Vars.headers.toString()), + if (useBrackets) 'useBrackets': literalBool(useBrackets), + if (includeNullQueryVars) + 'includeNullQueryVars': literalBool(includeNullQueryVars), + }, + ); - return literalMap(map, refer('String'), refer('dynamic')); - } + static Expression _generateMap( + Map queries, + ) => + literalMap( + { + for (final MapEntry query + in queries.entries) + query.value.peek('name')?.stringValue ?? query.key.displayName: + refer(query.key.displayName), + }, + refer('String'), + refer('dynamic'), + ); - Expression _generateList( + static Expression _generateList( Map parts, Map fileFields, ) { final List list = []; + parts.forEach((p, ConstantReader r) { final String name = r.peek('name')?.stringValue ?? p.displayName; final List params = [ @@ -573,6 +573,7 @@ class ChopperGenerator extends GeneratorForAnnotation { )}>', ).newInstance(params)); }); + fileFields.forEach((p, ConstantReader r) { final String name = r.peek('name')?.stringValue ?? p.displayName; final List params = [ @@ -591,10 +592,13 @@ class ChopperGenerator extends GeneratorForAnnotation { return literalList(list, refer('PartValue')); } - Code? _generateHeaders(MethodElement methodElement, ConstantReader method) { + static Code? _generateHeaders( + MethodElement methodElement, + ConstantReader method, + ) { final StringBuffer codeBuffer = StringBuffer('')..writeln('{'); - // Search for @Header anotation in method parameters + /// Search for @Header anotation in method parameters final Map annotations = _getAnnotations(methodElement, chopper.Header); @@ -628,81 +632,9 @@ class ChopperGenerator extends GeneratorForAnnotation { return code == '{\n}\n' ? null - : declareFinal(_headersVar, type: refer('Map')) - .assign(CodeExpression(Code(code))) - .statement; + : declareFinal( + Vars.headers.toString(), + type: refer('Map'), + ).assign(CodeExpression(Code(code))).statement; } } - -Builder chopperGeneratorFactoryBuilder(BuilderOptions options) => PartBuilder( - [ChopperGenerator()], - '.chopper.dart', - header: options.config['header'], - formatOutput: - PartBuilder([ChopperGenerator()], '.chopper.dart').formatOutput, - options: !options.config.containsKey('build_extensions') - ? options.overrideWith( - BuilderOptions({ - 'build_extensions': {'.dart': '.chopper.dart'}, - }), - ) - : options, - ); - -bool getMethodOptionalBody(ConstantReader method) => - method.read('optionalBody').boolValue; - -String getMethodPath(ConstantReader method) => method.read('path').stringValue; - -String getMethodName(ConstantReader method) => - method.read('method').stringValue; - -bool getUseBrackets(ConstantReader method) => - method.peek('useBrackets')?.boolValue ?? false; - -bool getIncludeNullQueryVars(ConstantReader method) => - method.peek('includeNullQueryVars')?.boolValue ?? false; - -extension DartTypeExtension on DartType { - bool get isNullable => nullabilitySuffix != NullabilitySuffix.none; -} - -// All positional required params must support nullability -Parameter buildRequiredPositionalParam(ParameterElement p) => Parameter( - (ParameterBuilder pb) => pb - ..name = p.name - ..type = Reference( - p.type.getDisplayString(withNullability: p.type.isNullable), - ), - ); - -// All optional positional params must support nullability -Parameter buildOptionalPositionalParam(ParameterElement p) => - Parameter((ParameterBuilder pb) { - pb - ..name = p.name - ..type = Reference( - p.type.getDisplayString(withNullability: p.type.isNullable), - ); - - if (p.defaultValueCode != null) { - pb.defaultTo = Code(p.defaultValueCode!); - } - }); - -// Named params can be optional or required, they also need to support -// nullability -Parameter buildNamedParam(ParameterElement p) => - Parameter((ParameterBuilder pb) { - pb - ..named = true - ..name = p.name - ..required = p.isRequiredNamed - ..type = Reference( - p.type.getDisplayString(withNullability: p.type.isNullable), - ); - - if (p.defaultValueCode != null) { - pb.defaultTo = Code(p.defaultValueCode!); - } - }); diff --git a/chopper_generator/lib/src/utils.dart b/chopper_generator/lib/src/utils.dart new file mode 100644 index 00000000..f1be6482 --- /dev/null +++ b/chopper_generator/lib/src/utils.dart @@ -0,0 +1,61 @@ +import 'package:analyzer/dart/element/element.dart'; +import 'package:chopper_generator/src/extensions.dart'; +import 'package:code_builder/code_builder.dart'; +import 'package:source_gen/source_gen.dart'; + +class Utils { + static bool getMethodOptionalBody(ConstantReader method) => + method.read('optionalBody').boolValue; + + static String getMethodPath(ConstantReader method) => + method.read('path').stringValue; + + static String getMethodName(ConstantReader method) => + method.read('method').stringValue; + + static bool getUseBrackets(ConstantReader method) => + method.peek('useBrackets')?.boolValue ?? false; + + static bool getIncludeNullQueryVars(ConstantReader method) => + method.peek('includeNullQueryVars')?.boolValue ?? false; + + /// All positional required params must support nullability + static Parameter buildRequiredPositionalParam(ParameterElement p) => + Parameter( + (ParameterBuilder pb) => pb + ..name = p.name + ..type = Reference( + p.type.getDisplayString(withNullability: p.type.isNullable), + ), + ); + + /// All optional positional params must support nullability + static Parameter buildOptionalPositionalParam(ParameterElement p) => + Parameter((ParameterBuilder pb) { + pb + ..name = p.name + ..type = Reference( + p.type.getDisplayString(withNullability: p.type.isNullable), + ); + + if (p.defaultValueCode != null) { + pb.defaultTo = Code(p.defaultValueCode!); + } + }); + + /// Named params can be optional or required, they also need to support nullability + static Parameter buildNamedParam(ParameterElement p) => + Parameter((ParameterBuilder pb) { + pb + ..named = true + ..name = p.name + ..required = p.isRequiredNamed + ..type = Reference( + p.type.getDisplayString(withNullability: p.type.isNullable), + ); + + if (p.defaultValueCode != null) { + pb.defaultTo = Code(p.defaultValueCode!); + } + }); +} diff --git a/chopper_generator/lib/src/vars.dart b/chopper_generator/lib/src/vars.dart new file mode 100644 index 00000000..cb7c5fdd --- /dev/null +++ b/chopper_generator/lib/src/vars.dart @@ -0,0 +1,17 @@ +enum Vars { + client('client'), + baseUrl('baseUrl'), + parameters(r'$params'), + headers(r'$headers'), + request(r'$request'), + body(r'$body'), + parts(r'$parts'), + url(r'$url'); + + const Vars(this.name); + + final String name; + + @override + String toString() => name; +} diff --git a/chopper_generator/mono_pkg.yaml b/chopper_generator/mono_pkg.yaml index 0620d98d..c0087871 100644 --- a/chopper_generator/mono_pkg.yaml +++ b/chopper_generator/mono_pkg.yaml @@ -6,6 +6,8 @@ stages: - group: - format - analyze: --fatal-infos . +- unit_test: + - test_with_coverage: cache: directories: diff --git a/chopper_generator/pubspec.yaml b/chopper_generator/pubspec.yaml index ee10e5de..8edb7a15 100644 --- a/chopper_generator/pubspec.yaml +++ b/chopper_generator/pubspec.yaml @@ -19,9 +19,12 @@ dependencies: source_gen: ^1.0.0 dev_dependencies: - test: ^1.16.4 + build_runner: ^2.0.0 + build_verify: ^3.1.0 dart_code_metrics: '>=4.8.1 <6.0.0' + http: ">=0.13.0 <2.0.0" lints: ^2.0.0 + test: ^1.16.4 dependency_overrides: # Comment before publish diff --git a/chopper_generator/test/ensure_build_test.dart b/chopper_generator/test/ensure_build_test.dart new file mode 100644 index 00000000..cb5dc60f --- /dev/null +++ b/chopper_generator/test/ensure_build_test.dart @@ -0,0 +1,16 @@ +@TestOn('vm') +@Timeout(Duration(seconds: 120)) +import 'package:build_verify/build_verify.dart'; +import 'package:test/test.dart'; + +void main() { + test( + 'ensure_build', + () => expectBuildClean( + packageRelativeDirectory: 'chopper_generator', + gitDiffPathArguments: [ + 'test/test_service.chopper.dart', + ], + ), + ); +} diff --git a/chopper_generator/test/test_service.chopper.dart b/chopper_generator/test/test_service.chopper.dart new file mode 100644 index 00000000..45e1d737 --- /dev/null +++ b/chopper_generator/test/test_service.chopper.dart @@ -0,0 +1,639 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'test_service.dart'; + +// ************************************************************************** +// ChopperGenerator +// ************************************************************************** + +// ignore_for_file: always_put_control_body_on_new_line, always_specify_types, prefer_const_declarations, unnecessary_brace_in_string_interps +class _$HttpTestService extends HttpTestService { + _$HttpTestService([ChopperClient? client]) { + if (client == null) return; + this.client = client; + } + + @override + final definitionType = HttpTestService; + + @override + Future> getTest( + String id, { + required String dynamicHeader, + }) { + final Uri $url = Uri.parse('/test/get/${id}'); + final Map $headers = { + 'test': dynamicHeader, + }; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + headers: $headers, + ); + return client.send($request); + } + + @override + Future> headTest() { + final Uri $url = Uri.parse('/test/head'); + final Request $request = Request( + 'HEAD', + $url, + client.baseUrl, + ); + return client.send($request); + } + + @override + Future> optionsTest() { + final Uri $url = Uri.parse('/test/options'); + final Request $request = Request( + 'OPTIONS', + $url, + client.baseUrl, + ); + return client.send($request); + } + + @override + Future>>> getStreamTest() { + final Uri $url = Uri.parse('/test/get'); + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + ); + return client.send>, int>($request); + } + + @override + Future> getAll() { + final Uri $url = Uri.parse('/test'); + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + ); + return client.send($request); + } + + @override + Future> getAllWithTrailingSlash() { + final Uri $url = Uri.parse('/test/'); + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + ); + return client.send($request); + } + + @override + Future> getQueryTest({ + String name = '', + int? number, + int? def = 42, + }) { + final Uri $url = Uri.parse('/test/query'); + final Map $params = { + 'name': name, + 'int': number, + 'default_value': def, + }; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + ); + return client.send($request); + } + + @override + Future> getQueryMapTest(Map query) { + final Uri $url = Uri.parse('/test/query_map'); + final Map $params = query; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + ); + return client.send($request); + } + + @override + Future> getQueryMapTest2( + Map query, { + bool? test, + }) { + final Uri $url = Uri.parse('/test/query_map'); + final Map $params = {'test': test}; + $params.addAll(query); + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + ); + return client.send($request); + } + + @override + Future> getQueryMapTest3({ + String name = '', + int? number, + Map filters = const {}, + }) { + final Uri $url = Uri.parse('/test/query_map'); + final Map $params = { + 'name': name, + 'number': number, + }; + $params.addAll(filters); + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + ); + return client.send($request); + } + + @override + Future> getQueryMapTest4({ + String name = '', + int? number, + Map? filters, + }) { + final Uri $url = Uri.parse('/test/query_map'); + final Map $params = { + 'name': name, + 'number': number, + }; + $params.addAll(filters ?? const {}); + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + ); + return client.send($request); + } + + @override + Future> getQueryMapTest5({Map? filters}) { + final Uri $url = Uri.parse('/test/query_map'); + final Map $params = filters ?? const {}; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + ); + return client.send($request); + } + + @override + Future> getBody(dynamic body) { + final Uri $url = Uri.parse('/test/get_body'); + final $body = body; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + body: $body, + ); + return client.send($request); + } + + @override + Future> postTest(String data) { + final Uri $url = Uri.parse('/test/post'); + final $body = data; + final Request $request = Request( + 'POST', + $url, + client.baseUrl, + body: $body, + ); + return client.send($request); + } + + @override + Future> postStreamTest(Stream> byteStream) { + final Uri $url = Uri.parse('/test/post'); + final $body = byteStream; + final Request $request = Request( + 'POST', + $url, + client.baseUrl, + body: $body, + ); + return client.send($request); + } + + @override + Future> putTest( + String test, + String data, + ) { + final Uri $url = Uri.parse('/test/put/${test}'); + final $body = data; + final Request $request = Request( + 'PUT', + $url, + client.baseUrl, + body: $body, + ); + return client.send($request); + } + + @override + Future> deleteTest(String id) { + final Uri $url = Uri.parse('/test/delete/${id}'); + final Map $headers = { + 'foo': 'bar', + }; + final Request $request = Request( + 'DELETE', + $url, + client.baseUrl, + headers: $headers, + ); + return client.send($request); + } + + @override + Future> patchTest( + String id, + String data, + ) { + final Uri $url = Uri.parse('/test/patch/${id}'); + final $body = data; + final Request $request = Request( + 'PATCH', + $url, + client.baseUrl, + body: $body, + ); + return client.send($request); + } + + @override + Future> mapTest(Map map) { + final Uri $url = Uri.parse('/test/map'); + final $body = map; + final Request $request = Request( + 'POST', + $url, + client.baseUrl, + body: $body, + ); + return client.send($request); + } + + @override + Future> postForm(Map fields) { + final Uri $url = Uri.parse('/test/form/body'); + final $body = fields; + final Request $request = Request( + 'POST', + $url, + client.baseUrl, + body: $body, + ); + return client.send( + $request, + requestConverter: convertForm, + ); + } + + @override + Future> postFormUsingHeaders(Map fields) { + final Uri $url = Uri.parse('/test/form/body'); + final Map $headers = { + 'content-type': 'application/x-www-form-urlencoded', + }; + final $body = fields; + final Request $request = Request( + 'POST', + $url, + client.baseUrl, + body: $body, + headers: $headers, + ); + return client.send($request); + } + + @override + Future> postFormFields( + String foo, + int bar, + ) { + final Uri $url = Uri.parse('/test/form/body/fields'); + final $body = { + 'foo': foo, + 'bar': bar, + }; + final Request $request = Request( + 'POST', + $url, + client.baseUrl, + body: $body, + ); + return client.send( + $request, + requestConverter: convertForm, + ); + } + + @override + Future> forceJsonTest(Map map) { + final Uri $url = Uri.parse('/test/map/json'); + final $body = map; + final Request $request = Request( + 'POST', + $url, + client.baseUrl, + body: $body, + ); + return client.send( + $request, + requestConverter: customConvertRequest, + responseConverter: customConvertResponse, + ); + } + + @override + Future> postResources( + Map a, + Map b, + ) { + final Uri $url = Uri.parse('/test/multi'); + final List $parts = [ + PartValue>( + '1', + a, + ), + PartValue>( + '2', + b, + ), + ]; + final Request $request = Request( + 'POST', + $url, + client.baseUrl, + parts: $parts, + multipart: true, + ); + return client.send($request); + } + + @override + Future> postFile(List bytes) { + final Uri $url = Uri.parse('/test/file'); + final List $parts = [ + PartValueFile>( + 'file', + bytes, + ) + ]; + final Request $request = Request( + 'POST', + $url, + client.baseUrl, + parts: $parts, + multipart: true, + ); + return client.send($request); + } + + @override + Future> postImage(List imageData) { + final Uri $url = Uri.parse('/test/image'); + final List $parts = [ + PartValueFile>( + 'image', + imageData, + ) + ]; + final Request $request = Request( + 'POST', + $url, + client.baseUrl, + parts: $parts, + multipart: true, + ); + return client.send($request); + } + + @override + Future> postMultipartFile( + MultipartFile file, { + String? id, + }) { + final Uri $url = Uri.parse('/test/file'); + final List $parts = [ + PartValue( + 'id', + id, + ), + PartValueFile( + 'file', + file, + ), + ]; + final Request $request = Request( + 'POST', + $url, + client.baseUrl, + parts: $parts, + multipart: true, + ); + return client.send($request); + } + + @override + Future> postListFiles(List files) { + final Uri $url = Uri.parse('/test/files'); + final List $parts = [ + PartValueFile>( + 'files', + files, + ) + ]; + final Request $request = Request( + 'POST', + $url, + client.baseUrl, + parts: $parts, + multipart: true, + ); + return client.send($request); + } + + @override + Future> postMultipartList({ + required List ints, + required List doubles, + required List nums, + required List strings, + }) { + final Uri $url = Uri.parse('/test/multipart_list'); + final List $parts = [ + PartValue>( + 'ints', + ints, + ), + PartValue>( + 'doubles', + doubles, + ), + PartValue>( + 'nums', + nums, + ), + PartValue>( + 'strings', + strings, + ), + ]; + final Request $request = Request( + 'POST', + $url, + client.baseUrl, + parts: $parts, + multipart: true, + ); + return client.send($request); + } + + @override + Future fullUrl() { + final Uri $url = Uri.parse('https://test.com'); + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + ); + return client.send($request); + } + + @override + Future>> listString() { + final Uri $url = Uri.parse('/test/list/string'); + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + ); + return client.send, String>($request); + } + + @override + Future> noBody() { + final Uri $url = Uri.parse('/test/no-body'); + final Request $request = Request( + 'POST', + $url, + client.baseUrl, + ); + return client.send($request); + } + + @override + Future> getUsingQueryParamIncludeNullQueryVars({ + String? foo, + String? bar, + String? baz, + }) { + final Uri $url = Uri.parse('/test/query_param_include_null_query_vars'); + final Map $params = { + 'foo': foo, + 'bar': bar, + 'baz': baz, + }; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + includeNullQueryVars: true, + ); + return client.send($request); + } + + @override + Future> getUsingListQueryParam(List value) { + final Uri $url = Uri.parse('/test/list_query_param'); + final Map $params = {'value': value}; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + ); + return client.send($request); + } + + @override + Future> getUsingListQueryParamWithBrackets( + List value) { + final Uri $url = Uri.parse('/test/list_query_param_with_brackets'); + final Map $params = {'value': value}; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + useBrackets: true, + ); + return client.send($request); + } + + @override + Future> getUsingMapQueryParam(Map value) { + final Uri $url = Uri.parse('/test/map_query_param'); + final Map $params = {'value': value}; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + ); + return client.send($request); + } + + @override + Future> getUsingMapQueryParamIncludeNullQueryVars( + Map value) { + final Uri $url = Uri.parse('/test/map_query_param_include_null_query_vars'); + final Map $params = {'value': value}; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + includeNullQueryVars: true, + ); + return client.send($request); + } + + @override + Future> getUsingMapQueryParamWithBrackets( + Map value) { + final Uri $url = Uri.parse('/test/map_query_param_with_brackets'); + final Map $params = {'value': value}; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + useBrackets: true, + ); + return client.send($request); + } +} diff --git a/chopper_generator/test/test_service.dart b/chopper_generator/test/test_service.dart new file mode 100644 index 00000000..c0baf1a4 --- /dev/null +++ b/chopper_generator/test/test_service.dart @@ -0,0 +1,218 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:chopper/chopper.dart'; +import 'package:http/http.dart' show MultipartFile; + +part 'test_service.chopper.dart'; + +@ChopperApi(baseUrl: '/test') +abstract class HttpTestService extends ChopperService { + static HttpTestService create([ChopperClient? client]) => + _$HttpTestService(client); + + @Get(path: 'get/{id}') + Future> getTest( + @Path() String id, { + @Header('test') required String dynamicHeader, + }); + + @Head(path: 'head') + Future headTest(); + + @Options(path: 'options') + Future optionsTest(); + + @Get(path: 'get') + Future>>> getStreamTest(); + + @Get(path: '') + Future getAll(); + + @Get(path: '/') + Future getAllWithTrailingSlash(); + + @Get(path: 'query') + Future getQueryTest({ + @Query('name') String name = '', + @Query('int') int? number, + @Query('default_value') int? def = 42, + }); + + @Get(path: 'query_map') + Future getQueryMapTest(@QueryMap() Map query); + + @Get(path: 'query_map') + Future getQueryMapTest2( + @QueryMap() Map query, { + @Query('test') bool? test, + }); + + @Get(path: 'query_map') + Future getQueryMapTest3({ + @Query('name') String name = '', + @Query('number') int? number, + @QueryMap() Map filters = const {}, + }); + + @Get(path: 'query_map') + Future getQueryMapTest4({ + @Query('name') String name = '', + @Query('number') int? number, + @QueryMap() Map? filters, + }); + + @Get(path: 'query_map') + Future getQueryMapTest5({ + @QueryMap() Map? filters, + }); + + @Get(path: 'get_body') + Future getBody(@Body() dynamic body); + + @Post(path: 'post') + Future postTest(@Body() String data); + + @Post(path: 'post') + Future postStreamTest(@Body() Stream> byteStream); + + @Put(path: 'put/{id}') + Future putTest(@Path('id') String test, @Body() String data); + + @Delete(path: 'delete/{id}', headers: {'foo': 'bar'}) + Future deleteTest(@Path() String id); + + @Patch(path: 'patch/{id}') + Future patchTest(@Path() String id, @Body() String data); + + @Post(path: 'map') + Future mapTest(@Body() Map map); + + @FactoryConverter(request: convertForm) + @Post(path: 'form/body') + Future postForm(@Body() Map fields); + + @Post(path: 'form/body', headers: {contentTypeKey: formEncodedHeaders}) + Future postFormUsingHeaders(@Body() Map fields); + + @FactoryConverter(request: convertForm) + @Post(path: 'form/body/fields') + Future postFormFields(@Field() String foo, @Field() int bar); + + @Post(path: 'map/json') + @FactoryConverter( + request: customConvertRequest, + response: customConvertResponse, + ) + Future forceJsonTest(@Body() Map map); + + @Post(path: 'multi') + @multipart + Future postResources( + @Part('1') Map a, + @Part('2') Map b, + ); + + @Post(path: 'file') + @multipart + Future postFile( + @PartFile('file') List bytes, + ); + + @Post(path: 'image') + @multipart + Future postImage( + @PartFile('image') List imageData, + ); + + @Post(path: 'file') + @multipart + Future postMultipartFile( + @PartFile() MultipartFile file, { + @Part() String? id, + }); + + @Post(path: 'files') + @multipart + Future postListFiles(@PartFile() List files); + + @Post(path: 'multipart_list') + @multipart + Future postMultipartList({ + @Part('ints') required List ints, + @Part('doubles') required List doubles, + @Part('nums') required List nums, + @Part('strings') required List strings, + }); + + @Get(path: 'https://test.com') + Future fullUrl(); + + @Get(path: '/list/string') + Future>> listString(); + + @Post(path: 'no-body') + Future noBody(); + + @Get(path: '/query_param_include_null_query_vars', includeNullQueryVars: true) + Future> getUsingQueryParamIncludeNullQueryVars({ + @Query('foo') String? foo, + @Query('bar') String? bar, + @Query('baz') String? baz, + }); + + @Get(path: '/list_query_param') + Future> getUsingListQueryParam( + @Query('value') List value, + ); + + @Get(path: '/list_query_param_with_brackets', useBrackets: true) + Future> getUsingListQueryParamWithBrackets( + @Query('value') List value, + ); + + @Get(path: '/map_query_param') + Future> getUsingMapQueryParam( + @Query('value') Map value, + ); + + @Get( + path: '/map_query_param_include_null_query_vars', + includeNullQueryVars: true, + ) + Future> getUsingMapQueryParamIncludeNullQueryVars( + @Query('value') Map value, + ); + + @Get(path: '/map_query_param_with_brackets', useBrackets: true) + Future> getUsingMapQueryParamWithBrackets( + @Query('value') Map value, + ); +} + +Request customConvertRequest(Request req) { + final r = JsonConverter().convertRequest(req); + + return applyHeader(r, 'customConverter', 'true'); +} + +Response customConvertResponse(Response res) => + res.copyWith(body: json.decode(res.body)); + +Request convertForm(Request req) { + req = applyHeader(req, contentTypeKey, formEncodedHeaders); + + if (req.body is Map) { + final body = {}; + + req.body.forEach((key, val) { + if (val != null) { + body[key.toString()] = val.toString(); + } + }); + + req = req.copyWith(body: body); + } + + return req; +} From 0856b86244a5343e159000af7a03184a1ca1172e Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Wed, 5 Jul 2023 10:52:15 +0100 Subject: [PATCH 073/171] :twisted_rightwards_arrows: post release branch sync (#450) --- chopper_generator/CHANGELOG.md | 13 +++++++++++++ chopper_generator/pubspec.yaml | 2 +- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/chopper_generator/CHANGELOG.md b/chopper_generator/CHANGELOG.md index 881ab59e..36c3e7ca 100644 --- a/chopper_generator/CHANGELOG.md +++ b/chopper_generator/CHANGELOG.md @@ -1,5 +1,18 @@ # Changelog +## 6.0.3 + +- Simplify library export +- Extract PartBuilder into its own file +- Extract constant variables into an enum +- Extract helper methods into a Utils class +- Use a const constructor +- Make all methods static +- Ensure all immutable variables are final +- Simplify syntax +- Add API documentation +- Update README + ## 6.0.2 - Add support for generating files in different directories ([#444](https://github.com/lejard-h/chopper/pull/444)) diff --git a/chopper_generator/pubspec.yaml b/chopper_generator/pubspec.yaml index 8edb7a15..fc438eca 100644 --- a/chopper_generator/pubspec.yaml +++ b/chopper_generator/pubspec.yaml @@ -1,6 +1,6 @@ name: chopper_generator description: Chopper is an http client generator using source_gen, inspired by Retrofit -version: 6.0.2 +version: 6.0.3 documentation: https://hadrien-lejard.gitbook.io/chopper repository: https://github.com/lejard-h/chopper From 96f9dbc7baaf38cf4cc0e59b860954274ff09850 Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Sun, 9 Jul 2023 12:34:47 +0100 Subject: [PATCH 074/171] :heavy_minus_sign: remove dart_code_metrics from dev_dependencies (#452) * :heavy_minus_sign: remove dart_code_metrics as a dev dependency * :technologist: update examples --- chopper/analysis_options.yaml | 21 -- chopper/pubspec.yaml | 1 - chopper_built_value/analysis_options.yaml | 21 -- chopper_built_value/pubspec.yaml | 1 - chopper_generator/analysis_options.yaml | 22 -- chopper_generator/pubspec.yaml | 1 - example/analysis_options.yaml | 20 -- .../lib/json_decode_service.activator.g.dart | 3 +- example/lib/json_decode_service.dart | 3 +- example/lib/json_decode_service.vm.g.dart | 12 +- example/lib/json_decode_service.worker.g.dart | 207 ++++++++++-------- example/pubspec.yaml | 21 +- 12 files changed, 132 insertions(+), 201 deletions(-) diff --git a/chopper/analysis_options.yaml b/chopper/analysis_options.yaml index 7f5a674f..6f56a451 100644 --- a/chopper/analysis_options.yaml +++ b/chopper/analysis_options.yaml @@ -6,27 +6,6 @@ analyzer: - "**.chopper.dart" - "**.mocks.dart" - "example/**" - plugins: - - dart_code_metrics - -dart_code_metrics: - metrics: - cyclomatic-complexity: 20 - number-of-arguments: 4 - maximum-nesting-level: 5 - number-of-parameters: 7 - metrics-exclude: - - test/** - rules: - - newline-before-return - - no-boolean-literal-compare - - no-empty-block - - prefer-trailing-comma - - prefer-conditional-expressions - - no-equal-then-else - anti-patterns: - - long-method - - long-parameter-list linter: rules: diff --git a/chopper/pubspec.yaml b/chopper/pubspec.yaml index 2af5cfbf..ba990c81 100644 --- a/chopper/pubspec.yaml +++ b/chopper/pubspec.yaml @@ -19,7 +19,6 @@ dev_dependencies: build_verify: ^3.1.0 collection: ^1.16.0 coverage: ^1.0.2 - dart_code_metrics: '>=4.8.1 <6.0.0' data_fixture_dart: ^2.2.0 faker: ^2.1.0 http_parser: ^4.0.0 diff --git a/chopper_built_value/analysis_options.yaml b/chopper_built_value/analysis_options.yaml index 7f5a674f..6f56a451 100644 --- a/chopper_built_value/analysis_options.yaml +++ b/chopper_built_value/analysis_options.yaml @@ -6,27 +6,6 @@ analyzer: - "**.chopper.dart" - "**.mocks.dart" - "example/**" - plugins: - - dart_code_metrics - -dart_code_metrics: - metrics: - cyclomatic-complexity: 20 - number-of-arguments: 4 - maximum-nesting-level: 5 - number-of-parameters: 7 - metrics-exclude: - - test/** - rules: - - newline-before-return - - no-boolean-literal-compare - - no-empty-block - - prefer-trailing-comma - - prefer-conditional-expressions - - no-equal-then-else - anti-patterns: - - long-method - - long-parameter-list linter: rules: diff --git a/chopper_built_value/pubspec.yaml b/chopper_built_value/pubspec.yaml index 1eed9725..65c3ab03 100644 --- a/chopper_built_value/pubspec.yaml +++ b/chopper_built_value/pubspec.yaml @@ -18,7 +18,6 @@ dev_dependencies: build_runner: ^2.0.0 build_test: ^2.0.0 built_value_generator: ^8.0.6 - dart_code_metrics: '>=4.8.1 <6.0.0' lints: ^2.0.0 dependency_overrides: diff --git a/chopper_generator/analysis_options.yaml b/chopper_generator/analysis_options.yaml index 2caa0f09..6f56a451 100644 --- a/chopper_generator/analysis_options.yaml +++ b/chopper_generator/analysis_options.yaml @@ -6,28 +6,6 @@ analyzer: - "**.chopper.dart" - "**.mocks.dart" - "example/**" - plugins: - - dart_code_metrics - -dart_code_metrics: - metrics: - cyclomatic-complexity: 20 - number-of-arguments: 4 - maximum-nesting-level: 5 - number-of-parameters: 10 - source-lines-of-code: 250 - metrics-exclude: - - test/** - rules: - - newline-before-return - - no-boolean-literal-compare - - no-empty-block - - prefer-trailing-comma - - prefer-conditional-expressions - - no-equal-then-else - anti-patterns: - - long-method - - long-parameter-list linter: rules: diff --git a/chopper_generator/pubspec.yaml b/chopper_generator/pubspec.yaml index fc438eca..645318cd 100644 --- a/chopper_generator/pubspec.yaml +++ b/chopper_generator/pubspec.yaml @@ -21,7 +21,6 @@ dependencies: dev_dependencies: build_runner: ^2.0.0 build_verify: ^3.1.0 - dart_code_metrics: '>=4.8.1 <6.0.0' http: ">=0.13.0 <2.0.0" lints: ^2.0.0 test: ^1.16.4 diff --git a/example/analysis_options.yaml b/example/analysis_options.yaml index 7061686f..8ed20e77 100644 --- a/example/analysis_options.yaml +++ b/example/analysis_options.yaml @@ -5,26 +5,6 @@ analyzer: - "**.g.dart" - "**.chopper.dart" - "**.mocks.dart" - plugins: - - dart_code_metrics - -dart_code_metrics: - metrics: - cyclomatic-complexity: 20 - number-of-arguments: 4 - maximum-nesting-level: 5 - metrics-exclude: - - test/** - rules: - - newline-before-return - - no-boolean-literal-compare - - no-empty-block - - prefer-trailing-comma - - prefer-conditional-expressions - - no-equal-then-else - anti-patterns: - - long-method - - long-parameter-list linter: rules: diff --git a/example/lib/json_decode_service.activator.g.dart b/example/lib/json_decode_service.activator.g.dart index 1e151e3c..48ed6298 100644 --- a/example/lib/json_decode_service.activator.g.dart +++ b/example/lib/json_decode_service.activator.g.dart @@ -1,9 +1,10 @@ // GENERATED CODE - DO NOT MODIFY BY HAND // ************************************************************************** -// Generated by: WorkerGenerator +// Generator: WorkerGenerator 2.4.1 // ************************************************************************** import 'json_decode_service.vm.g.dart'; +/// Service activator for JsonDecodeService final $JsonDecodeServiceActivator = $getJsonDecodeServiceActivator(); diff --git a/example/lib/json_decode_service.dart b/example/lib/json_decode_service.dart index 41c94785..299b73c3 100644 --- a/example/lib/json_decode_service.dart +++ b/example/lib/json_decode_service.dart @@ -14,8 +14,7 @@ part 'json_decode_service.worker.g.dart'; // disable web to keep the number of generated files low for this example web: false, ) -class JsonDecodeService extends WorkerService - with $JsonDecodeServiceOperations { +class JsonDecodeService { @SquadronMethod() Future jsonDecode(String source) async => json.decode(source); } diff --git a/example/lib/json_decode_service.vm.g.dart b/example/lib/json_decode_service.vm.g.dart index 656bcef1..09d73096 100644 --- a/example/lib/json_decode_service.vm.g.dart +++ b/example/lib/json_decode_service.vm.g.dart @@ -1,13 +1,15 @@ // GENERATED CODE - DO NOT MODIFY BY HAND // ************************************************************************** -// Generated by: WorkerGenerator +// Generator: WorkerGenerator 2.4.1 // ************************************************************************** -import 'package:squadron/squadron_service.dart'; +import 'package:squadron/squadron.dart'; + import 'json_decode_service.dart'; -// VM entry point -void _start(List command) => run($JsonDecodeServiceInitializer, command, null); +/// VM entry point for JsonDecodeService +void _start$JsonDecodeService(List command) => + run($JsonDecodeServiceInitializer, command, null); -dynamic $getJsonDecodeServiceActivator() => _start; +EntryPoint $getJsonDecodeServiceActivator() => _start$JsonDecodeService; diff --git a/example/lib/json_decode_service.worker.g.dart b/example/lib/json_decode_service.worker.g.dart index 0cd306ce..18a33d72 100644 --- a/example/lib/json_decode_service.worker.g.dart +++ b/example/lib/json_decode_service.worker.g.dart @@ -3,53 +3,64 @@ part of 'json_decode_service.dart'; // ************************************************************************** -// WorkerGenerator +// Generator: WorkerGenerator 2.4.1 // ************************************************************************** -// Operations map for JsonDecodeService -mixin $JsonDecodeServiceOperations on WorkerService { +/// WorkerService class for JsonDecodeService +class _$JsonDecodeServiceWorkerService extends JsonDecodeService + implements WorkerService { + _$JsonDecodeServiceWorkerService() : super(); + @override - late final Map operations = - _getOperations(this as JsonDecodeService); + Map get operations => _operations; - static const int _$jsonDecodeId = 1; + late final Map _operations = { + _$jsonDecodeId: ($) => jsonDecode($.args[0]) + }; - static Map _getOperations(JsonDecodeService svc) => - {_$jsonDecodeId: (req) => svc.jsonDecode(req.args[0])}; + static const int _$jsonDecodeId = 1; } -// Service initializer -JsonDecodeService $JsonDecodeServiceInitializer(WorkerRequest startRequest) => - JsonDecodeService(); - -// Worker for JsonDecodeService -class _JsonDecodeServiceWorker extends Worker - with $JsonDecodeServiceOperations - implements JsonDecodeService { - _JsonDecodeServiceWorker() : super($JsonDecodeServiceActivator); +/// Service initializer for JsonDecodeService +WorkerService $JsonDecodeServiceInitializer(WorkerRequest startRequest) => + _$JsonDecodeServiceWorkerService(); +/// Operations map for JsonDecodeService +@Deprecated( + 'squadron_builder now supports "plain old Dart objects" as services. ' + 'Services do not need to derive from WorkerService nor do they need to mix in ' + 'with \$JsonDecodeServiceOperations anymore.') +mixin $JsonDecodeServiceOperations on WorkerService { @override - Future jsonDecode(String source) => send( - $JsonDecodeServiceOperations._$jsonDecodeId, - args: [source], - ); + // not needed anymore, generated for compatibility with previous versions of squadron_builder + Map get operations => WorkerService.noOperations; +} + +/// Worker for JsonDecodeService +class _$JsonDecodeServiceWorker extends Worker implements JsonDecodeService { + _$JsonDecodeServiceWorker({PlatformWorkerHook? platformWorkerHook}) + : super($JsonDecodeServiceActivator, + platformWorkerHook: platformWorkerHook); @override - Map get operations => WorkerService.noOperations; + Future jsonDecode(String source) => + send(_$JsonDecodeServiceWorkerService._$jsonDecodeId, args: [source]); final Object _detachToken = Object(); } -// Finalizable worker wrapper for JsonDecodeService -class JsonDecodeServiceWorker implements _JsonDecodeServiceWorker { - JsonDecodeServiceWorker() : _worker = _JsonDecodeServiceWorker() { - _finalizer.attach(this, _worker, detach: _worker._detachToken); +/// Finalizable worker wrapper for JsonDecodeService +class JsonDecodeServiceWorker implements _$JsonDecodeServiceWorker { + JsonDecodeServiceWorker({PlatformWorkerHook? platformWorkerHook}) + : _$w = + _$JsonDecodeServiceWorker(platformWorkerHook: platformWorkerHook) { + _finalizer.attach(this, _$w, detach: _$w._detachToken); } - final _JsonDecodeServiceWorker _worker; + final _$JsonDecodeServiceWorker _$w; - static final Finalizer<_JsonDecodeServiceWorker> _finalizer = - Finalizer<_JsonDecodeServiceWorker>((w) { + static final Finalizer<_$JsonDecodeServiceWorker> _finalizer = + Finalizer<_$JsonDecodeServiceWorker>((w) { try { _finalizer.detach(w._detachToken); w.stop(); @@ -59,52 +70,52 @@ class JsonDecodeServiceWorker implements _JsonDecodeServiceWorker { }); @override - Future jsonDecode(String source) => _worker.jsonDecode(source); + Future jsonDecode(String source) => _$w.jsonDecode(source); @override - Map get operations => _worker.operations; + List get args => _$w.args; @override - List get args => _worker.args; + Channel? get channel => _$w.channel; @override - Channel? get channel => _worker.channel; + Duration get idleTime => _$w.idleTime; @override - Duration get idleTime => _worker.idleTime; + bool get isStopped => _$w.isStopped; @override - bool get isStopped => _worker.isStopped; + int get maxWorkload => _$w.maxWorkload; @override - int get maxWorkload => _worker.maxWorkload; + WorkerStat get stats => _$w.stats; @override - WorkerStat get stats => _worker.stats; + String get status => _$w.status; @override - String get status => _worker.status; + int get totalErrors => _$w.totalErrors; @override - int get totalErrors => _worker.totalErrors; + int get totalWorkload => _$w.totalWorkload; @override - int get totalWorkload => _worker.totalWorkload; + Duration get upTime => _$w.upTime; @override - Duration get upTime => _worker.upTime; + String get workerId => _$w.workerId; @override - String get workerId => _worker.workerId; + int get workload => _$w.workload; @override - int get workload => _worker.workload; + PlatformWorkerHook? get platformWorkerHook => _$w.platformWorkerHook; @override - Future start() => _worker.start(); + Future start() => _$w.start(); @override - void stop() => _worker.stop(); + void stop() => _$w.stop(); @override Future send(int command, @@ -112,7 +123,7 @@ class JsonDecodeServiceWorker implements _JsonDecodeServiceWorker { CancellationToken? token, bool inspectRequest = false, bool inspectResponse = false}) => - _worker.send(command, + _$w.send(command, args: args, token: token, inspectRequest: inspectRequest, @@ -124,46 +135,52 @@ class JsonDecodeServiceWorker implements _JsonDecodeServiceWorker { CancellationToken? token, bool inspectRequest = false, bool inspectResponse = false}) => - _worker.stream(command, + _$w.stream(command, args: args, token: token, inspectRequest: inspectRequest, inspectResponse: inspectResponse); @override - Object get _detachToken => _worker._detachToken; + Object get _detachToken => _$w._detachToken; + + @override + Map get operations => WorkerService.noOperations; } -// Worker pool for JsonDecodeService -class _JsonDecodeServiceWorkerPool extends WorkerPool - with $JsonDecodeServiceOperations +/// Worker pool for JsonDecodeService +class _$JsonDecodeServiceWorkerPool extends WorkerPool implements JsonDecodeService { - _JsonDecodeServiceWorkerPool({ConcurrencySettings? concurrencySettings}) - : super(() => JsonDecodeServiceWorker(), + _$JsonDecodeServiceWorkerPool( + {ConcurrencySettings? concurrencySettings, + PlatformWorkerHook? platformWorkerHook}) + : super( + () => + JsonDecodeServiceWorker(platformWorkerHook: platformWorkerHook), concurrencySettings: concurrencySettings); @override Future jsonDecode(String source) => - execute(($w) => $w.jsonDecode(source)); - - @override - Map get operations => WorkerService.noOperations; + execute((w) => w.jsonDecode(source)); final Object _detachToken = Object(); } -// Finalizable worker pool wrapper for JsonDecodeService -class JsonDecodeServiceWorkerPool implements _JsonDecodeServiceWorkerPool { - JsonDecodeServiceWorkerPool({ConcurrencySettings? concurrencySettings}) - : _pool = _JsonDecodeServiceWorkerPool( - concurrencySettings: concurrencySettings) { - _finalizer.attach(this, _pool, detach: _pool._detachToken); +/// Finalizable worker pool wrapper for JsonDecodeService +class JsonDecodeServiceWorkerPool implements _$JsonDecodeServiceWorkerPool { + JsonDecodeServiceWorkerPool( + {ConcurrencySettings? concurrencySettings, + PlatformWorkerHook? platformWorkerHook}) + : _$p = _$JsonDecodeServiceWorkerPool( + concurrencySettings: concurrencySettings, + platformWorkerHook: platformWorkerHook) { + _finalizer.attach(this, _$p, detach: _$p._detachToken); } - final _JsonDecodeServiceWorkerPool _pool; + final _$JsonDecodeServiceWorkerPool _$p; - static final Finalizer<_JsonDecodeServiceWorkerPool> _finalizer = - Finalizer<_JsonDecodeServiceWorkerPool>((p) { + static final Finalizer<_$JsonDecodeServiceWorkerPool> _finalizer = + Finalizer<_$JsonDecodeServiceWorkerPool>((p) { try { _finalizer.detach(p._detachToken); p.stop(); @@ -173,101 +190,101 @@ class JsonDecodeServiceWorkerPool implements _JsonDecodeServiceWorkerPool { }); @override - Future jsonDecode(String source) => _pool.jsonDecode(source); + Future jsonDecode(String source) => _$p.jsonDecode(source); @override - Map get operations => _pool.operations; + ConcurrencySettings get concurrencySettings => _$p.concurrencySettings; @override - ConcurrencySettings get concurrencySettings => _pool.concurrencySettings; + Iterable get fullStats => _$p.fullStats; @override - Iterable get fullStats => _pool.fullStats; + int get maxConcurrency => _$p.maxConcurrency; @override - int get maxConcurrency => _pool.maxConcurrency; + int get maxParallel => _$p.maxParallel; @override - int get maxParallel => _pool.maxParallel; + int get maxSize => _$p.maxSize; @override - int get maxSize => _pool.maxSize; + int get maxWorkers => _$p.maxWorkers; @override - int get maxWorkers => _pool.maxWorkers; + int get maxWorkload => _$p.maxWorkload; @override - int get maxWorkload => _pool.maxWorkload; + int get minWorkers => _$p.minWorkers; @override - int get minWorkers => _pool.minWorkers; + int get pendingWorkload => _$p.pendingWorkload; @override - int get pendingWorkload => _pool.pendingWorkload; + int get size => _$p.size; @override - int get size => _pool.size; + Iterable get stats => _$p.stats; @override - Iterable get stats => _pool.stats; + bool get stopped => _$p.stopped; @override - bool get stopped => _pool.stopped; + int get totalErrors => _$p.totalErrors; @override - int get totalErrors => _pool.totalErrors; + int get totalWorkload => _$p.totalWorkload; @override - int get totalWorkload => _pool.totalWorkload; + int get workload => _$p.workload; @override - int get workload => _pool.workload; + void cancel([Task? task, String? message]) => _$p.cancel(task, message); @override - void cancel([Task? task, String? message]) => _pool.cancel(task, message); - - @override - FutureOr start() => _pool.start(); + FutureOr start() => _$p.start(); @override int stop([bool Function(JsonDecodeServiceWorker worker)? predicate]) => - _pool.stop(predicate); + _$p.stop(predicate); @override Object registerWorkerPoolListener( void Function(JsonDecodeServiceWorker worker, bool removed) listener) => - _pool.registerWorkerPoolListener(listener); + _$p.registerWorkerPoolListener(listener); @override void unregisterWorkerPoolListener( {void Function(JsonDecodeServiceWorker worker, bool removed)? listener, Object? token}) => - _pool.unregisterWorkerPoolListener(listener: listener, token: token); + _$p.unregisterWorkerPoolListener(listener: listener, token: token); @override Future execute(Future Function(JsonDecodeServiceWorker worker) task, {PerfCounter? counter}) => - _pool.execute(task, counter: counter); + _$p.execute(task, counter: counter); @override StreamTask scheduleStream( Stream Function(JsonDecodeServiceWorker worker) task, {PerfCounter? counter}) => - _pool.scheduleStream(task, counter: counter); + _$p.scheduleStream(task, counter: counter); @override ValueTask scheduleTask( Future Function(JsonDecodeServiceWorker worker) task, {PerfCounter? counter}) => - _pool.scheduleTask(task, counter: counter); + _$p.scheduleTask(task, counter: counter); @override Stream stream(Stream Function(JsonDecodeServiceWorker worker) task, {PerfCounter? counter}) => - _pool.stream(task, counter: counter); + _$p.stream(task, counter: counter); @override - Object get _detachToken => _pool._detachToken; + Object get _detachToken => _$p._detachToken; + + @override + Map get operations => WorkerService.noOperations; } diff --git a/example/pubspec.yaml b/example/pubspec.yaml index d88b0adf..18b93a17 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -9,21 +9,20 @@ environment: dependencies: chopper: - json_annotation: + json_annotation: ^4.8.1 built_value: - analyzer: - http: - built_collection: - squadron: ^5.0.0 + analyzer: ^5.13.0 + http: ^1.1.0 + built_collection: ^5.1.1 + squadron: ^5.1.3 dev_dependencies: - build_runner: + build_runner: ^2.4.6 chopper_generator: - json_serializable: - built_value_generator: - dart_code_metrics: '>=4.8.1 <6.0.0' - lints: ^2.0.0 - squadron_builder: ^2.1.2 + json_serializable: ^6.7.1 + built_value_generator: ^8.6.1 + lints: ^2.1.1 + squadron_builder: ^2.4.1 dependency_overrides: chopper: From 6afebb0f6712698c60b9f0aa8016af9c99f31275 Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Sat, 29 Jul 2023 17:16:56 +0100 Subject: [PATCH 075/171] :sparkles: implement Dart 3 class modifiers (#453) * bump minimum Dart SDK version to 3.0.0 * bump dependencies to the latest (except analyzer) * implement Dart 3 class modifiers --- chopper/lib/src/annotations.dart | 44 +++++++++---------- chopper/lib/src/base.dart | 4 +- chopper/lib/src/constants.dart | 2 +- chopper/lib/src/interceptor.dart | 8 ++-- chopper/lib/src/request.dart | 6 +-- chopper/lib/src/response.dart | 2 +- chopper/lib/src/utils.dart | 2 +- chopper/pubspec.yaml | 26 +++++------ .../test/fixtures/http_response_fixture.dart | 2 +- chopper/test/fixtures/payload_fixture.dart | 2 +- chopper/test/fixtures/request_fixture.dart | 2 +- chopper/test/fixtures/response_fixture.dart | 2 +- chopper/test/helpers/payload.dart | 2 +- chopper_built_value/pubspec.yaml | 22 +++++----- chopper_generator/lib/src/generator.dart | 9 ++-- chopper_generator/lib/src/utils.dart | 2 +- chopper_generator/pubspec.yaml | 30 ++++++------- example/pubspec.yaml | 2 +- 18 files changed, 84 insertions(+), 85 deletions(-) diff --git a/chopper/lib/src/annotations.dart b/chopper/lib/src/annotations.dart index e7fa787c..9030d432 100644 --- a/chopper/lib/src/annotations.dart +++ b/chopper/lib/src/annotations.dart @@ -20,7 +20,7 @@ import 'package:meta/meta.dart'; /// /// See [Method] to define an HTTP request @immutable -class ChopperApi { +final class ChopperApi { /// A part of a URL that every request defined inside a class annotated with [ChopperApi] will be prefixed with. final String baseUrl; @@ -42,7 +42,7 @@ class ChopperApi { /// Future fetch(@Path() String param); /// ``` @immutable -class Path { +final class Path { /// Name is used to bind a method parameter to /// a URL path parameter. /// ```dart @@ -65,7 +65,7 @@ class Path { /// /// See [QueryMap] to pass an [Map] as value @immutable -class Query { +final class Query { /// Name is used to bind a method parameter to /// the query parameter. /// ```dart @@ -90,7 +90,7 @@ class Query { /// // something?foo=bar&list=1&list=2 /// ``` @immutable -class QueryMap { +final class QueryMap { const QueryMap(); } @@ -104,7 +104,7 @@ class QueryMap { /// The body can be of any type, but chopper does not automatically convert it to JSON. /// See [Converter] to apply conversion to the body. @immutable -class Body { +final class Body { const Body(); } @@ -117,7 +117,7 @@ class Body { /// Future fetch(@Header() String foo); /// ``` @immutable -class Header { +final class Header { /// Name is used to bind a method parameter to /// a header name. /// ```dart @@ -147,7 +147,7 @@ class Header { /// However, chopper will not automatically convert the body response to your type. /// A [Converter] needs to be specified for conversion. @immutable -class Method { +sealed class Method { /// HTTP method for the request final String method; @@ -207,7 +207,7 @@ class Method { /// Defines a method as an HTTP GET request. @immutable -class Get extends Method { +final class Get extends Method { const Get({ super.optionalBody = true, super.path, @@ -221,7 +221,7 @@ class Get extends Method { /// /// Use the [Body] annotation to pass data to send. @immutable -class Post extends Method { +final class Post extends Method { const Post({ super.optionalBody, super.path, @@ -233,7 +233,7 @@ class Post extends Method { /// Defines a method as an HTTP DELETE request. @immutable -class Delete extends Method { +final class Delete extends Method { const Delete({ super.optionalBody = true, super.path, @@ -247,7 +247,7 @@ class Delete extends Method { /// /// Use the [Body] annotation to pass data to send. @immutable -class Put extends Method { +final class Put extends Method { const Put({ super.optionalBody, super.path, @@ -260,7 +260,7 @@ class Put extends Method { /// Defines a method as an HTTP PATCH request. /// Use the [Body] annotation to pass data to send. @immutable -class Patch extends Method { +final class Patch extends Method { const Patch({ super.optionalBody, super.path, @@ -272,7 +272,7 @@ class Patch extends Method { /// Defines a method as an HTTP HEAD request. @immutable -class Head extends Method { +final class Head extends Method { const Head({ super.optionalBody = true, super.path, @@ -283,7 +283,7 @@ class Head extends Method { } @immutable -class Options extends Method { +final class Options extends Method { const Options({ super.optionalBody = true, super.path, @@ -330,7 +330,7 @@ typedef ConvertResponse = FutureOr Function(Response response); /// } /// ``` @immutable -class FactoryConverter { +final class FactoryConverter { final ConvertRequest? request; final ConvertResponse? response; @@ -349,7 +349,7 @@ class FactoryConverter { /// ``` /// Will be converted to `{ 'name': value }`. @immutable -class Field { +final class Field { /// Name can be use to specify the name of the field /// ```dart /// @Post(path: '/') @@ -367,7 +367,7 @@ class Field { /// Future fetch(@FieldMap List> query); /// ``` @immutable -class FieldMap { +final class FieldMap { const FieldMap(); } @@ -382,7 +382,7 @@ class FieldMap { /// Use [Part] annotation to send simple data. /// Use [PartFile] annotation to send `File` or `List`. @immutable -class Multipart { +final class Multipart { const Multipart(); } @@ -392,7 +392,7 @@ class Multipart { /// /// Also accepts `MultipartFile` (from package:http). @immutable -class Part { +final class Part { final String? name; const Part([this.name]); @@ -406,7 +406,7 @@ class Part { /// Future fetch(@PartMap() List query); /// ``` @immutable -class PartMap { +final class PartMap { const PartMap(); } @@ -423,7 +423,7 @@ class PartMap { /// - [String] (path of your file) /// - `MultipartFile` (from package:http) @immutable -class PartFile { +final class PartFile { final String? name; const PartFile([this.name]); @@ -437,7 +437,7 @@ class PartFile { /// Future fetch(@PartFileMap() List query); /// ``` @immutable -class PartFileMap { +final class PartFileMap { const PartFileMap(); } diff --git a/chopper/lib/src/base.dart b/chopper/lib/src/base.dart index 4fa556c2..27ab57fc 100644 --- a/chopper/lib/src/base.dart +++ b/chopper/lib/src/base.dart @@ -13,7 +13,7 @@ import 'package:meta/meta.dart'; Type _typeOf() => T; @visibleForTesting -final List allowedInterceptorsType = [ +const List allowedInterceptorsType = [ RequestInterceptor, RequestInterceptorFunc, ResponseInterceptor, @@ -26,7 +26,7 @@ final List allowedInterceptorsType = [ /// /// It manages registered services, encodes and decodes data, and intercepts /// requests and responses. -class ChopperClient { +base class ChopperClient { /// Base URL of each request of the registered services. /// E.g., the hostname of your service. final Uri baseUrl; diff --git a/chopper/lib/src/constants.dart b/chopper/lib/src/constants.dart index 52db96c8..e1c9d7f1 100644 --- a/chopper/lib/src/constants.dart +++ b/chopper/lib/src/constants.dart @@ -7,7 +7,7 @@ const String formEncodedHeaders = 'application/x-www-form-urlencoded'; // Represent the header for a json api response https://jsonapi.org/#mime-types const String jsonApiHeaders = 'application/vnd.api+json'; -abstract class HttpMethod { +abstract final class HttpMethod { static const String Get = 'GET'; static const String Post = 'POST'; static const String Put = 'PUT'; diff --git a/chopper/lib/src/interceptor.dart b/chopper/lib/src/interceptor.dart index b8595cea..185bfc97 100644 --- a/chopper/lib/src/interceptor.dart +++ b/chopper/lib/src/interceptor.dart @@ -31,7 +31,7 @@ import 'package:meta/meta.dart'; /// } /// ``` @immutable -abstract class ResponseInterceptor { +abstract interface class ResponseInterceptor { FutureOr onResponse(Response response); } @@ -58,7 +58,7 @@ abstract class ResponseInterceptor { /// /// (See [applyHeader(request, name, value)] and [applyHeaders(request, headers)].) @immutable -abstract class RequestInterceptor { +abstract interface class RequestInterceptor { FutureOr onRequest(Request request); } @@ -72,7 +72,7 @@ abstract class RequestInterceptor { /// /// See [JsonConverter] and [FormUrlEncodedConverter] for example implementations. @immutable -abstract class Converter { +abstract interface class Converter { /// Converts the received [Request] to a [Request] which has a body with the /// HTTP representation of the original body. FutureOr convertRequest(Request request); @@ -94,7 +94,7 @@ abstract class Converter { /// /// An `ErrorConverter` is called only on error responses /// (statusCode < 200 || statusCode >= 300) and before any [ResponseInterceptor]s. -abstract class ErrorConverter { +abstract interface class ErrorConverter { /// Converts the received [Response] to a [Response] which has a body with the /// HTTP representation of the original body. FutureOr convertError(Response response); diff --git a/chopper/lib/src/request.dart b/chopper/lib/src/request.dart index 736126ee..1441ef3d 100644 --- a/chopper/lib/src/request.dart +++ b/chopper/lib/src/request.dart @@ -7,7 +7,7 @@ import 'package:http/http.dart' as http; import 'package:meta/meta.dart'; /// This class represents an HTTP request that can be made with Chopper. -class Request extends http.BaseRequest with EquatableMixin { +base class Request extends http.BaseRequest with EquatableMixin { final Uri uri; final Uri baseUri; final dynamic body; @@ -232,7 +232,7 @@ class Request extends http.BaseRequest with EquatableMixin { /// Represents a part in a multipart request. @immutable -class PartValue with EquatableMixin { +final class PartValue with EquatableMixin { final T value; final String name; @@ -258,6 +258,6 @@ class PartValue with EquatableMixin { /// Represents a file [PartValue] in a multipart request. @immutable -class PartValueFile extends PartValue { +final class PartValueFile extends PartValue { const PartValueFile(super.name, super.value); } diff --git a/chopper/lib/src/response.dart b/chopper/lib/src/response.dart index 8f44e1c5..3ec5c499 100644 --- a/chopper/lib/src/response.dart +++ b/chopper/lib/src/response.dart @@ -16,7 +16,7 @@ import 'package:meta/meta.dart'; /// Future> fetchItem(); /// ``` @immutable -class Response with EquatableMixin { +base class Response with EquatableMixin { /// The [http.BaseResponse] from `package:http` that this [Response] wraps. final http.BaseResponse base; diff --git a/chopper/lib/src/utils.dart b/chopper/lib/src/utils.dart index c8853170..ce78291c 100644 --- a/chopper/lib/src/utils.dart +++ b/chopper/lib/src/utils.dart @@ -131,7 +131,7 @@ Iterable<_Pair> _iterableToQuery( String _normalizeValue(value) => Uri.encodeComponent(value?.toString() ?? ''); -class _Pair with EquatableMixin { +final class _Pair with EquatableMixin { final A first; final B second; final bool useBrackets; diff --git a/chopper/pubspec.yaml b/chopper/pubspec.yaml index ba990c81..5a6d5859 100644 --- a/chopper/pubspec.yaml +++ b/chopper/pubspec.yaml @@ -1,29 +1,29 @@ name: chopper description: Chopper is an http client generator using source_gen, inspired by Retrofit -version: 6.1.4 +version: 7.0.0 documentation: https://hadrien-lejard.gitbook.io/chopper repository: https://github.com/lejard-h/chopper environment: - sdk: ">=2.17.0 <4.0.0" + sdk: ">=3.0.0 <4.0.0" dependencies: equatable: ^2.0.5 - http: ">=0.13.0 <2.0.0" - logging: ^1.0.0 - meta: ^1.3.0 + http: ^1.1.0 + logging: ^1.2.0 + meta: ^1.9.1 dev_dependencies: - build_runner: ^2.0.0 - build_test: ^2.0.0 + build_runner: ^2.4.6 + build_test: ^2.2.0 build_verify: ^3.1.0 - collection: ^1.16.0 - coverage: ^1.0.2 + collection: ^1.18.0 + coverage: ^1.6.3 data_fixture_dart: ^2.2.0 faker: ^2.1.0 - http_parser: ^4.0.0 - lints: ^2.0.0 - test: ^1.16.4 - transparent_image: ^2.0.0 + http_parser: ^4.0.2 + lints: ^2.1.1 + test: ^1.24.4 + transparent_image: ^2.0.1 chopper_generator: path: ../chopper_generator diff --git a/chopper/test/fixtures/http_response_fixture.dart b/chopper/test/fixtures/http_response_fixture.dart index 6b6b6071..96292abd 100644 --- a/chopper/test/fixtures/http_response_fixture.dart +++ b/chopper/test/fixtures/http_response_fixture.dart @@ -12,7 +12,7 @@ extension ResponseFixture on http.Response { } @internal -class ResponseFactory extends FixtureFactory { +final class ResponseFactory extends FixtureFactory { @override FixtureDefinition definition() => define( (Faker faker) => http.Response( diff --git a/chopper/test/fixtures/payload_fixture.dart b/chopper/test/fixtures/payload_fixture.dart index 435883cf..c7a5fb24 100644 --- a/chopper/test/fixtures/payload_fixture.dart +++ b/chopper/test/fixtures/payload_fixture.dart @@ -8,7 +8,7 @@ extension PayloadFixture on Payload { } @internal -class PayloadFactory extends FixtureFactory { +final class PayloadFactory extends FixtureFactory { @override FixtureDefinition definition() => define( (Faker faker) => Payload( diff --git a/chopper/test/fixtures/request_fixture.dart b/chopper/test/fixtures/request_fixture.dart index 6d422cf4..073488ef 100644 --- a/chopper/test/fixtures/request_fixture.dart +++ b/chopper/test/fixtures/request_fixture.dart @@ -9,7 +9,7 @@ extension RequestFixture on Request { } @internal -class RequestFixtureFactory extends FixtureFactory { +final class RequestFixtureFactory extends FixtureFactory { @override FixtureDefinition definition() { final String method = diff --git a/chopper/test/fixtures/response_fixture.dart b/chopper/test/fixtures/response_fixture.dart index cd604e4f..f894c77e 100644 --- a/chopper/test/fixtures/response_fixture.dart +++ b/chopper/test/fixtures/response_fixture.dart @@ -10,7 +10,7 @@ extension ResponseFixture on Response { } @internal -class ResponseFixtureFactory extends FixtureFactory> { +final class ResponseFixtureFactory extends FixtureFactory> { @override FixtureDefinition> definition() { final http.Response base = diff --git a/chopper/test/helpers/payload.dart b/chopper/test/helpers/payload.dart index 4c4ea8a3..6403f7e3 100644 --- a/chopper/test/helpers/payload.dart +++ b/chopper/test/helpers/payload.dart @@ -1,6 +1,6 @@ import 'package:equatable/equatable.dart'; -class Payload with EquatableMixin { +final class Payload with EquatableMixin { const Payload({ this.statusCode = 200, this.message = 'OK', diff --git a/chopper_built_value/pubspec.yaml b/chopper_built_value/pubspec.yaml index 65c3ab03..a59baadb 100644 --- a/chopper_built_value/pubspec.yaml +++ b/chopper_built_value/pubspec.yaml @@ -1,24 +1,24 @@ name: chopper_built_value description: A built_value based Converter for Chopper. -version: 1.2.2 +version: 2.0.0 documentation: https://hadrien-lejard.gitbook.io/chopper/converters/built-value-converter repository: https://github.com/lejard-h/chopper environment: - sdk: ">=2.17.0 <4.0.0" + sdk: ">=3.0.0 <4.0.0" dependencies: - built_value: ^8.0.0 - built_collection: ^5.0.0 - chopper: ^6.0.0 - http: ">=0.13.0 <2.0.0" + built_value: ^8.6.1 + built_collection: ^5.1.1 + chopper: ^7.0.0 + http: ^1.1.0 dev_dependencies: - test: ^1.16.4 - build_runner: ^2.0.0 - build_test: ^2.0.0 - built_value_generator: ^8.0.6 - lints: ^2.0.0 + test: ^1.24.4 + build_runner: ^2.4.6 + build_test: ^2.2.0 + built_value_generator: ^8.6.1 + lints: ^2.1.1 dependency_overrides: # Comment before publish diff --git a/chopper_generator/lib/src/generator.dart b/chopper_generator/lib/src/generator.dart index 39353006..f91820f7 100644 --- a/chopper_generator/lib/src/generator.dart +++ b/chopper_generator/lib/src/generator.dart @@ -15,7 +15,8 @@ import 'package:logging/logging.dart'; import 'package:source_gen/source_gen.dart'; /// Code generator for [chopper.ChopperApi] annotated classes. -class ChopperGenerator extends GeneratorForAnnotation { +final class ChopperGenerator + extends GeneratorForAnnotation { const ChopperGenerator(); static final Logger _logger = Logger('Chopper Generator'); @@ -376,13 +377,11 @@ class ChopperGenerator extends GeneratorForAnnotation { }); } - /// TODO: Upgrade to `Element.enclosingElement` when analyzer 6.0.0 is released; in the mean time ignore the deprecation warning - /// https://github.com/dart-lang/sdk/blob/main/pkg/analyzer/CHANGELOG.md#520 static String _factoryForFunction(FunctionTypedElement function) => // ignore: deprecated_member_use - function.enclosingElement3 is ClassElement + function.enclosingElement is ClassElement // ignore: deprecated_member_use - ? '${function.enclosingElement3!.name}.${function.name}' + ? '${function.enclosingElement!.name}.${function.name}' : function.name!; static Map _getAnnotation( diff --git a/chopper_generator/lib/src/utils.dart b/chopper_generator/lib/src/utils.dart index f1be6482..315238ed 100644 --- a/chopper_generator/lib/src/utils.dart +++ b/chopper_generator/lib/src/utils.dart @@ -3,7 +3,7 @@ import 'package:chopper_generator/src/extensions.dart'; import 'package:code_builder/code_builder.dart'; import 'package:source_gen/source_gen.dart'; -class Utils { +final class Utils { static bool getMethodOptionalBody(ConstantReader method) => method.read('optionalBody').boolValue; diff --git a/chopper_generator/pubspec.yaml b/chopper_generator/pubspec.yaml index 645318cd..e342ecc7 100644 --- a/chopper_generator/pubspec.yaml +++ b/chopper_generator/pubspec.yaml @@ -1,29 +1,29 @@ name: chopper_generator description: Chopper is an http client generator using source_gen, inspired by Retrofit -version: 6.0.3 +version: 7.0.0 documentation: https://hadrien-lejard.gitbook.io/chopper repository: https://github.com/lejard-h/chopper environment: - sdk: ">=2.17.0 <4.0.0" + sdk: ">=3.0.0 <4.0.0" dependencies: - analyzer: '>=4.4.0 <6.0.0' - build: ^2.0.0 - built_collection: ^5.0.0 - chopper: ^6.0.0 - code_builder: ^4.3.0 - dart_style: ^2.0.0 - logging: ^1.0.0 - meta: ^1.3.0 - source_gen: ^1.0.0 + analyzer: ^5.13.0 + build: ^2.4.1 + built_collection: ^5.1.1 + chopper: ^7.0.0 + code_builder: ^4.5.0 + dart_style: ^2.3.2 + logging: ^1.2.0 + meta: ^1.9.1 + source_gen: ^1.4.0 dev_dependencies: - build_runner: ^2.0.0 + build_runner: ^2.4.6 build_verify: ^3.1.0 - http: ">=0.13.0 <2.0.0" - lints: ^2.0.0 - test: ^1.16.4 + http: ^1.1.0 + lints: ^2.1.1 + test: ^1.24.4 dependency_overrides: # Comment before publish diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 18b93a17..735d046c 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -5,7 +5,7 @@ documentation: https://hadrien-lejard.gitbook.io/chopper/ #author: Hadrien Lejard environment: - sdk: '>=2.17.0 <4.0.0' + sdk: '>=3.0.0 <4.0.0' dependencies: chopper: From 93b4e8ae5bd57bac3c58361d30f0679e1d558da5 Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Sun, 30 Jul 2023 09:37:28 +0100 Subject: [PATCH 076/171] :twisted_rightwards_arrows: post release branch sync (#455) --- .github/workflows/publish.yml | 2 ++ .github/workflows/publish_dry_run.yml | 2 ++ chopper/CHANGELOG.md | 5 +++++ chopper_built_value/CHANGELOG.md | 4 ++++ chopper_generator/CHANGELOG.md | 6 ++++++ 5 files changed, 19 insertions(+) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 664fde42..41280a53 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -43,6 +43,8 @@ jobs: strategy: matrix: package: [ chopper, chopper_generator, chopper_built_value ] + fail-fast: true + max-parallel: 1 steps: - uses: dart-lang/setup-dart@v1 with: diff --git a/.github/workflows/publish_dry_run.yml b/.github/workflows/publish_dry_run.yml index b16ef582..14d51179 100644 --- a/.github/workflows/publish_dry_run.yml +++ b/.github/workflows/publish_dry_run.yml @@ -42,6 +42,8 @@ jobs: strategy: matrix: package: [ chopper, chopper_generator, chopper_built_value ] + fail-fast: true + max-parallel: 1 steps: - uses: dart-lang/setup-dart@v1 with: diff --git a/chopper/CHANGELOG.md b/chopper/CHANGELOG.md index d8a56152..b2846189 100644 --- a/chopper/CHANGELOG.md +++ b/chopper/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## 7.0.0 + +- Require Dart 3.0 or later +- Add base, final, and interface modifiers to some classes ([#453](https://github.com/lejard-h/chopper/pull/453)) + ## 6.1.4 - Fix Multipart for List and List ([#439](https://github.com/lejard-h/chopper/pull/439)) diff --git a/chopper_built_value/CHANGELOG.md b/chopper_built_value/CHANGELOG.md index 5fc1f1f7..7e536f4d 100644 --- a/chopper_built_value/CHANGELOG.md +++ b/chopper_built_value/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 2.0.0 + +- Require Dart 3.0 or later + ## 1.2.2 - Update http constraint to ">=0.13.0 <2.0.0" ([#431](https://github.com/lejard-h/chopper/pull/431)) diff --git a/chopper_generator/CHANGELOG.md b/chopper_generator/CHANGELOG.md index 36c3e7ca..d7b19300 100644 --- a/chopper_generator/CHANGELOG.md +++ b/chopper_generator/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 7.0.0 + +- Require Dart 3.0 or later +- Add final modifier to some classes ([#453](https://github.com/lejard-h/chopper/pull/453)) +- Replace deprecated Element.enclosingElement3 with Element.enclosingElement + ## 6.0.3 - Simplify library export From 7cd824cd3c37ee70cca1dfdbf1d0b1fe16c855c7 Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Sun, 30 Jul 2023 11:55:41 +0100 Subject: [PATCH 077/171] :passport_control: fix CODEOWNERS (#456) Fixes: * Unknown owner on line 5: make sure @Guldem exists and has write access to the repository * Unknown owner on line 5: make sure @pixeltoast exists and has write access to the repository --- .github/CODEOWNERS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 1b115426..3912df55 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -2,4 +2,4 @@ # the repo. Unless a later match takes precedence, # @global-owner1 and @global-owner2 will be requested for # review when someone opens a pull request. -* @Guldem @JEuler @lejard-h @meysam1717 @pixeltoast @stewemetal @techouse \ No newline at end of file +* @JEuler @lejard-h @meysam1717 @stewemetal @techouse \ No newline at end of file From aa632837a84dfb27afb873b55332b81c488733ad Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Wed, 2 Aug 2023 08:13:26 +0200 Subject: [PATCH 078/171] :recycle: refactor chopper (#460) * refactor getService * refactor CurlInterceptor * refactor utils._Instance --- chopper/lib/src/base.dart | 9 +++------ chopper/lib/src/interceptor.dart | 31 +++++++++++++------------------ chopper/lib/src/utils.dart | 4 +--- chopper/test/base_test.dart | 14 +++++++++++++- 4 files changed, 30 insertions(+), 28 deletions(-) diff --git a/chopper/lib/src/base.dart b/chopper/lib/src/base.dart index 27ab57fc..b958d140 100644 --- a/chopper/lib/src/base.dart +++ b/chopper/lib/src/base.dart @@ -10,8 +10,6 @@ import 'package:chopper/src/utils.dart'; import 'package:http/http.dart' as http; import 'package:meta/meta.dart'; -Type _typeOf() => T; - @visibleForTesting const List allowedInterceptorsType = [ RequestInterceptor, @@ -168,15 +166,14 @@ base class ChopperClient { /// final todoService = chopper.getService(); /// ``` ServiceType getService() { - final Type serviceType = _typeOf(); - if (serviceType == dynamic || serviceType == ChopperService) { + if (ServiceType == dynamic || ServiceType == ChopperService) { throw Exception( 'Service type should be provided, `dynamic` is not allowed.', ); } - final ChopperService? service = _services[serviceType]; + final ChopperService? service = _services[ServiceType]; if (service == null) { - throw Exception('Service of type \'$serviceType\' not found.'); + throw Exception("Service of type '$ServiceType' not found."); } return service as ServiceType; diff --git a/chopper/lib/src/interceptor.dart b/chopper/lib/src/interceptor.dart index 185bfc97..1431fc23 100644 --- a/chopper/lib/src/interceptor.dart +++ b/chopper/lib/src/interceptor.dart @@ -137,32 +137,27 @@ class CurlInterceptor implements RequestInterceptor { @override Future onRequest(Request request) async { final http.BaseRequest baseRequest = await request.toBaseRequest(); - final String method = baseRequest.method; - final String url = baseRequest.url.toString(); - final Map headers = baseRequest.headers; - String curl = 'curl -v -X $method'; - headers.forEach((k, v) { - curl += ' -H \'$k: $v\''; - }); + final List curlParts = ['curl -v -X ${baseRequest.method}']; + for (final MapEntry header in baseRequest.headers.entries) { + curlParts.add("-H '${header.key}: ${header.value}'"); + } // this is fairly naive, but it should cover most cases if (baseRequest is http.Request) { - final body = baseRequest.body; + final String body = baseRequest.body; if (body.isNotEmpty) { - curl += ' -d \'$body\''; + curlParts.add("-d '$body'"); } } if (baseRequest is http.MultipartRequest) { - final fields = baseRequest.fields; - final files = baseRequest.files; - fields.forEach((k, v) { - curl += ' -f \'$k: $v\''; - }); - for (var file in files) { - curl += ' -f \'${file.field}: ${file.filename ?? ''}\''; + for (final MapEntry field in baseRequest.fields.entries) { + curlParts.add("-f '${field.key}: ${field.value}'"); + } + for (final http.MultipartFile file in baseRequest.files) { + curlParts.add("-f '${file.field}: ${file.filename ?? ''}'"); } } - curl += ' "$url"'; - chopperLogger.info(curl); + curlParts.add('"${baseRequest.url}"'); + chopperLogger.info(curlParts.join(' ')); return request; } diff --git a/chopper/lib/src/utils.dart b/chopper/lib/src/utils.dart index ce78291c..ea4995ff 100644 --- a/chopper/lib/src/utils.dart +++ b/chopper/lib/src/utils.dart @@ -156,6 +156,4 @@ final class _Pair with EquatableMixin { bool isTypeOf() => _Instance() is _Instance; -class _Instance { - // -} +final class _Instance {} diff --git a/chopper/test/base_test.dart b/chopper/test/base_test.dart index d6486c2e..48cbb771 100644 --- a/chopper/test/base_test.dart +++ b/chopper/test/base_test.dart @@ -28,6 +28,18 @@ void main() { ); group('Base', () { + test('getService', () async { + final httpClient = MockClient( + (_) async => http.Response('get response', 200), + ); + + final chopper = buildClient(httpClient); + final service = chopper.getService(); + + expect(service, isNotNull); + expect(service, isA()); + }); + test('get service errors', () async { final chopper = ChopperClient( baseUrl: baseUrl, @@ -38,7 +50,7 @@ void main() { } on Exception catch (e) { expect( e.toString(), - equals('Exception: Service of type \'HttpTestService\' not found.'), + equals("Exception: Service of type 'HttpTestService' not found."), ); } From a386e86a38dd17af5c9ffc4900cf67ae9748a4bd Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Wed, 2 Aug 2023 18:38:56 +0200 Subject: [PATCH 079/171] :recycle: refactor ChopperClient constructor (#461) --- chopper/lib/src/base.dart | 62 ++++++++++++++++++++----------------- chopper/test/base_test.dart | 40 +++++++++++++++++++++--- 2 files changed, 68 insertions(+), 34 deletions(-) diff --git a/chopper/lib/src/base.dart b/chopper/lib/src/base.dart index b958d140..e791c13b 100644 --- a/chopper/lib/src/base.dart +++ b/chopper/lib/src/base.dart @@ -45,11 +45,13 @@ base class ChopperClient { /// (statusCode < 200 || statusCode >= 300\). final ErrorConverter? errorConverter; - final Map _services = {}; - final _requestInterceptors = []; - final _responseInterceptors = []; - final _requestController = StreamController.broadcast(); - final _responseController = StreamController.broadcast(); + late final Map _services; + late final List _requestInterceptors; + late final List _responseInterceptors; + final StreamController _requestController = + StreamController.broadcast(); + final StreamController _responseController = + StreamController.broadcast(); final bool _clientIsInternal; @@ -112,44 +114,46 @@ base class ChopperClient { ChopperClient({ Uri? baseUrl, http.Client? client, - Iterable interceptors = const [], + Iterable? interceptors, this.authenticator, this.converter, this.errorConverter, - Iterable services = const [], + Iterable? services, }) : assert( - baseUrl == null || !baseUrl.hasQuery, - 'baseUrl should not contain query parameters.' - 'Use a request interceptor to add default query parameters'), + baseUrl == null || !baseUrl.hasQuery, + 'baseUrl should not contain query parameters. ' + 'Use a request interceptor to add default query parameters', + ), baseUrl = baseUrl ?? Uri(), httpClient = client ?? http.Client(), - _clientIsInternal = client == null { - if (!interceptors.every(_isAnInterceptor)) { - throw ArgumentError( - 'Unsupported type for interceptors, it only support the following types:\n' - '${allowedInterceptorsType.join('\n - ')}', - ); - } - - _requestInterceptors.addAll(interceptors.where(_isRequestInterceptor)); - _responseInterceptors.addAll(interceptors.where(_isResponseInterceptor)); - - services.toSet().forEach((s) { - s.client = this; - _services[s.definitionType] = s; - }); + _clientIsInternal = client == null, + assert( + interceptors?.every(_isAnInterceptor) ?? true, + 'Unsupported type for interceptors, it only support the following types:\n' + ' - ${allowedInterceptorsType.join('\n - ')}', + ), + _requestInterceptors = [ + ...?interceptors?.where(_isRequestInterceptor), + ], + _responseInterceptors = [ + ...?interceptors?.where(_isResponseInterceptor), + ] { + _services = { + for (final ChopperService service in services?.toSet() ?? []) + service.definitionType: service..client = this + }; } - bool _isRequestInterceptor(value) => + static bool _isRequestInterceptor(value) => value is RequestInterceptor || value is RequestInterceptorFunc; - bool _isResponseInterceptor(value) => + static bool _isResponseInterceptor(value) => value is ResponseInterceptor || value is ResponseInterceptorFunc1 || value is ResponseInterceptorFunc2 || value is DynamicResponseInterceptorFunc; - bool _isAnInterceptor(value) => + static bool _isAnInterceptor(value) => _isResponseInterceptor(value) || _isRequestInterceptor(value); /// Retrieve any service included in the [ChopperClient] @@ -182,7 +186,7 @@ base class ChopperClient { Future _encodeRequest(Request request) async => converter?.convertRequest(request) ?? request; - Future> _decodeResponse( + static Future> _decodeResponse( Response response, Converter withConverter, ) async => diff --git a/chopper/test/base_test.dart b/chopper/test/base_test.dart index 48cbb771..3e6a6f01 100644 --- a/chopper/test/base_test.dart +++ b/chopper/test/base_test.dart @@ -647,20 +647,50 @@ void main() { }); test('wrong type for interceptor', () { + expect( + () => ChopperClient(interceptors: [(bool foo) => 'bar']), + throwsA(isA()), + ); + try { ChopperClient( interceptors: [ (bool foo) => 'bar', ], ); - } on ArgumentError catch (e) { + } on AssertionError catch (error) { expect( - e.toString(), - 'Invalid argument(s): Unsupported type for interceptors, it only support the following types:\n' - '${allowedInterceptorsType.join('\n - ')}', + error.toString(), + contains( + 'Unsupported type for interceptors, it only support the following types:\n' + ' - ${allowedInterceptorsType.join('\n - ')}', + ), ); } - }); + }, testOn: 'vm'); + + test('wrong type for interceptor', () { + expect( + () => ChopperClient(interceptors: [(bool foo) => 'bar']), + throwsA(isA()), + ); + + try { + ChopperClient( + interceptors: [ + (bool foo) => 'bar', + ], + ); + } on AssertionError catch (error) { + expect( + error.toString(), + contains( + 'Unsupported type for interceptors, it only support the following types:\\n' + ' - ${allowedInterceptorsType.join('\\n - ')}', + ), + ); + } + }, testOn: 'browser'); test('Query Map 1', () async { final httpClient = MockClient((request) async { From 7a6b5b9bebfeb45b647c93121e621d58da94cb49 Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Wed, 2 Aug 2023 19:17:22 +0200 Subject: [PATCH 080/171] :arrow_up: update mono_repo (#462) --- .github/workflows/dart.yml | 40 ++++++------------------------- chopper_built_value/mono_pkg.yaml | 2 +- chopper_generator/mono_pkg.yaml | 2 +- mono_repo.yaml | 2 +- tool/ci.sh | 2 +- 5 files changed, 11 insertions(+), 37 deletions(-) diff --git a/.github/workflows/dart.yml b/.github/workflows/dart.yml index ee10d7f9..6a6322d9 100644 --- a/.github/workflows/dart.yml +++ b/.github/workflows/dart.yml @@ -1,4 +1,4 @@ -# Created with package:mono_repo v6.5.5 +# Created with package:mono_repo v6.5.7 name: Dart CI on: push: @@ -37,20 +37,20 @@ jobs: name: Checkout repository uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab - name: mono_repo self validate - run: dart pub global activate mono_repo 6.5.5 + run: dart pub global activate mono_repo 6.5.7 - name: mono_repo self validate run: dart pub global run mono_repo generate --validate job_002: - name: "analyze_and_format; PKG: chopper; `dart format --output=none --set-exit-if-changed .`, `dart analyze --fatal-infos .`" + name: "analyze_and_format; PKGS: chopper, chopper_built_value, chopper_generator; `dart format --output=none --set-exit-if-changed .`, `dart analyze --fatal-infos .`" runs-on: ubuntu-latest steps: - name: Cache Pub hosted dependencies uses: actions/cache@88522ab9f39a2ea568f7027eddc7d8d8bc9d59c8 with: path: "~/.pub-cache/hosted" - key: "os:ubuntu-latest;pub-cache-hosted;sdk:stable;packages:chopper;commands:format-analyze" + key: "os:ubuntu-latest;pub-cache-hosted;sdk:stable;packages:chopper-chopper_built_value-chopper_generator;commands:format-analyze" restore-keys: | - os:ubuntu-latest;pub-cache-hosted;sdk:stable;packages:chopper + os:ubuntu-latest;pub-cache-hosted;sdk:stable;packages:chopper-chopper_built_value-chopper_generator os:ubuntu-latest;pub-cache-hosted;sdk:stable os:ubuntu-latest;pub-cache-hosted os:ubuntu-latest @@ -74,29 +74,6 @@ jobs: run: dart analyze --fatal-infos . if: "always() && steps.chopper_pub_upgrade.conclusion == 'success'" working-directory: chopper - needs: - - job_001 - job_003: - name: "analyzer_and_format; PKGS: chopper_built_value, chopper_generator; `dart format --output=none --set-exit-if-changed .`, `dart analyze --fatal-infos .`" - runs-on: ubuntu-latest - steps: - - name: Cache Pub hosted dependencies - uses: actions/cache@88522ab9f39a2ea568f7027eddc7d8d8bc9d59c8 - with: - path: "~/.pub-cache/hosted" - key: "os:ubuntu-latest;pub-cache-hosted;sdk:stable;packages:chopper_built_value-chopper_generator;commands:format-analyze" - restore-keys: | - os:ubuntu-latest;pub-cache-hosted;sdk:stable;packages:chopper_built_value-chopper_generator - os:ubuntu-latest;pub-cache-hosted;sdk:stable - os:ubuntu-latest;pub-cache-hosted - os:ubuntu-latest - - name: Setup Dart SDK - uses: dart-lang/setup-dart@d6a63dab3335f427404425de0fbfed4686d93c4f - with: - sdk: stable - - id: checkout - name: Checkout repository - uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab - id: chopper_built_value_pub_upgrade name: chopper_built_value; dart pub upgrade run: dart pub upgrade @@ -125,8 +102,7 @@ jobs: working-directory: chopper_generator needs: - job_001 - - job_002 - job_004: + job_003: name: "unit_test; PKGS: chopper, chopper_built_value, chopper_generator; `dart pub global run coverage:test_with_coverage`" runs-on: ubuntu-latest steps: @@ -197,8 +173,7 @@ jobs: needs: - job_001 - job_002 - - job_003 - job_005: + job_004: name: "unit_test; PKGS: chopper, chopper_built_value; `dart test -p chrome`" runs-on: ubuntu-latest steps: @@ -240,4 +215,3 @@ jobs: needs: - job_001 - job_002 - - job_003 diff --git a/chopper_built_value/mono_pkg.yaml b/chopper_built_value/mono_pkg.yaml index ae7b5f25..8cce50b9 100644 --- a/chopper_built_value/mono_pkg.yaml +++ b/chopper_built_value/mono_pkg.yaml @@ -2,7 +2,7 @@ sdk: - stable stages: -- analyzer_and_format: +- analyze_and_format: - group: - format - analyze: --fatal-infos . diff --git a/chopper_generator/mono_pkg.yaml b/chopper_generator/mono_pkg.yaml index c0087871..df365840 100644 --- a/chopper_generator/mono_pkg.yaml +++ b/chopper_generator/mono_pkg.yaml @@ -2,7 +2,7 @@ sdk: - stable stages: -- analyzer_and_format: +- analyze_and_format: - group: - format - analyze: --fatal-infos . diff --git a/mono_repo.yaml b/mono_repo.yaml index 28e29828..1cdb37ac 100644 --- a/mono_repo.yaml +++ b/mono_repo.yaml @@ -12,7 +12,7 @@ github: - develop merge_stages: - - analyzer_and_format + - analyze_and_format - unit_test coverage_service: diff --git a/tool/ci.sh b/tool/ci.sh index 7449806d..9c1ac4c1 100755 --- a/tool/ci.sh +++ b/tool/ci.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Created with package:mono_repo v6.5.5 +# Created with package:mono_repo v6.5.7 # Support built in commands on windows out of the box. # When it is a flutter repo (check the pubspec.yaml for "sdk: flutter") From e9fc26da51922ff89ddfb023150265ca59ca324a Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Thu, 3 Aug 2023 07:50:53 +0200 Subject: [PATCH 081/171] :sparkles: add final class modifier to generated Chopper API implementation (#463) --- chopper/example/definition.chopper.dart | 2 +- chopper/test/test_service.chopper.dart | 2 +- chopper_generator/lib/src/generator.dart | 1 + chopper_generator/test/test_service.chopper.dart | 2 +- example/lib/built_value_resource.chopper.dart | 2 +- example/lib/json_serializable.chopper.dart | 2 +- 6 files changed, 6 insertions(+), 5 deletions(-) diff --git a/chopper/example/definition.chopper.dart b/chopper/example/definition.chopper.dart index c7c86ef7..a7fdf51a 100644 --- a/chopper/example/definition.chopper.dart +++ b/chopper/example/definition.chopper.dart @@ -7,7 +7,7 @@ part of 'definition.dart'; // ************************************************************************** // ignore_for_file: always_put_control_body_on_new_line, always_specify_types, prefer_const_declarations, unnecessary_brace_in_string_interps -class _$MyService extends MyService { +final class _$MyService extends MyService { _$MyService([ChopperClient? client]) { if (client == null) return; this.client = client; diff --git a/chopper/test/test_service.chopper.dart b/chopper/test/test_service.chopper.dart index 45e1d737..d0379988 100644 --- a/chopper/test/test_service.chopper.dart +++ b/chopper/test/test_service.chopper.dart @@ -7,7 +7,7 @@ part of 'test_service.dart'; // ************************************************************************** // ignore_for_file: always_put_control_body_on_new_line, always_specify_types, prefer_const_declarations, unnecessary_brace_in_string_interps -class _$HttpTestService extends HttpTestService { +final class _$HttpTestService extends HttpTestService { _$HttpTestService([ChopperClient? client]) { if (client == null) return; this.client = client; diff --git a/chopper_generator/lib/src/generator.dart b/chopper_generator/lib/src/generator.dart index f91820f7..70ee0790 100644 --- a/chopper_generator/lib/src/generator.dart +++ b/chopper_generator/lib/src/generator.dart @@ -68,6 +68,7 @@ final class ChopperGenerator final Class classBuilder = Class((builder) { builder + ..modifier = ClassModifier.final$ ..name = name ..extend = refer(friendlyName) ..fields.add(_buildDefinitionTypeMethod(friendlyName)) diff --git a/chopper_generator/test/test_service.chopper.dart b/chopper_generator/test/test_service.chopper.dart index 45e1d737..d0379988 100644 --- a/chopper_generator/test/test_service.chopper.dart +++ b/chopper_generator/test/test_service.chopper.dart @@ -7,7 +7,7 @@ part of 'test_service.dart'; // ************************************************************************** // ignore_for_file: always_put_control_body_on_new_line, always_specify_types, prefer_const_declarations, unnecessary_brace_in_string_interps -class _$HttpTestService extends HttpTestService { +final class _$HttpTestService extends HttpTestService { _$HttpTestService([ChopperClient? client]) { if (client == null) return; this.client = client; diff --git a/example/lib/built_value_resource.chopper.dart b/example/lib/built_value_resource.chopper.dart index e30a468b..af0b742d 100644 --- a/example/lib/built_value_resource.chopper.dart +++ b/example/lib/built_value_resource.chopper.dart @@ -7,7 +7,7 @@ part of 'built_value_resource.dart'; // ************************************************************************** // ignore_for_file: always_put_control_body_on_new_line, always_specify_types, prefer_const_declarations, unnecessary_brace_in_string_interps -class _$MyService extends MyService { +final class _$MyService extends MyService { _$MyService([ChopperClient? client]) { if (client == null) return; this.client = client; diff --git a/example/lib/json_serializable.chopper.dart b/example/lib/json_serializable.chopper.dart index 7a60b5ab..1aa9352e 100644 --- a/example/lib/json_serializable.chopper.dart +++ b/example/lib/json_serializable.chopper.dart @@ -7,7 +7,7 @@ part of 'json_serializable.dart'; // ************************************************************************** // ignore_for_file: always_put_control_body_on_new_line, always_specify_types, prefer_const_declarations, unnecessary_brace_in_string_interps -class _$MyService extends MyService { +final class _$MyService extends MyService { _$MyService([ChopperClient? client]) { if (client == null) return; this.client = client; From e07afcfa14efe5debecf83930a2698c13a69b425 Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Thu, 3 Aug 2023 18:09:14 +0200 Subject: [PATCH 082/171] :twisted_rightwards_arrows: post release branch sync (#466) --- chopper/CHANGELOG.md | 6 ++++++ chopper/pubspec.yaml | 2 +- chopper_generator/CHANGELOG.md | 4 ++++ chopper_generator/pubspec.yaml | 2 +- 4 files changed, 12 insertions(+), 2 deletions(-) diff --git a/chopper/CHANGELOG.md b/chopper/CHANGELOG.md index b2846189..b226ff15 100644 --- a/chopper/CHANGELOG.md +++ b/chopper/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 7.0.1 + +- Refactor ChopperClient constructor +- Refactor ChopperClient.getService +- Refactor CurlInterceptor + ## 7.0.0 - Require Dart 3.0 or later diff --git a/chopper/pubspec.yaml b/chopper/pubspec.yaml index 5a6d5859..0e86235c 100644 --- a/chopper/pubspec.yaml +++ b/chopper/pubspec.yaml @@ -1,6 +1,6 @@ name: chopper description: Chopper is an http client generator using source_gen, inspired by Retrofit -version: 7.0.0 +version: 7.0.1 documentation: https://hadrien-lejard.gitbook.io/chopper repository: https://github.com/lejard-h/chopper diff --git a/chopper_generator/CHANGELOG.md b/chopper_generator/CHANGELOG.md index d7b19300..7943acf4 100644 --- a/chopper_generator/CHANGELOG.md +++ b/chopper_generator/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 7.0.1 + +- Add final class modifier to generated Chopper API implementations + ## 7.0.0 - Require Dart 3.0 or later diff --git a/chopper_generator/pubspec.yaml b/chopper_generator/pubspec.yaml index e342ecc7..ac8b3aa7 100644 --- a/chopper_generator/pubspec.yaml +++ b/chopper_generator/pubspec.yaml @@ -1,6 +1,6 @@ name: chopper_generator description: Chopper is an http client generator using source_gen, inspired by Retrofit -version: 7.0.0 +version: 7.0.1 documentation: https://hadrien-lejard.gitbook.io/chopper repository: https://github.com/lejard-h/chopper From 2f7f4147ea75875082da3ffbaf9c2c52677d3c60 Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Fri, 4 Aug 2023 10:43:08 +0200 Subject: [PATCH 083/171] :construction_worker: add Dependabot workflow (#467) --- .github/dependabot.yml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..68152415 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,17 @@ +version: 2 +updates: + - package-ecosystem: "pub" + directory: "/chopper" + schedule: + interval: "weekly" + target-branch: "develop" + - package-ecosystem: "pub" + directory: "/chopper_built_value" + schedule: + interval: "weekly" + target-branch: "develop" + - package-ecosystem: "pub" + directory: "/chopper_generator" + schedule: + interval: "weekly" + target-branch: "develop" \ No newline at end of file From 03f7bf73a835f453031a8c6795f81c761fe776d7 Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Fri, 4 Aug 2023 10:58:51 +0200 Subject: [PATCH 084/171] :pencil2: fix typo in mono_repo.yaml (#468) --- .github/workflows/dart.yml | 2 -- mono_repo.yaml | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/dart.yml b/.github/workflows/dart.yml index 6a6322d9..0a20e47e 100644 --- a/.github/workflows/dart.yml +++ b/.github/workflows/dart.yml @@ -100,8 +100,6 @@ jobs: run: dart analyze --fatal-infos . if: "always() && steps.chopper_generator_pub_upgrade.conclusion == 'success'" working-directory: chopper_generator - needs: - - job_001 job_003: name: "unit_test; PKGS: chopper, chopper_built_value, chopper_generator; `dart pub global run coverage:test_with_coverage`" runs-on: ubuntu-latest diff --git a/mono_repo.yaml b/mono_repo.yaml index 1cdb37ac..5f0226ff 100644 --- a/mono_repo.yaml +++ b/mono_repo.yaml @@ -1,4 +1,4 @@ -self_validate: analyzer_and_format +self_validate: analyze_and_format github: on: From 8dbde76f8e4e3555266408dc260125cafd743f83 Mon Sep 17 00:00:00 2001 From: Max Rumpf Date: Sat, 5 Aug 2023 07:54:48 +0200 Subject: [PATCH 085/171] :sparkles: HttpLoggingInterceptor: allow to pass custom logger (#470) --- chopper/lib/src/http_logging_interceptor.dart | 35 ++++++++++--------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/chopper/lib/src/http_logging_interceptor.dart b/chopper/lib/src/http_logging_interceptor.dart index 0be43965..f8965bee 100644 --- a/chopper/lib/src/http_logging_interceptor.dart +++ b/chopper/lib/src/http_logging_interceptor.dart @@ -5,6 +5,7 @@ import 'package:chopper/src/request.dart'; import 'package:chopper/src/response.dart'; import 'package:chopper/src/utils.dart'; import 'package:http/http.dart' as http; +import 'package:logging/logging.dart'; import 'package:meta/meta.dart'; enum Level { @@ -71,11 +72,13 @@ enum Level { @immutable class HttpLoggingInterceptor implements RequestInterceptor, ResponseInterceptor { - const HttpLoggingInterceptor({this.level = Level.body}) - : _logBody = level == Level.body, + HttpLoggingInterceptor({this.level = Level.body, Logger? logger}) + : _logger = logger ?? chopperLogger, + _logBody = level == Level.body, _logHeaders = level == Level.body || level == Level.headers; final Level level; + final Logger _logger; final bool _logBody; final bool _logHeaders; @@ -97,25 +100,25 @@ class HttpLoggingInterceptor } // Always start on a new line - chopperLogger.info(''); - chopperLogger.info(startRequestMessage); + _logger.info(''); + _logger.info(startRequestMessage); if (_logHeaders) { - base.headers.forEach((k, v) => chopperLogger.info('$k: $v')); + base.headers.forEach((k, v) => _logger.info('$k: $v')); if (base.contentLength != null && base.headers['content-length'] == null) { - chopperLogger.info('content-length: ${base.contentLength}'); + _logger.info('content-length: ${base.contentLength}'); } } if (_logBody && bodyMessage.isNotEmpty) { - chopperLogger.info(''); - chopperLogger.info(bodyMessage); + _logger.info(''); + _logger.info(bodyMessage); } if (_logHeaders || _logBody) { - chopperLogger.info('--> END ${base.method}'); + _logger.info('--> END ${base.method}'); } return request; @@ -145,27 +148,27 @@ class HttpLoggingInterceptor } // Always start on a new line - chopperLogger.info(''); - chopperLogger.info( + _logger.info(''); + _logger.info( '<-- $reasonPhrase ${base.request?.method} ${base.request?.url.toString()}$bytes', ); if (_logHeaders) { - base.headers.forEach((k, v) => chopperLogger.info('$k: $v')); + base.headers.forEach((k, v) => _logger.info('$k: $v')); if (base.contentLength != null && base.headers['content-length'] == null) { - chopperLogger.info('content-length: ${base.contentLength}'); + _logger.info('content-length: ${base.contentLength}'); } } if (_logBody && bodyMessage.isNotEmpty) { - chopperLogger.info(''); - chopperLogger.info(bodyMessage); + _logger.info(''); + _logger.info(bodyMessage); } if (_logBody || _logHeaders) { - chopperLogger.info('<-- END HTTP'); + _logger.info('<-- END HTTP'); } return response; From 2e00b6a4763d0ded18049f1d9e23d1d88809fed8 Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Mon, 7 Aug 2023 07:24:50 +0200 Subject: [PATCH 086/171] :construction_worker: use yq to manipulate pubspec.yaml in publish CI (#471) --- .github/workflows/publish.yml | 11 ++++++++--- .github/workflows/publish_dry_run.yml | 9 +++++++-- chopper/pubspec.yaml | 3 +++ chopper_built_value/pubspec.yaml | 1 - chopper_generator/pubspec.yaml | 1 - tool/publish.sh | 19 ------------------- 6 files changed, 18 insertions(+), 26 deletions(-) delete mode 100644 tool/publish.sh diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 41280a53..a9da77ad 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -74,11 +74,16 @@ jobs: run: | set -e mkdir -p $XDG_CONFIG_HOME/dart - echo '${{ secrets.CREDENTIAL_JSON }}' > "$XDG_CONFIG_HOME/dart/pub-credentials.json" + echo -n '${{ secrets.CREDENTIAL_JSON }}' > $XDG_CONFIG_HOME/dart/pub-credentials.json - name: Publish id: publish if: ${{ env.IS_VERSION_GREATER == 1 }} - run: bash tool/publish.sh ${{ matrix.package }} + run: | + set -e + pushd ${{ matrix.package }} || exit + yq -i 'del(.dependency_overrides)' pubspec.yaml + dart pub publish --force + popd || exit - name: Skip publish id: skip_publish if: ${{ env.IS_VERSION_GREATER == 0 }} @@ -87,4 +92,4 @@ jobs: id: cleanup if: ${{ always() }} run: | - rm -rf "$XDG_CONFIG_HOME/dart/pub-credentials.json" \ No newline at end of file + rm -rf $XDG_CONFIG_HOME/dart/pub-credentials.json diff --git a/.github/workflows/publish_dry_run.yml b/.github/workflows/publish_dry_run.yml index 14d51179..6ab66c84 100644 --- a/.github/workflows/publish_dry_run.yml +++ b/.github/workflows/publish_dry_run.yml @@ -70,8 +70,13 @@ jobs: - name: Publish (dry run) id: publish_dry_run if: ${{ env.IS_VERSION_GREATER == 1 }} - run: bash tool/publish.sh ${{ matrix.package }} --dry-run + run: | + set -e + pushd ${{ matrix.package }} || exit + yq -i 'del(.dependency_overrides)' pubspec.yaml + dart pub publish --dry-run + popd || exit - name: Skip publish (dry run) id: skip_publish_dry_run if: ${{ env.IS_VERSION_GREATER == 0 }} - run: echo "Skipping publish (dry run) for ${{ matrix.package }} because the version is not greater than the one on pub.dev" \ No newline at end of file + run: echo "Skipping publish (dry run) for ${{ matrix.package }} because the version is not greater than the one on pub.dev" diff --git a/chopper/pubspec.yaml b/chopper/pubspec.yaml index 0e86235c..5f57ccb0 100644 --- a/chopper/pubspec.yaml +++ b/chopper/pubspec.yaml @@ -25,5 +25,8 @@ dev_dependencies: lints: ^2.1.1 test: ^1.24.4 transparent_image: ^2.0.1 + chopper_generator: ^7.0.0 + +dependency_overrides: chopper_generator: path: ../chopper_generator diff --git a/chopper_built_value/pubspec.yaml b/chopper_built_value/pubspec.yaml index a59baadb..b23d0a66 100644 --- a/chopper_built_value/pubspec.yaml +++ b/chopper_built_value/pubspec.yaml @@ -21,6 +21,5 @@ dev_dependencies: lints: ^2.1.1 dependency_overrides: - # Comment before publish chopper: path: ../chopper diff --git a/chopper_generator/pubspec.yaml b/chopper_generator/pubspec.yaml index ac8b3aa7..4d541b87 100644 --- a/chopper_generator/pubspec.yaml +++ b/chopper_generator/pubspec.yaml @@ -26,6 +26,5 @@ dev_dependencies: test: ^1.24.4 dependency_overrides: - # Comment before publish chopper: path: ../chopper diff --git a/tool/publish.sh b/tool/publish.sh deleted file mode 100644 index f5b81db2..00000000 --- a/tool/publish.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/usr/bin/env bash - -set -e - -PKG=$1 -echo -e "\033[1mPKG: ${PKG}\033[22m" -pushd "${PKG}" - -sed '/Comment before publish$/,+2 d' pubspec.yaml > pubspec.temp.yaml -rm pubspec.yaml -mv pubspec.temp.yaml pubspec.yaml - -if [ "$2" == "--dry-run" ]; then - dart pub publish --dry-run -else - dart pub publish --force -fi - -popd \ No newline at end of file From 0af17f871fefefe9361311800eefe5a2e0137b80 Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Mon, 7 Aug 2023 18:05:48 +0200 Subject: [PATCH 087/171] :twisted_rightwards_arrows: post release branch sync (#473) --- chopper/CHANGELOG.md | 4 ++++ chopper/pubspec.yaml | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/chopper/CHANGELOG.md b/chopper/CHANGELOG.md index b226ff15..d45a4dee 100644 --- a/chopper/CHANGELOG.md +++ b/chopper/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 7.0.2 + +- Add option to pass custom Logger to HttpLoggingInterceptor ([#470](https://github.com/lejard-h/chopper/pull/470)) + ## 7.0.1 - Refactor ChopperClient constructor diff --git a/chopper/pubspec.yaml b/chopper/pubspec.yaml index 5f57ccb0..f6be31f8 100644 --- a/chopper/pubspec.yaml +++ b/chopper/pubspec.yaml @@ -1,6 +1,6 @@ name: chopper description: Chopper is an http client generator using source_gen, inspired by Retrofit -version: 7.0.1 +version: 7.0.2 documentation: https://hadrien-lejard.gitbook.io/chopper repository: https://github.com/lejard-h/chopper From 1a01b6096f355da01b93e6117fe5afa38230dee7 Mon Sep 17 00:00:00 2001 From: Max Rumpf Date: Thu, 10 Aug 2023 09:12:30 +0200 Subject: [PATCH 088/171] :sparkles: Attach request/response to interceptor log lines (#475) --- chopper/lib/src/chopper_log_record.dart | 13 ++++++ chopper/lib/src/http_logging_interceptor.dart | 45 ++++++++++++------- 2 files changed, 43 insertions(+), 15 deletions(-) create mode 100644 chopper/lib/src/chopper_log_record.dart diff --git a/chopper/lib/src/chopper_log_record.dart b/chopper/lib/src/chopper_log_record.dart new file mode 100644 index 00000000..181ca492 --- /dev/null +++ b/chopper/lib/src/chopper_log_record.dart @@ -0,0 +1,13 @@ +import 'package:chopper/src/request.dart'; +import 'package:chopper/src/response.dart'; + +class ChopperLogRecord { + const ChopperLogRecord(this.message, {this.request, this.response}); + + final String message; + final Request? request; + final Response? response; + + @override + String toString() => message; +} diff --git a/chopper/lib/src/http_logging_interceptor.dart b/chopper/lib/src/http_logging_interceptor.dart index f8965bee..e2453673 100644 --- a/chopper/lib/src/http_logging_interceptor.dart +++ b/chopper/lib/src/http_logging_interceptor.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:chopper/src/chopper_log_record.dart'; import 'package:chopper/src/interceptor.dart'; import 'package:chopper/src/request.dart'; import 'package:chopper/src/response.dart'; @@ -100,25 +101,33 @@ class HttpLoggingInterceptor } // Always start on a new line - _logger.info(''); - _logger.info(startRequestMessage); + _logger.info(ChopperLogRecord('', request: request)); + _logger.info(ChopperLogRecord(startRequestMessage, request: request)); if (_logHeaders) { - base.headers.forEach((k, v) => _logger.info('$k: $v')); + base.headers.forEach( + (k, v) => _logger.info(ChopperLogRecord('$k: $v', request: request)), + ); if (base.contentLength != null && base.headers['content-length'] == null) { - _logger.info('content-length: ${base.contentLength}'); + _logger.info(ChopperLogRecord( + 'content-length: ${base.contentLength}', + request: request, + )); } } if (_logBody && bodyMessage.isNotEmpty) { - _logger.info(''); - _logger.info(bodyMessage); + _logger.info(ChopperLogRecord('', request: request)); + _logger.info(ChopperLogRecord(bodyMessage, request: request)); } if (_logHeaders || _logBody) { - _logger.info('--> END ${base.method}'); + _logger.info(ChopperLogRecord( + '--> END ${base.method}', + request: request, + )); } return request; @@ -148,27 +157,33 @@ class HttpLoggingInterceptor } // Always start on a new line - _logger.info(''); - _logger.info( + _logger.info(ChopperLogRecord('', response: response)); + _logger.info(ChopperLogRecord( '<-- $reasonPhrase ${base.request?.method} ${base.request?.url.toString()}$bytes', - ); + response: response, + )); if (_logHeaders) { - base.headers.forEach((k, v) => _logger.info('$k: $v')); + base.headers.forEach( + (k, v) => _logger.info(ChopperLogRecord('$k: $v', response: response)), + ); if (base.contentLength != null && base.headers['content-length'] == null) { - _logger.info('content-length: ${base.contentLength}'); + _logger.info(ChopperLogRecord( + 'content-length: ${base.contentLength}', + response: response, + )); } } if (_logBody && bodyMessage.isNotEmpty) { - _logger.info(''); - _logger.info(bodyMessage); + _logger.info(ChopperLogRecord('', response: response)); + _logger.info(ChopperLogRecord(bodyMessage, response: response)); } if (_logBody || _logHeaders) { - _logger.info('<-- END HTTP'); + _logger.info(ChopperLogRecord('<-- END HTTP', response: response)); } return response; From 764d57d28eff6a284d5e07b91141a822c246f6a7 Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Fri, 11 Aug 2023 07:45:04 +0200 Subject: [PATCH 089/171] :twisted_rightwards_arrows: post release branch sync (#477) --- chopper/CHANGELOG.md | 4 ++++ chopper/pubspec.yaml | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/chopper/CHANGELOG.md b/chopper/CHANGELOG.md index d45a4dee..2035bf9d 100644 --- a/chopper/CHANGELOG.md +++ b/chopper/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 7.0.3 + +- Use ChopperLogRecord in HttpLoggingInterceptor to log lines ([#475](https://github.com/lejard-h/chopper/pull/475)) + ## 7.0.2 - Add option to pass custom Logger to HttpLoggingInterceptor ([#470](https://github.com/lejard-h/chopper/pull/470)) diff --git a/chopper/pubspec.yaml b/chopper/pubspec.yaml index f6be31f8..847b1b79 100644 --- a/chopper/pubspec.yaml +++ b/chopper/pubspec.yaml @@ -1,6 +1,6 @@ name: chopper description: Chopper is an http client generator using source_gen, inspired by Retrofit -version: 7.0.2 +version: 7.0.3 documentation: https://hadrien-lejard.gitbook.io/chopper repository: https://github.com/lejard-h/chopper From f8905ad97e09756fceac8e78c55fd634943465ab Mon Sep 17 00:00:00 2001 From: Max Rumpf Date: Mon, 14 Aug 2023 07:36:44 +0200 Subject: [PATCH 090/171] :adhesive_bandage: export ChopperLogRecord to library surface (#480) --- chopper/lib/chopper.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/chopper/lib/chopper.dart b/chopper/lib/chopper.dart index 2290febd..3c84987b 100644 --- a/chopper/lib/chopper.dart +++ b/chopper/lib/chopper.dart @@ -10,6 +10,7 @@ export 'src/constants.dart'; export 'src/extensions.dart'; export 'src/interceptor.dart'; export 'src/http_logging_interceptor.dart'; +export 'src/chopper_log_record.dart'; export 'src/request.dart'; export 'src/response.dart'; export 'src/utils.dart' hide mapToQuery; From c658d3980bbf3dbbcabcd81f378559764e7ae623 Mon Sep 17 00:00:00 2001 From: Max Rumpf Date: Mon, 14 Aug 2023 07:37:10 +0200 Subject: [PATCH 091/171] :label: make ChopperLogRecord final (#481) --- chopper/lib/src/chopper_log_record.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chopper/lib/src/chopper_log_record.dart b/chopper/lib/src/chopper_log_record.dart index 181ca492..1f02a036 100644 --- a/chopper/lib/src/chopper_log_record.dart +++ b/chopper/lib/src/chopper_log_record.dart @@ -1,7 +1,7 @@ import 'package:chopper/src/request.dart'; import 'package:chopper/src/response.dart'; -class ChopperLogRecord { +final class ChopperLogRecord { const ChopperLogRecord(this.message, {this.request, this.response}); final String message; From a0d70ace8554e0f9c4a422e1028f68b7fc9453c4 Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Mon, 14 Aug 2023 08:51:41 +0100 Subject: [PATCH 092/171] :twisted_rightwards_arrows: post release branch sync (#483) --- chopper/CHANGELOG.md | 5 +++++ chopper/pubspec.yaml | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/chopper/CHANGELOG.md b/chopper/CHANGELOG.md index 2035bf9d..ba98f843 100644 --- a/chopper/CHANGELOG.md +++ b/chopper/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## 7.0.4 + +- Export ChopperLogRecord to library surface ([#480](https://github.com/lejard-h/chopper/pull/480)) +- Make ChopperLogRecord final ([#481](https://github.com/lejard-h/chopper/pull/481)) + ## 7.0.3 - Use ChopperLogRecord in HttpLoggingInterceptor to log lines ([#475](https://github.com/lejard-h/chopper/pull/475)) diff --git a/chopper/pubspec.yaml b/chopper/pubspec.yaml index 847b1b79..2ec6ec11 100644 --- a/chopper/pubspec.yaml +++ b/chopper/pubspec.yaml @@ -1,6 +1,6 @@ name: chopper description: Chopper is an http client generator using source_gen, inspired by Retrofit -version: 7.0.3 +version: 7.0.4 documentation: https://hadrien-lejard.gitbook.io/chopper repository: https://github.com/lejard-h/chopper From 2ab9d87829bad8a5728b7eb3884d6a6ea563223f Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Wed, 16 Aug 2023 19:29:51 +0100 Subject: [PATCH 093/171] :arrow_up: analyzer: ">=5.13.0 <7.0.0" (#484) --- chopper_generator/pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chopper_generator/pubspec.yaml b/chopper_generator/pubspec.yaml index 4d541b87..3c612ef7 100644 --- a/chopper_generator/pubspec.yaml +++ b/chopper_generator/pubspec.yaml @@ -8,7 +8,7 @@ environment: sdk: ">=3.0.0 <4.0.0" dependencies: - analyzer: ^5.13.0 + analyzer: ">=5.13.0 <7.0.0" build: ^2.4.1 built_collection: ^5.1.1 chopper: ^7.0.0 From 08bab6c169a84f97e908fbd025182edfe78e3e68 Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Thu, 17 Aug 2023 10:22:01 +0100 Subject: [PATCH 094/171] :twisted_rightwards_arrows: post release branch sync (#486) --- chopper_generator/CHANGELOG.md | 4 ++++ chopper_generator/pubspec.yaml | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/chopper_generator/CHANGELOG.md b/chopper_generator/CHANGELOG.md index 7943acf4..55018b6f 100644 --- a/chopper_generator/CHANGELOG.md +++ b/chopper_generator/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 7.0.2 + +- Update analyzer dependency to >=5.13.0 <7.0.0 ([#484](https://github.com/lejard-h/chopper/pull/484)) + ## 7.0.1 - Add final class modifier to generated Chopper API implementations diff --git a/chopper_generator/pubspec.yaml b/chopper_generator/pubspec.yaml index 3c612ef7..6e2d7d4c 100644 --- a/chopper_generator/pubspec.yaml +++ b/chopper_generator/pubspec.yaml @@ -1,6 +1,6 @@ name: chopper_generator description: Chopper is an http client generator using source_gen, inspired by Retrofit -version: 7.0.1 +version: 7.0.2 documentation: https://hadrien-lejard.gitbook.io/chopper repository: https://github.com/lejard-h/chopper From 4c52b09cf5359d27075b4bdc81b67ef4094aba1c Mon Sep 17 00:00:00 2001 From: Hunter Wilhelm <71348224+hunterwilhelm@users.noreply.github.com> Date: Sat, 19 Aug 2023 03:04:03 -0600 Subject: [PATCH 095/171] Update README.md (#488) --- chopper/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/chopper/README.md b/chopper/README.md index b3453bef..d2419358 100644 --- a/chopper/README.md +++ b/chopper/README.md @@ -37,9 +37,9 @@ Latest versions: ## Documentation -* [Getting started](getting-started.md) -* [Converters](converters/converters.md) -* [Interceptors](interceptors.md) +* [Getting started](../getting-started.md) +* [Converters](../converters/converters.md) +* [Interceptors](../interceptors.md) ## Examples From c7b67a619901d785aba5efbd7745e6f171de937e Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Tue, 22 Aug 2023 09:27:52 +0100 Subject: [PATCH 096/171] :twisted_rightwards_arrows: post release branch sync (#491) --- chopper/CHANGELOG.md | 4 ++++ chopper/pubspec.yaml | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/chopper/CHANGELOG.md b/chopper/CHANGELOG.md index ba98f843..d820ae48 100644 --- a/chopper/CHANGELOG.md +++ b/chopper/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 7.0.5 + +- Fix documentation links in README ([#488](https://github.com/lejard-h/chopper/pull/488)) + ## 7.0.4 - Export ChopperLogRecord to library surface ([#480](https://github.com/lejard-h/chopper/pull/480)) diff --git a/chopper/pubspec.yaml b/chopper/pubspec.yaml index 2ec6ec11..0ddea437 100644 --- a/chopper/pubspec.yaml +++ b/chopper/pubspec.yaml @@ -1,6 +1,6 @@ name: chopper description: Chopper is an http client generator using source_gen, inspired by Retrofit -version: 7.0.4 +version: 7.0.5 documentation: https://hadrien-lejard.gitbook.io/chopper repository: https://github.com/lejard-h/chopper From e706150b9a57863114a43de337a9c30b7257a833 Mon Sep 17 00:00:00 2001 From: Martin Alejandro Escobar Espinel <56127727+martinale14@users.noreply.github.com> Date: Thu, 31 Aug 2023 03:08:36 -0500 Subject: [PATCH 097/171] Adding support for top level variables (#493) --- chopper/lib/src/annotations.dart | 2 + chopper/test/base_test.dart | 141 ++++ chopper/test/ensure_build_test.dart | 20 +- .../test/test_service_variable.chopper.dart | 641 ++++++++++++++++++ chopper/test/test_service_variable.dart | 220 ++++++ chopper_generator/lib/src/generator.dart | 75 +- chopper_generator/test/ensure_build_test.dart | 21 +- .../test/test_service_variable.chopper.dart | 641 ++++++++++++++++++ .../test/test_service_variable.dart | 220 ++++++ 9 files changed, 1956 insertions(+), 25 deletions(-) create mode 100644 chopper/test/test_service_variable.chopper.dart create mode 100644 chopper/test/test_service_variable.dart create mode 100644 chopper_generator/test/test_service_variable.chopper.dart create mode 100644 chopper_generator/test/test_service_variable.dart diff --git a/chopper/lib/src/annotations.dart b/chopper/lib/src/annotations.dart index 9030d432..4fa5e492 100644 --- a/chopper/lib/src/annotations.dart +++ b/chopper/lib/src/annotations.dart @@ -22,6 +22,8 @@ import 'package:meta/meta.dart'; @immutable final class ChopperApi { /// A part of a URL that every request defined inside a class annotated with [ChopperApi] will be prefixed with. + /// + /// The `baseUrl` can be a top level constant string variable. final String baseUrl; const ChopperApi({ diff --git a/chopper/test/base_test.dart b/chopper/test/base_test.dart index 3e6a6f01..dee04012 100644 --- a/chopper/test/base_test.dart +++ b/chopper/test/base_test.dart @@ -9,8 +9,10 @@ import 'package:http/testing.dart'; import 'package:test/test.dart'; import 'test_service.dart'; +import 'test_service_variable.dart'; final baseUrl = Uri.parse('http://localhost:8000'); +const String testEnv = 'https://localhost:4000'; void main() { ChopperClient buildClient([ @@ -22,6 +24,7 @@ void main() { services: [ // the generated service HttpTestService.create(), + HttpTestServiceVariable.create(), ], client: httpClient, errorConverter: errorConverter, @@ -35,9 +38,12 @@ void main() { final chopper = buildClient(httpClient); final service = chopper.getService(); + final serviceVariable = chopper.getService(); expect(service, isNotNull); + expect(serviceVariable, isNotNull); expect(service, isA()); + expect(serviceVariable, isA()); }); test('get service errors', () async { @@ -86,6 +92,28 @@ void main() { httpClient.close(); }); + test('GET Variable', () async { + final httpClient = MockClient((request) async { + expect( + request.url.toString(), + equals('$testEnv/get/1234'), + ); + expect(request.method, equals('GET')); + + return http.Response('get response', 200); + }); + + final chopper = buildClient(httpClient); + final service = chopper.getService(); + + final response = await service.getTest('1234', dynamicHeader: ''); + + expect(response.body, equals('get response')); + expect(response.statusCode, equals(200)); + + httpClient.close(); + }); + test('GET stream', () async { final httpClient = MockClient.streaming((request, stream) async { expect( @@ -231,6 +259,29 @@ void main() { httpClient.close(); }); + test('POST Variable', () async { + final httpClient = MockClient((request) async { + expect( + request.url.toString(), + equals('$testEnv/post'), + ); + expect(request.method, equals('POST')); + expect(request.body, equals('post body')); + + return http.Response('post response', 200); + }); + + final chopper = buildClient(httpClient); + final service = chopper.getService(); + + final response = await service.postTest('post body'); + + expect(response.body, equals('post response')); + expect(response.statusCode, equals(200)); + + httpClient.close(); + }); + test('POST with streamed body', () async { final httpClient = MockClient((request) async { expect( @@ -282,6 +333,29 @@ void main() { httpClient.close(); }); + test('PUT Variable', () async { + final httpClient = MockClient((request) async { + expect( + request.url.toString(), + equals('$testEnv/put/1234'), + ); + expect(request.method, equals('PUT')); + expect(request.body, equals('put body')); + + return http.Response('put response', 200); + }); + + final chopper = buildClient(httpClient); + final service = chopper.getService(); + + final response = await service.putTest('1234', 'put body'); + + expect(response.body, equals('put response')); + expect(response.statusCode, equals(200)); + + httpClient.close(); + }); + test('PATCH', () async { final httpClient = MockClient((request) async { expect( @@ -305,6 +379,29 @@ void main() { httpClient.close(); }); + test('PATCH Variable', () async { + final httpClient = MockClient((request) async { + expect( + request.url.toString(), + equals('$testEnv/patch/1234'), + ); + expect(request.method, equals('PATCH')); + expect(request.body, equals('patch body')); + + return http.Response('patch response', 200); + }); + + final chopper = buildClient(httpClient); + final service = chopper.getService(); + + final response = await service.patchTest('1234', 'patch body'); + + expect(response.body, equals('patch response')); + expect(response.statusCode, equals(200)); + + httpClient.close(); + }); + test('DELETE', () async { final httpClient = MockClient((request) async { expect( @@ -327,6 +424,28 @@ void main() { httpClient.close(); }); + test('DELETE Variable', () async { + final httpClient = MockClient((request) async { + expect( + request.url.toString(), + equals('$testEnv/delete/1234'), + ); + expect(request.method, equals('DELETE')); + + return http.Response('delete response', 200); + }); + + final chopper = buildClient(httpClient); + final service = chopper.getService(); + + final response = await service.deleteTest('1234'); + + expect(response.body, equals('delete response')); + expect(response.statusCode, equals(200)); + + httpClient.close(); + }); + test('Head', () async { final httpClient = MockClient((request) async { expect( @@ -349,6 +468,28 @@ void main() { httpClient.close(); }); + test('Head Variable', () async { + final httpClient = MockClient((request) async { + expect( + request.url.toString(), + equals('$testEnv/head'), + ); + expect(request.method, equals('HEAD')); + + return http.Response('head response', 200); + }); + + final chopper = buildClient(httpClient); + final service = chopper.getService(); + + final response = await service.headTest(); + + expect(response.body, equals('head response')); + expect(response.statusCode, equals(200)); + + httpClient.close(); + }); + test('const headers', () async { final client = MockClient((http.Request req) async { expect(req.headers.containsKey('foo'), isTrue); diff --git a/chopper/test/ensure_build_test.dart b/chopper/test/ensure_build_test.dart index 73f46337..a599c5d2 100644 --- a/chopper/test/ensure_build_test.dart +++ b/chopper/test/ensure_build_test.dart @@ -6,11 +6,19 @@ import 'package:test/test.dart'; void main() { test( 'ensure_build', - () => expectBuildClean( - packageRelativeDirectory: 'chopper', - gitDiffPathArguments: [ - 'test/test_service.chopper.dart', - ], - ), + () { + expectBuildClean( + packageRelativeDirectory: 'chopper', + gitDiffPathArguments: [ + 'test/test_service.chopper.dart', + ], + ); + expectBuildClean( + packageRelativeDirectory: 'chopper', + gitDiffPathArguments: [ + 'test/test_service_variable.chopper.dart', + ], + ); + }, ); } diff --git a/chopper/test/test_service_variable.chopper.dart b/chopper/test/test_service_variable.chopper.dart new file mode 100644 index 00000000..c4423e71 --- /dev/null +++ b/chopper/test/test_service_variable.chopper.dart @@ -0,0 +1,641 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'test_service_variable.dart'; + +// ************************************************************************** +// ChopperGenerator +// ************************************************************************** + +// ignore_for_file: always_put_control_body_on_new_line, always_specify_types, prefer_const_declarations, unnecessary_brace_in_string_interps +final class _$HttpTestServiceVariable extends HttpTestServiceVariable { + _$HttpTestServiceVariable([ChopperClient? client]) { + if (client == null) return; + this.client = client; + } + + @override + final definitionType = HttpTestServiceVariable; + + @override + Future> getTest( + String id, { + required String dynamicHeader, + }) { + final Uri $url = Uri.parse('${service}/get/${id}'); + final Map $headers = { + 'test': dynamicHeader, + }; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + headers: $headers, + ); + return client.send($request); + } + + @override + Future> headTest() { + final Uri $url = Uri.parse('${service}/head'); + final Request $request = Request( + 'HEAD', + $url, + client.baseUrl, + ); + return client.send($request); + } + + @override + Future> optionsTest() { + final Uri $url = Uri.parse('${service}/options'); + final Request $request = Request( + 'OPTIONS', + $url, + client.baseUrl, + ); + return client.send($request); + } + + @override + Future>>> getStreamTest() { + final Uri $url = Uri.parse('${service}/get'); + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + ); + return client.send>, int>($request); + } + + @override + Future> getAll() { + final Uri $url = Uri.parse('${service}'); + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + ); + return client.send($request); + } + + @override + Future> getAllWithTrailingSlash() { + final Uri $url = Uri.parse('${service}/'); + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + ); + return client.send($request); + } + + @override + Future> getQueryTest({ + String name = '', + int? number, + int? def = 42, + }) { + final Uri $url = Uri.parse('${service}/query'); + final Map $params = { + 'name': name, + 'int': number, + 'default_value': def, + }; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + ); + return client.send($request); + } + + @override + Future> getQueryMapTest(Map query) { + final Uri $url = Uri.parse('${service}/query_map'); + final Map $params = query; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + ); + return client.send($request); + } + + @override + Future> getQueryMapTest2( + Map query, { + bool? test, + }) { + final Uri $url = Uri.parse('${service}/query_map'); + final Map $params = {'test': test}; + $params.addAll(query); + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + ); + return client.send($request); + } + + @override + Future> getQueryMapTest3({ + String name = '', + int? number, + Map filters = const {}, + }) { + final Uri $url = Uri.parse('${service}/query_map'); + final Map $params = { + 'name': name, + 'number': number, + }; + $params.addAll(filters); + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + ); + return client.send($request); + } + + @override + Future> getQueryMapTest4({ + String name = '', + int? number, + Map? filters, + }) { + final Uri $url = Uri.parse('${service}/query_map'); + final Map $params = { + 'name': name, + 'number': number, + }; + $params.addAll(filters ?? const {}); + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + ); + return client.send($request); + } + + @override + Future> getQueryMapTest5({Map? filters}) { + final Uri $url = Uri.parse('${service}/query_map'); + final Map $params = filters ?? const {}; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + ); + return client.send($request); + } + + @override + Future> getBody(dynamic body) { + final Uri $url = Uri.parse('${service}/get_body'); + final $body = body; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + body: $body, + ); + return client.send($request); + } + + @override + Future> postTest(String data) { + final Uri $url = Uri.parse('${service}/post'); + final $body = data; + final Request $request = Request( + 'POST', + $url, + client.baseUrl, + body: $body, + ); + return client.send($request); + } + + @override + Future> postStreamTest(Stream> byteStream) { + final Uri $url = Uri.parse('${service}/post'); + final $body = byteStream; + final Request $request = Request( + 'POST', + $url, + client.baseUrl, + body: $body, + ); + return client.send($request); + } + + @override + Future> putTest( + String test, + String data, + ) { + final Uri $url = Uri.parse('${service}/put/${test}'); + final $body = data; + final Request $request = Request( + 'PUT', + $url, + client.baseUrl, + body: $body, + ); + return client.send($request); + } + + @override + Future> deleteTest(String id) { + final Uri $url = Uri.parse('${service}/delete/${id}'); + final Map $headers = { + 'foo': 'bar', + }; + final Request $request = Request( + 'DELETE', + $url, + client.baseUrl, + headers: $headers, + ); + return client.send($request); + } + + @override + Future> patchTest( + String id, + String data, + ) { + final Uri $url = Uri.parse('${service}/patch/${id}'); + final $body = data; + final Request $request = Request( + 'PATCH', + $url, + client.baseUrl, + body: $body, + ); + return client.send($request); + } + + @override + Future> mapTest(Map map) { + final Uri $url = Uri.parse('${service}/map'); + final $body = map; + final Request $request = Request( + 'POST', + $url, + client.baseUrl, + body: $body, + ); + return client.send($request); + } + + @override + Future> postForm(Map fields) { + final Uri $url = Uri.parse('${service}/form/body'); + final $body = fields; + final Request $request = Request( + 'POST', + $url, + client.baseUrl, + body: $body, + ); + return client.send( + $request, + requestConverter: convertForm, + ); + } + + @override + Future> postFormUsingHeaders(Map fields) { + final Uri $url = Uri.parse('${service}/form/body'); + final Map $headers = { + 'content-type': 'application/x-www-form-urlencoded', + }; + final $body = fields; + final Request $request = Request( + 'POST', + $url, + client.baseUrl, + body: $body, + headers: $headers, + ); + return client.send($request); + } + + @override + Future> postFormFields( + String foo, + int bar, + ) { + final Uri $url = Uri.parse('${service}/form/body/fields'); + final $body = { + 'foo': foo, + 'bar': bar, + }; + final Request $request = Request( + 'POST', + $url, + client.baseUrl, + body: $body, + ); + return client.send( + $request, + requestConverter: convertForm, + ); + } + + @override + Future> forceJsonTest(Map map) { + final Uri $url = Uri.parse('${service}/map/json'); + final $body = map; + final Request $request = Request( + 'POST', + $url, + client.baseUrl, + body: $body, + ); + return client.send( + $request, + requestConverter: customConvertRequest, + responseConverter: customConvertResponse, + ); + } + + @override + Future> postResources( + Map a, + Map b, + ) { + final Uri $url = Uri.parse('${service}/multi'); + final List $parts = [ + PartValue>( + '1', + a, + ), + PartValue>( + '2', + b, + ), + ]; + final Request $request = Request( + 'POST', + $url, + client.baseUrl, + parts: $parts, + multipart: true, + ); + return client.send($request); + } + + @override + Future> postFile(List bytes) { + final Uri $url = Uri.parse('${service}/file'); + final List $parts = [ + PartValueFile>( + 'file', + bytes, + ) + ]; + final Request $request = Request( + 'POST', + $url, + client.baseUrl, + parts: $parts, + multipart: true, + ); + return client.send($request); + } + + @override + Future> postImage(List imageData) { + final Uri $url = Uri.parse('${service}/image'); + final List $parts = [ + PartValueFile>( + 'image', + imageData, + ) + ]; + final Request $request = Request( + 'POST', + $url, + client.baseUrl, + parts: $parts, + multipart: true, + ); + return client.send($request); + } + + @override + Future> postMultipartFile( + MultipartFile file, { + String? id, + }) { + final Uri $url = Uri.parse('${service}/file'); + final List $parts = [ + PartValue( + 'id', + id, + ), + PartValueFile( + 'file', + file, + ), + ]; + final Request $request = Request( + 'POST', + $url, + client.baseUrl, + parts: $parts, + multipart: true, + ); + return client.send($request); + } + + @override + Future> postListFiles(List files) { + final Uri $url = Uri.parse('${service}/files'); + final List $parts = [ + PartValueFile>( + 'files', + files, + ) + ]; + final Request $request = Request( + 'POST', + $url, + client.baseUrl, + parts: $parts, + multipart: true, + ); + return client.send($request); + } + + @override + Future> postMultipartList({ + required List ints, + required List doubles, + required List nums, + required List strings, + }) { + final Uri $url = Uri.parse('${service}/multipart_list'); + final List $parts = [ + PartValue>( + 'ints', + ints, + ), + PartValue>( + 'doubles', + doubles, + ), + PartValue>( + 'nums', + nums, + ), + PartValue>( + 'strings', + strings, + ), + ]; + final Request $request = Request( + 'POST', + $url, + client.baseUrl, + parts: $parts, + multipart: true, + ); + return client.send($request); + } + + @override + Future fullUrl() { + final Uri $url = Uri.parse('https://test.com'); + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + ); + return client.send($request); + } + + @override + Future>> listString() { + final Uri $url = Uri.parse('${service}/list/string'); + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + ); + return client.send, String>($request); + } + + @override + Future> noBody() { + final Uri $url = Uri.parse('${service}/no-body'); + final Request $request = Request( + 'POST', + $url, + client.baseUrl, + ); + return client.send($request); + } + + @override + Future> getUsingQueryParamIncludeNullQueryVars({ + String? foo, + String? bar, + String? baz, + }) { + final Uri $url = + Uri.parse('${service}/query_param_include_null_query_vars'); + final Map $params = { + 'foo': foo, + 'bar': bar, + 'baz': baz, + }; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + includeNullQueryVars: true, + ); + return client.send($request); + } + + @override + Future> getUsingListQueryParam(List value) { + final Uri $url = Uri.parse('${service}/list_query_param'); + final Map $params = {'value': value}; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + ); + return client.send($request); + } + + @override + Future> getUsingListQueryParamWithBrackets( + List value) { + final Uri $url = Uri.parse('${service}/list_query_param_with_brackets'); + final Map $params = {'value': value}; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + useBrackets: true, + ); + return client.send($request); + } + + @override + Future> getUsingMapQueryParam(Map value) { + final Uri $url = Uri.parse('${service}/map_query_param'); + final Map $params = {'value': value}; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + ); + return client.send($request); + } + + @override + Future> getUsingMapQueryParamIncludeNullQueryVars( + Map value) { + final Uri $url = + Uri.parse('${service}/map_query_param_include_null_query_vars'); + final Map $params = {'value': value}; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + includeNullQueryVars: true, + ); + return client.send($request); + } + + @override + Future> getUsingMapQueryParamWithBrackets( + Map value) { + final Uri $url = Uri.parse('${service}/map_query_param_with_brackets'); + final Map $params = {'value': value}; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + useBrackets: true, + ); + return client.send($request); + } +} diff --git a/chopper/test/test_service_variable.dart b/chopper/test/test_service_variable.dart new file mode 100644 index 00000000..81532976 --- /dev/null +++ b/chopper/test/test_service_variable.dart @@ -0,0 +1,220 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:chopper/chopper.dart'; +import 'package:http/http.dart' show MultipartFile; + +part 'test_service_variable.chopper.dart'; + +const String service = 'https://localhost:4000'; + +@ChopperApi(baseUrl: service) +abstract class HttpTestServiceVariable extends ChopperService { + static HttpTestServiceVariable create([ChopperClient? client]) => + _$HttpTestServiceVariable(client); + + @Get(path: 'get/{id}') + Future> getTest( + @Path() String id, { + @Header('test') required String dynamicHeader, + }); + + @Head(path: 'head') + Future headTest(); + + @Options(path: 'options') + Future optionsTest(); + + @Get(path: 'get') + Future>>> getStreamTest(); + + @Get(path: '') + Future getAll(); + + @Get(path: '/') + Future getAllWithTrailingSlash(); + + @Get(path: 'query') + Future getQueryTest({ + @Query('name') String name = '', + @Query('int') int? number, + @Query('default_value') int? def = 42, + }); + + @Get(path: 'query_map') + Future getQueryMapTest(@QueryMap() Map query); + + @Get(path: 'query_map') + Future getQueryMapTest2( + @QueryMap() Map query, { + @Query('test') bool? test, + }); + + @Get(path: 'query_map') + Future getQueryMapTest3({ + @Query('name') String name = '', + @Query('number') int? number, + @QueryMap() Map filters = const {}, + }); + + @Get(path: 'query_map') + Future getQueryMapTest4({ + @Query('name') String name = '', + @Query('number') int? number, + @QueryMap() Map? filters, + }); + + @Get(path: 'query_map') + Future getQueryMapTest5({ + @QueryMap() Map? filters, + }); + + @Get(path: 'get_body') + Future getBody(@Body() dynamic body); + + @Post(path: 'post') + Future postTest(@Body() String data); + + @Post(path: 'post') + Future postStreamTest(@Body() Stream> byteStream); + + @Put(path: 'put/{id}') + Future putTest(@Path('id') String test, @Body() String data); + + @Delete(path: 'delete/{id}', headers: {'foo': 'bar'}) + Future deleteTest(@Path() String id); + + @Patch(path: 'patch/{id}') + Future patchTest(@Path() String id, @Body() String data); + + @Post(path: 'map') + Future mapTest(@Body() Map map); + + @FactoryConverter(request: convertForm) + @Post(path: 'form/body') + Future postForm(@Body() Map fields); + + @Post(path: 'form/body', headers: {contentTypeKey: formEncodedHeaders}) + Future postFormUsingHeaders(@Body() Map fields); + + @FactoryConverter(request: convertForm) + @Post(path: 'form/body/fields') + Future postFormFields(@Field() String foo, @Field() int bar); + + @Post(path: 'map/json') + @FactoryConverter( + request: customConvertRequest, + response: customConvertResponse, + ) + Future forceJsonTest(@Body() Map map); + + @Post(path: 'multi') + @multipart + Future postResources( + @Part('1') Map a, + @Part('2') Map b, + ); + + @Post(path: 'file') + @multipart + Future postFile( + @PartFile('file') List bytes, + ); + + @Post(path: 'image') + @multipart + Future postImage( + @PartFile('image') List imageData, + ); + + @Post(path: 'file') + @multipart + Future postMultipartFile( + @PartFile() MultipartFile file, { + @Part() String? id, + }); + + @Post(path: 'files') + @multipart + Future postListFiles(@PartFile() List files); + + @Post(path: 'multipart_list') + @multipart + Future postMultipartList({ + @Part('ints') required List ints, + @Part('doubles') required List doubles, + @Part('nums') required List nums, + @Part('strings') required List strings, + }); + + @Get(path: 'https://test.com') + Future fullUrl(); + + @Get(path: '/list/string') + Future>> listString(); + + @Post(path: 'no-body') + Future noBody(); + + @Get(path: '/query_param_include_null_query_vars', includeNullQueryVars: true) + Future> getUsingQueryParamIncludeNullQueryVars({ + @Query('foo') String? foo, + @Query('bar') String? bar, + @Query('baz') String? baz, + }); + + @Get(path: '/list_query_param') + Future> getUsingListQueryParam( + @Query('value') List value, + ); + + @Get(path: '/list_query_param_with_brackets', useBrackets: true) + Future> getUsingListQueryParamWithBrackets( + @Query('value') List value, + ); + + @Get(path: '/map_query_param') + Future> getUsingMapQueryParam( + @Query('value') Map value, + ); + + @Get( + path: '/map_query_param_include_null_query_vars', + includeNullQueryVars: true, + ) + Future> getUsingMapQueryParamIncludeNullQueryVars( + @Query('value') Map value, + ); + + @Get(path: '/map_query_param_with_brackets', useBrackets: true) + Future> getUsingMapQueryParamWithBrackets( + @Query('value') Map value, + ); +} + +Request customConvertRequest(Request req) { + final r = JsonConverter().convertRequest(req); + + return applyHeader(r, 'customConverter', 'true'); +} + +Response customConvertResponse(Response res) => + res.copyWith(body: json.decode(res.body)); + +Request convertForm(Request req) { + req = applyHeader(req, contentTypeKey, formEncodedHeaders); + + if (req.body is Map) { + final body = {}; + + req.body.forEach((key, val) { + if (val != null) { + body[key.toString()] = val.toString(); + } + }); + + req = req.copyWith(body: body); + } + + return req; +} diff --git a/chopper_generator/lib/src/generator.dart b/chopper_generator/lib/src/generator.dart index 70ee0790..81518109 100644 --- a/chopper_generator/lib/src/generator.dart +++ b/chopper_generator/lib/src/generator.dart @@ -63,8 +63,21 @@ final class ChopperGenerator final String friendlyName = element.name; final String name = '_\$$friendlyName'; - final String baseUrl = - annotation.peek(Vars.baseUrl.toString())?.stringValue ?? ''; + + final ConstantReader? baseUrlReader = + annotation.peek(Vars.baseUrl.toString()); + + TopLevelVariableElement? baseUrlVariableElement; + + final VariableElement? posibleBaseUrl = baseUrlReader?.objectValue.variable; + + if (posibleBaseUrl is TopLevelVariableElement && + posibleBaseUrl.type.isDartCoreString && + posibleBaseUrl.isConst) { + baseUrlVariableElement = posibleBaseUrl; + } + + final String baseUrl = baseUrlReader?.stringValue ?? ''; final Class classBuilder = Class((builder) { builder @@ -73,7 +86,11 @@ final class ChopperGenerator ..extend = refer(friendlyName) ..fields.add(_buildDefinitionTypeMethod(friendlyName)) ..constructors.add(_generateConstructor()) - ..methods.addAll(_parseMethods(element, baseUrl)); + ..methods.addAll(_parseMethods( + element, + baseUrl, + baseUrlVariableElement, + )); }); const String ignore = '// ignore_for_file: ' @@ -102,7 +119,11 @@ final class ChopperGenerator }, ); - static Iterable _parseMethods(ClassElement element, String baseUrl) => + static Iterable _parseMethods( + ClassElement element, + String baseUrl, + TopLevelVariableElement? baseUrlVariableElement, + ) => element.methods .where( (MethodElement method) => @@ -110,9 +131,19 @@ final class ChopperGenerator method.isAbstract && method.returnType.isDartAsyncFuture, ) - .map((MethodElement m) => _generateMethod(m, baseUrl)); + .map( + (MethodElement m) => _generateMethod( + m, + baseUrl, + baseUrlVariableElement, + ), + ); - static Method _generateMethod(MethodElement m, String baseUrl) { + static Method _generateMethod( + MethodElement m, + String baseUrl, + TopLevelVariableElement? baseUrlVariableElement, + ) { final ConstantReader? method = _getMethodAnnotation(m); final bool multipart = _hasAnnotation(m, chopper.Multipart); final ConstantReader? factoryConverter = _getFactoryConverterAnnotation(m); @@ -138,7 +169,12 @@ final class ChopperGenerator _getAnnotation(m, chopper.PartFileMap); final Code? headers = _generateHeaders(m, method!); - final Expression url = _generateUrl(method, paths, baseUrl); + final Expression url = _generateUrl( + method, + paths, + baseUrl, + baseUrlVariableElement, + ); final DartType? responseType = _getResponseType(m.returnType); final DartType? responseInnerType = _getResponseInnerType(m.returnType) ?? responseType; @@ -481,6 +517,7 @@ final class ChopperGenerator ConstantReader method, Map paths, String baseUrl, + TopLevelVariableElement? baseUrlVariableElement, ) { String path = Utils.getMethodPath(method); paths.forEach((p, ConstantReader r) { @@ -498,14 +535,26 @@ final class ChopperGenerator return _generateUri(''); } - if (path.isNotEmpty && - baseUrl.isNotEmpty && - !baseUrl.endsWith('/') && - !path.startsWith('/')) { - return _generateUri('$baseUrl/$path'); + String finalBaseUrl = baseUrl; + + if (baseUrlVariableElement != null) { + finalBaseUrl = '\${${baseUrlVariableElement.displayName}}'; + } + + if (path.isNotEmpty && baseUrl.isNotEmpty) { + bool pathHasSlash = path.startsWith('/'); + bool baseUrlHasSlash = baseUrl.endsWith('/'); + + if ((!baseUrlHasSlash && !pathHasSlash)) { + return _generateUri('$finalBaseUrl/$path'); + } + + if (baseUrlHasSlash && pathHasSlash) { + return _generateUri('$finalBaseUrl${path.replaceFirst('/', '')}'); + } } - return _generateUri('$baseUrl$path'); + return _generateUri('$finalBaseUrl$path'.replaceAll('//', '/')); } static Expression _generateUri(String url) => diff --git a/chopper_generator/test/ensure_build_test.dart b/chopper_generator/test/ensure_build_test.dart index cb5dc60f..c708d8d1 100644 --- a/chopper_generator/test/ensure_build_test.dart +++ b/chopper_generator/test/ensure_build_test.dart @@ -6,11 +6,20 @@ import 'package:test/test.dart'; void main() { test( 'ensure_build', - () => expectBuildClean( - packageRelativeDirectory: 'chopper_generator', - gitDiffPathArguments: [ - 'test/test_service.chopper.dart', - ], - ), + () { + expectBuildClean( + packageRelativeDirectory: 'chopper_generator', + gitDiffPathArguments: [ + 'test/test_service.chopper.dart', + ], + ); + + expectBuildClean( + packageRelativeDirectory: 'chopper_generator', + gitDiffPathArguments: [ + 'test/test_service_variable.chopper.dart', + ], + ); + }, ); } diff --git a/chopper_generator/test/test_service_variable.chopper.dart b/chopper_generator/test/test_service_variable.chopper.dart new file mode 100644 index 00000000..c4423e71 --- /dev/null +++ b/chopper_generator/test/test_service_variable.chopper.dart @@ -0,0 +1,641 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'test_service_variable.dart'; + +// ************************************************************************** +// ChopperGenerator +// ************************************************************************** + +// ignore_for_file: always_put_control_body_on_new_line, always_specify_types, prefer_const_declarations, unnecessary_brace_in_string_interps +final class _$HttpTestServiceVariable extends HttpTestServiceVariable { + _$HttpTestServiceVariable([ChopperClient? client]) { + if (client == null) return; + this.client = client; + } + + @override + final definitionType = HttpTestServiceVariable; + + @override + Future> getTest( + String id, { + required String dynamicHeader, + }) { + final Uri $url = Uri.parse('${service}/get/${id}'); + final Map $headers = { + 'test': dynamicHeader, + }; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + headers: $headers, + ); + return client.send($request); + } + + @override + Future> headTest() { + final Uri $url = Uri.parse('${service}/head'); + final Request $request = Request( + 'HEAD', + $url, + client.baseUrl, + ); + return client.send($request); + } + + @override + Future> optionsTest() { + final Uri $url = Uri.parse('${service}/options'); + final Request $request = Request( + 'OPTIONS', + $url, + client.baseUrl, + ); + return client.send($request); + } + + @override + Future>>> getStreamTest() { + final Uri $url = Uri.parse('${service}/get'); + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + ); + return client.send>, int>($request); + } + + @override + Future> getAll() { + final Uri $url = Uri.parse('${service}'); + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + ); + return client.send($request); + } + + @override + Future> getAllWithTrailingSlash() { + final Uri $url = Uri.parse('${service}/'); + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + ); + return client.send($request); + } + + @override + Future> getQueryTest({ + String name = '', + int? number, + int? def = 42, + }) { + final Uri $url = Uri.parse('${service}/query'); + final Map $params = { + 'name': name, + 'int': number, + 'default_value': def, + }; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + ); + return client.send($request); + } + + @override + Future> getQueryMapTest(Map query) { + final Uri $url = Uri.parse('${service}/query_map'); + final Map $params = query; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + ); + return client.send($request); + } + + @override + Future> getQueryMapTest2( + Map query, { + bool? test, + }) { + final Uri $url = Uri.parse('${service}/query_map'); + final Map $params = {'test': test}; + $params.addAll(query); + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + ); + return client.send($request); + } + + @override + Future> getQueryMapTest3({ + String name = '', + int? number, + Map filters = const {}, + }) { + final Uri $url = Uri.parse('${service}/query_map'); + final Map $params = { + 'name': name, + 'number': number, + }; + $params.addAll(filters); + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + ); + return client.send($request); + } + + @override + Future> getQueryMapTest4({ + String name = '', + int? number, + Map? filters, + }) { + final Uri $url = Uri.parse('${service}/query_map'); + final Map $params = { + 'name': name, + 'number': number, + }; + $params.addAll(filters ?? const {}); + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + ); + return client.send($request); + } + + @override + Future> getQueryMapTest5({Map? filters}) { + final Uri $url = Uri.parse('${service}/query_map'); + final Map $params = filters ?? const {}; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + ); + return client.send($request); + } + + @override + Future> getBody(dynamic body) { + final Uri $url = Uri.parse('${service}/get_body'); + final $body = body; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + body: $body, + ); + return client.send($request); + } + + @override + Future> postTest(String data) { + final Uri $url = Uri.parse('${service}/post'); + final $body = data; + final Request $request = Request( + 'POST', + $url, + client.baseUrl, + body: $body, + ); + return client.send($request); + } + + @override + Future> postStreamTest(Stream> byteStream) { + final Uri $url = Uri.parse('${service}/post'); + final $body = byteStream; + final Request $request = Request( + 'POST', + $url, + client.baseUrl, + body: $body, + ); + return client.send($request); + } + + @override + Future> putTest( + String test, + String data, + ) { + final Uri $url = Uri.parse('${service}/put/${test}'); + final $body = data; + final Request $request = Request( + 'PUT', + $url, + client.baseUrl, + body: $body, + ); + return client.send($request); + } + + @override + Future> deleteTest(String id) { + final Uri $url = Uri.parse('${service}/delete/${id}'); + final Map $headers = { + 'foo': 'bar', + }; + final Request $request = Request( + 'DELETE', + $url, + client.baseUrl, + headers: $headers, + ); + return client.send($request); + } + + @override + Future> patchTest( + String id, + String data, + ) { + final Uri $url = Uri.parse('${service}/patch/${id}'); + final $body = data; + final Request $request = Request( + 'PATCH', + $url, + client.baseUrl, + body: $body, + ); + return client.send($request); + } + + @override + Future> mapTest(Map map) { + final Uri $url = Uri.parse('${service}/map'); + final $body = map; + final Request $request = Request( + 'POST', + $url, + client.baseUrl, + body: $body, + ); + return client.send($request); + } + + @override + Future> postForm(Map fields) { + final Uri $url = Uri.parse('${service}/form/body'); + final $body = fields; + final Request $request = Request( + 'POST', + $url, + client.baseUrl, + body: $body, + ); + return client.send( + $request, + requestConverter: convertForm, + ); + } + + @override + Future> postFormUsingHeaders(Map fields) { + final Uri $url = Uri.parse('${service}/form/body'); + final Map $headers = { + 'content-type': 'application/x-www-form-urlencoded', + }; + final $body = fields; + final Request $request = Request( + 'POST', + $url, + client.baseUrl, + body: $body, + headers: $headers, + ); + return client.send($request); + } + + @override + Future> postFormFields( + String foo, + int bar, + ) { + final Uri $url = Uri.parse('${service}/form/body/fields'); + final $body = { + 'foo': foo, + 'bar': bar, + }; + final Request $request = Request( + 'POST', + $url, + client.baseUrl, + body: $body, + ); + return client.send( + $request, + requestConverter: convertForm, + ); + } + + @override + Future> forceJsonTest(Map map) { + final Uri $url = Uri.parse('${service}/map/json'); + final $body = map; + final Request $request = Request( + 'POST', + $url, + client.baseUrl, + body: $body, + ); + return client.send( + $request, + requestConverter: customConvertRequest, + responseConverter: customConvertResponse, + ); + } + + @override + Future> postResources( + Map a, + Map b, + ) { + final Uri $url = Uri.parse('${service}/multi'); + final List $parts = [ + PartValue>( + '1', + a, + ), + PartValue>( + '2', + b, + ), + ]; + final Request $request = Request( + 'POST', + $url, + client.baseUrl, + parts: $parts, + multipart: true, + ); + return client.send($request); + } + + @override + Future> postFile(List bytes) { + final Uri $url = Uri.parse('${service}/file'); + final List $parts = [ + PartValueFile>( + 'file', + bytes, + ) + ]; + final Request $request = Request( + 'POST', + $url, + client.baseUrl, + parts: $parts, + multipart: true, + ); + return client.send($request); + } + + @override + Future> postImage(List imageData) { + final Uri $url = Uri.parse('${service}/image'); + final List $parts = [ + PartValueFile>( + 'image', + imageData, + ) + ]; + final Request $request = Request( + 'POST', + $url, + client.baseUrl, + parts: $parts, + multipart: true, + ); + return client.send($request); + } + + @override + Future> postMultipartFile( + MultipartFile file, { + String? id, + }) { + final Uri $url = Uri.parse('${service}/file'); + final List $parts = [ + PartValue( + 'id', + id, + ), + PartValueFile( + 'file', + file, + ), + ]; + final Request $request = Request( + 'POST', + $url, + client.baseUrl, + parts: $parts, + multipart: true, + ); + return client.send($request); + } + + @override + Future> postListFiles(List files) { + final Uri $url = Uri.parse('${service}/files'); + final List $parts = [ + PartValueFile>( + 'files', + files, + ) + ]; + final Request $request = Request( + 'POST', + $url, + client.baseUrl, + parts: $parts, + multipart: true, + ); + return client.send($request); + } + + @override + Future> postMultipartList({ + required List ints, + required List doubles, + required List nums, + required List strings, + }) { + final Uri $url = Uri.parse('${service}/multipart_list'); + final List $parts = [ + PartValue>( + 'ints', + ints, + ), + PartValue>( + 'doubles', + doubles, + ), + PartValue>( + 'nums', + nums, + ), + PartValue>( + 'strings', + strings, + ), + ]; + final Request $request = Request( + 'POST', + $url, + client.baseUrl, + parts: $parts, + multipart: true, + ); + return client.send($request); + } + + @override + Future fullUrl() { + final Uri $url = Uri.parse('https://test.com'); + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + ); + return client.send($request); + } + + @override + Future>> listString() { + final Uri $url = Uri.parse('${service}/list/string'); + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + ); + return client.send, String>($request); + } + + @override + Future> noBody() { + final Uri $url = Uri.parse('${service}/no-body'); + final Request $request = Request( + 'POST', + $url, + client.baseUrl, + ); + return client.send($request); + } + + @override + Future> getUsingQueryParamIncludeNullQueryVars({ + String? foo, + String? bar, + String? baz, + }) { + final Uri $url = + Uri.parse('${service}/query_param_include_null_query_vars'); + final Map $params = { + 'foo': foo, + 'bar': bar, + 'baz': baz, + }; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + includeNullQueryVars: true, + ); + return client.send($request); + } + + @override + Future> getUsingListQueryParam(List value) { + final Uri $url = Uri.parse('${service}/list_query_param'); + final Map $params = {'value': value}; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + ); + return client.send($request); + } + + @override + Future> getUsingListQueryParamWithBrackets( + List value) { + final Uri $url = Uri.parse('${service}/list_query_param_with_brackets'); + final Map $params = {'value': value}; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + useBrackets: true, + ); + return client.send($request); + } + + @override + Future> getUsingMapQueryParam(Map value) { + final Uri $url = Uri.parse('${service}/map_query_param'); + final Map $params = {'value': value}; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + ); + return client.send($request); + } + + @override + Future> getUsingMapQueryParamIncludeNullQueryVars( + Map value) { + final Uri $url = + Uri.parse('${service}/map_query_param_include_null_query_vars'); + final Map $params = {'value': value}; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + includeNullQueryVars: true, + ); + return client.send($request); + } + + @override + Future> getUsingMapQueryParamWithBrackets( + Map value) { + final Uri $url = Uri.parse('${service}/map_query_param_with_brackets'); + final Map $params = {'value': value}; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + useBrackets: true, + ); + return client.send($request); + } +} diff --git a/chopper_generator/test/test_service_variable.dart b/chopper_generator/test/test_service_variable.dart new file mode 100644 index 00000000..81532976 --- /dev/null +++ b/chopper_generator/test/test_service_variable.dart @@ -0,0 +1,220 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:chopper/chopper.dart'; +import 'package:http/http.dart' show MultipartFile; + +part 'test_service_variable.chopper.dart'; + +const String service = 'https://localhost:4000'; + +@ChopperApi(baseUrl: service) +abstract class HttpTestServiceVariable extends ChopperService { + static HttpTestServiceVariable create([ChopperClient? client]) => + _$HttpTestServiceVariable(client); + + @Get(path: 'get/{id}') + Future> getTest( + @Path() String id, { + @Header('test') required String dynamicHeader, + }); + + @Head(path: 'head') + Future headTest(); + + @Options(path: 'options') + Future optionsTest(); + + @Get(path: 'get') + Future>>> getStreamTest(); + + @Get(path: '') + Future getAll(); + + @Get(path: '/') + Future getAllWithTrailingSlash(); + + @Get(path: 'query') + Future getQueryTest({ + @Query('name') String name = '', + @Query('int') int? number, + @Query('default_value') int? def = 42, + }); + + @Get(path: 'query_map') + Future getQueryMapTest(@QueryMap() Map query); + + @Get(path: 'query_map') + Future getQueryMapTest2( + @QueryMap() Map query, { + @Query('test') bool? test, + }); + + @Get(path: 'query_map') + Future getQueryMapTest3({ + @Query('name') String name = '', + @Query('number') int? number, + @QueryMap() Map filters = const {}, + }); + + @Get(path: 'query_map') + Future getQueryMapTest4({ + @Query('name') String name = '', + @Query('number') int? number, + @QueryMap() Map? filters, + }); + + @Get(path: 'query_map') + Future getQueryMapTest5({ + @QueryMap() Map? filters, + }); + + @Get(path: 'get_body') + Future getBody(@Body() dynamic body); + + @Post(path: 'post') + Future postTest(@Body() String data); + + @Post(path: 'post') + Future postStreamTest(@Body() Stream> byteStream); + + @Put(path: 'put/{id}') + Future putTest(@Path('id') String test, @Body() String data); + + @Delete(path: 'delete/{id}', headers: {'foo': 'bar'}) + Future deleteTest(@Path() String id); + + @Patch(path: 'patch/{id}') + Future patchTest(@Path() String id, @Body() String data); + + @Post(path: 'map') + Future mapTest(@Body() Map map); + + @FactoryConverter(request: convertForm) + @Post(path: 'form/body') + Future postForm(@Body() Map fields); + + @Post(path: 'form/body', headers: {contentTypeKey: formEncodedHeaders}) + Future postFormUsingHeaders(@Body() Map fields); + + @FactoryConverter(request: convertForm) + @Post(path: 'form/body/fields') + Future postFormFields(@Field() String foo, @Field() int bar); + + @Post(path: 'map/json') + @FactoryConverter( + request: customConvertRequest, + response: customConvertResponse, + ) + Future forceJsonTest(@Body() Map map); + + @Post(path: 'multi') + @multipart + Future postResources( + @Part('1') Map a, + @Part('2') Map b, + ); + + @Post(path: 'file') + @multipart + Future postFile( + @PartFile('file') List bytes, + ); + + @Post(path: 'image') + @multipart + Future postImage( + @PartFile('image') List imageData, + ); + + @Post(path: 'file') + @multipart + Future postMultipartFile( + @PartFile() MultipartFile file, { + @Part() String? id, + }); + + @Post(path: 'files') + @multipart + Future postListFiles(@PartFile() List files); + + @Post(path: 'multipart_list') + @multipart + Future postMultipartList({ + @Part('ints') required List ints, + @Part('doubles') required List doubles, + @Part('nums') required List nums, + @Part('strings') required List strings, + }); + + @Get(path: 'https://test.com') + Future fullUrl(); + + @Get(path: '/list/string') + Future>> listString(); + + @Post(path: 'no-body') + Future noBody(); + + @Get(path: '/query_param_include_null_query_vars', includeNullQueryVars: true) + Future> getUsingQueryParamIncludeNullQueryVars({ + @Query('foo') String? foo, + @Query('bar') String? bar, + @Query('baz') String? baz, + }); + + @Get(path: '/list_query_param') + Future> getUsingListQueryParam( + @Query('value') List value, + ); + + @Get(path: '/list_query_param_with_brackets', useBrackets: true) + Future> getUsingListQueryParamWithBrackets( + @Query('value') List value, + ); + + @Get(path: '/map_query_param') + Future> getUsingMapQueryParam( + @Query('value') Map value, + ); + + @Get( + path: '/map_query_param_include_null_query_vars', + includeNullQueryVars: true, + ) + Future> getUsingMapQueryParamIncludeNullQueryVars( + @Query('value') Map value, + ); + + @Get(path: '/map_query_param_with_brackets', useBrackets: true) + Future> getUsingMapQueryParamWithBrackets( + @Query('value') Map value, + ); +} + +Request customConvertRequest(Request req) { + final r = JsonConverter().convertRequest(req); + + return applyHeader(r, 'customConverter', 'true'); +} + +Response customConvertResponse(Response res) => + res.copyWith(body: json.decode(res.body)); + +Request convertForm(Request req) { + req = applyHeader(req, contentTypeKey, formEncodedHeaders); + + if (req.body is Map) { + final body = {}; + + req.body.forEach((key, val) { + if (val != null) { + body[key.toString()] = val.toString(); + } + }); + + req = req.copyWith(body: body); + } + + return req; +} From 31c3967c426b2692b4b09857c0480d417d4a2e86 Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Fri, 1 Sep 2023 07:06:33 +0100 Subject: [PATCH 098/171] :label: add topics (#495) --- chopper/pubspec.yaml | 6 ++++++ chopper_built_value/pubspec.yaml | 5 +++++ chopper_generator/pubspec.yaml | 6 ++++++ 3 files changed, 17 insertions(+) diff --git a/chopper/pubspec.yaml b/chopper/pubspec.yaml index 0ddea437..4db86721 100644 --- a/chopper/pubspec.yaml +++ b/chopper/pubspec.yaml @@ -30,3 +30,9 @@ dev_dependencies: dependency_overrides: chopper_generator: path: ../chopper_generator + +topics: + - api + - client + - http + - rest \ No newline at end of file diff --git a/chopper_built_value/pubspec.yaml b/chopper_built_value/pubspec.yaml index b23d0a66..19eb9227 100644 --- a/chopper_built_value/pubspec.yaml +++ b/chopper_built_value/pubspec.yaml @@ -23,3 +23,8 @@ dev_dependencies: dependency_overrides: chopper: path: ../chopper + +topics: + - codegen + - converter + - built_value \ No newline at end of file diff --git a/chopper_generator/pubspec.yaml b/chopper_generator/pubspec.yaml index 6e2d7d4c..a4691a32 100644 --- a/chopper_generator/pubspec.yaml +++ b/chopper_generator/pubspec.yaml @@ -28,3 +28,9 @@ dev_dependencies: dependency_overrides: chopper: path: ../chopper + +topics: + - api + - codegen + - http + - rest From a0f106d67fcf9448a0907f7dc60b47a5807d6720 Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Sat, 2 Sep 2023 08:12:45 +0100 Subject: [PATCH 099/171] :bug: fix chopper base.dart send() authenticate sending wrong request (#497) --- chopper/lib/src/base.dart | 7 ++----- chopper/lib/src/interceptor.dart | 19 ++++++++++++++++--- chopper/test/converter_test.dart | 19 +++++++++++++++++++ 3 files changed, 37 insertions(+), 8 deletions(-) diff --git a/chopper/lib/src/base.dart b/chopper/lib/src/base.dart index e791c13b..504590e2 100644 --- a/chopper/lib/src/base.dart +++ b/chopper/lib/src/base.dart @@ -309,11 +309,8 @@ base class ChopperClient { dynamic res = Response(response, response.body); if (authenticator != null) { - final Request? updatedRequest = await authenticator!.authenticate( - request, - res, - request, - ); + final Request? updatedRequest = + await authenticator!.authenticate(req, res, request); if (updatedRequest != null) { res = await send( diff --git a/chopper/lib/src/interceptor.dart b/chopper/lib/src/interceptor.dart index 1431fc23..0b11f53f 100644 --- a/chopper/lib/src/interceptor.dart +++ b/chopper/lib/src/interceptor.dart @@ -193,9 +193,12 @@ class JsonConverter implements Converter, ErrorConverter { Request encodeJson(Request request) { final String? contentType = request.headers[contentTypeKey]; - return (contentType?.contains(jsonHeaders) ?? false) - ? request.copyWith(body: json.encode(request.body)) - : request; + if ((contentType?.contains(jsonHeaders) ?? false) && + (request.body.runtimeType != String || !isJson(request.body))) { + return request.copyWith(body: json.encode(request.body)); + } + + return request; } FutureOr decodeJson(Response response) async { @@ -255,6 +258,16 @@ class JsonConverter implements Converter, ErrorConverter { static Request requestFactory(Request request) => const JsonConverter().convertRequest(request); + + @visibleForTesting + static bool isJson(dynamic data) { + try { + json.decode(data); + return true; + } catch (_) { + return false; + } + } } /// A [Converter] implementation that converts only [Request]s having a [Map] as their body. diff --git a/chopper/test/converter_test.dart b/chopper/test/converter_test.dart index d02d0a81..e1b44d33 100644 --- a/chopper/test/converter_test.dart +++ b/chopper/test/converter_test.dart @@ -111,6 +111,25 @@ void main() { expect(converted.body, equals({'foo': 'bar'})); }); + + test('JsonConverter.isJson', () { + expect(JsonConverter.isJson('{"foo":"bar"}'), isTrue); + expect(JsonConverter.isJson('foo'), isFalse); + expect(JsonConverter.isJson(''), isFalse); + expect(JsonConverter.isJson(null), isFalse); + expect(JsonConverter.isJson(42), isFalse); + expect(JsonConverter.isJson([]), isFalse); + expect(JsonConverter.isJson([1, 2, 3]), isFalse); + expect(JsonConverter.isJson(['a', 'b', 'c']), isFalse); + expect(JsonConverter.isJson({}), isFalse); + expect( + JsonConverter.isJson({ + 'foo': 'bar', + 'list': [1, 2, 3], + }), + isFalse, + ); + }); }); test('respects content-type headers', () { From f85eacd5dec9ff52ae07d710ed058c79faeeb6f3 Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Sat, 2 Sep 2023 13:00:07 +0100 Subject: [PATCH 100/171] :twisted_rightwards_arrows: post release branch sync (#499) --- .github/workflows/publish.yml | 13 +++++++++++++ .github/workflows/publish_dry_run.yml | 13 +++++++++++++ chopper/CHANGELOG.md | 6 ++++++ chopper/pubspec.yaml | 2 +- chopper_built_value/CHANGELOG.md | 8 ++++++++ chopper_built_value/pubspec.yaml | 4 ++-- chopper_generator/CHANGELOG.md | 5 +++++ chopper_generator/pubspec.yaml | 2 +- 8 files changed, 49 insertions(+), 4 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index a9da77ad..32ee9bc4 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -68,6 +68,19 @@ jobs: dart pub get echo "IS_VERSION_GREATER=$(dart run compare_versions.dart $THIS_VERSION $BASE_VERSION_${{ matrix.package }})" >> $GITHUB_ENV popd || exit + - name: Validate pub.dev topics + id: validate_pub_dev_topics + run: | + set -e + pushd ${{ matrix.package }} || exit + pattern="^[a-z][a-z0-9-]*[a-z0-9]$" + for topic in $(yq -r '.topics[]' pubspec.yaml); do + if [[ ! $topic =~ $pattern ]]; then + echo "Invalid topic: $topic" + exit 1 + fi + done + popd || exit - name: Set up pub credentials id: credentials if: ${{ env.IS_VERSION_GREATER == 1 }} diff --git a/.github/workflows/publish_dry_run.yml b/.github/workflows/publish_dry_run.yml index 6ab66c84..6095578e 100644 --- a/.github/workflows/publish_dry_run.yml +++ b/.github/workflows/publish_dry_run.yml @@ -67,6 +67,19 @@ jobs: dart pub get echo "IS_VERSION_GREATER=$(dart run compare_versions.dart $THIS_VERSION $BASE_VERSION_${{ matrix.package }})" >> $GITHUB_ENV popd || exit + - name: Validate pub.dev topics + id: validate_pub_dev_topics + run: | + set -e + pushd ${{ matrix.package }} || exit + pattern="^[a-z][a-z0-9-]*[a-z0-9]$" + for topic in $(yq -r '.topics[]' pubspec.yaml); do + if [[ ! $topic =~ $pattern ]]; then + echo "Invalid topic: $topic" + exit 1 + fi + done + popd || exit - name: Publish (dry run) id: publish_dry_run if: ${{ env.IS_VERSION_GREATER == 1 }} diff --git a/chopper/CHANGELOG.md b/chopper/CHANGELOG.md index d820ae48..a2753721 100644 --- a/chopper/CHANGELOG.md +++ b/chopper/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 7.0.6 + +- The @ChopperApi annotation's baseUrl property can be used as a top level constant string variable ([#493](https://github.com/lejard-h/chopper/pull/493)) +- Fix ChopperClient.send() sending wrong request when using an Authenticator ([#497](https://github.com/lejard-h/chopper/pull/497)) +- Add pub.dev topics to package metadata ([#495](https://github.com/lejard-h/chopper/pull/495)) + ## 7.0.5 - Fix documentation links in README ([#488](https://github.com/lejard-h/chopper/pull/488)) diff --git a/chopper/pubspec.yaml b/chopper/pubspec.yaml index 4db86721..6ec0d139 100644 --- a/chopper/pubspec.yaml +++ b/chopper/pubspec.yaml @@ -1,6 +1,6 @@ name: chopper description: Chopper is an http client generator using source_gen, inspired by Retrofit -version: 7.0.5 +version: 7.0.6 documentation: https://hadrien-lejard.gitbook.io/chopper repository: https://github.com/lejard-h/chopper diff --git a/chopper_built_value/CHANGELOG.md b/chopper_built_value/CHANGELOG.md index 7e536f4d..16e4dacf 100644 --- a/chopper_built_value/CHANGELOG.md +++ b/chopper_built_value/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## 2.0.1+1 + +- Fix pub.dev topic in package metadata ([#498](https://github.com/lejard-h/chopper/pull/498)) + +## 2.0.1 + +- Add pub.dev topics to package metadata ([#495](https://github.com/lejard-h/chopper/pull/495)) + ## 2.0.0 - Require Dart 3.0 or later diff --git a/chopper_built_value/pubspec.yaml b/chopper_built_value/pubspec.yaml index 19eb9227..1b15b2b0 100644 --- a/chopper_built_value/pubspec.yaml +++ b/chopper_built_value/pubspec.yaml @@ -1,6 +1,6 @@ name: chopper_built_value description: A built_value based Converter for Chopper. -version: 2.0.0 +version: 2.0.1+1 documentation: https://hadrien-lejard.gitbook.io/chopper/converters/built-value-converter repository: https://github.com/lejard-h/chopper @@ -27,4 +27,4 @@ dependency_overrides: topics: - codegen - converter - - built_value \ No newline at end of file + - built-value \ No newline at end of file diff --git a/chopper_generator/CHANGELOG.md b/chopper_generator/CHANGELOG.md index 55018b6f..8697a281 100644 --- a/chopper_generator/CHANGELOG.md +++ b/chopper_generator/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## 7.0.3 + +- The @ChopperApi annotation's baseUrl property can be used as a top level constant string variable ([#493](https://github.com/lejard-h/chopper/pull/493)) +- Add pub.dev topics to package metadata ([#495](https://github.com/lejard-h/chopper/pull/495)) + ## 7.0.2 - Update analyzer dependency to >=5.13.0 <7.0.0 ([#484](https://github.com/lejard-h/chopper/pull/484)) diff --git a/chopper_generator/pubspec.yaml b/chopper_generator/pubspec.yaml index a4691a32..4a10721f 100644 --- a/chopper_generator/pubspec.yaml +++ b/chopper_generator/pubspec.yaml @@ -1,6 +1,6 @@ name: chopper_generator description: Chopper is an http client generator using source_gen, inspired by Retrofit -version: 7.0.2 +version: 7.0.3 documentation: https://hadrien-lejard.gitbook.io/chopper repository: https://github.com/lejard-h/chopper From be71064fd715187f8319b6612bf5b3728c2b92b2 Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Tue, 5 Sep 2023 09:09:11 +0100 Subject: [PATCH 101/171] :rotating_light: ignore unnecessary_string_interpolations (#501) --- chopper/example/definition.chopper.dart | 2 +- chopper/test/test_service.chopper.dart | 2 +- chopper/test/test_service_variable.chopper.dart | 2 +- chopper_generator/lib/src/generator.dart | 1 + chopper_generator/test/test_service.chopper.dart | 2 +- chopper_generator/test/test_service_variable.chopper.dart | 2 +- 6 files changed, 6 insertions(+), 5 deletions(-) diff --git a/chopper/example/definition.chopper.dart b/chopper/example/definition.chopper.dart index a7fdf51a..a2684115 100644 --- a/chopper/example/definition.chopper.dart +++ b/chopper/example/definition.chopper.dart @@ -6,7 +6,7 @@ part of 'definition.dart'; // ChopperGenerator // ************************************************************************** -// ignore_for_file: always_put_control_body_on_new_line, always_specify_types, prefer_const_declarations, unnecessary_brace_in_string_interps +// ignore_for_file: always_put_control_body_on_new_line, always_specify_types, prefer_const_declarations, unnecessary_string_interpolations, unnecessary_brace_in_string_interps final class _$MyService extends MyService { _$MyService([ChopperClient? client]) { if (client == null) return; diff --git a/chopper/test/test_service.chopper.dart b/chopper/test/test_service.chopper.dart index d0379988..4df2dd03 100644 --- a/chopper/test/test_service.chopper.dart +++ b/chopper/test/test_service.chopper.dart @@ -6,7 +6,7 @@ part of 'test_service.dart'; // ChopperGenerator // ************************************************************************** -// ignore_for_file: always_put_control_body_on_new_line, always_specify_types, prefer_const_declarations, unnecessary_brace_in_string_interps +// ignore_for_file: always_put_control_body_on_new_line, always_specify_types, prefer_const_declarations, unnecessary_string_interpolations, unnecessary_brace_in_string_interps final class _$HttpTestService extends HttpTestService { _$HttpTestService([ChopperClient? client]) { if (client == null) return; diff --git a/chopper/test/test_service_variable.chopper.dart b/chopper/test/test_service_variable.chopper.dart index c4423e71..038868a7 100644 --- a/chopper/test/test_service_variable.chopper.dart +++ b/chopper/test/test_service_variable.chopper.dart @@ -6,7 +6,7 @@ part of 'test_service_variable.dart'; // ChopperGenerator // ************************************************************************** -// ignore_for_file: always_put_control_body_on_new_line, always_specify_types, prefer_const_declarations, unnecessary_brace_in_string_interps +// ignore_for_file: always_put_control_body_on_new_line, always_specify_types, prefer_const_declarations, unnecessary_string_interpolations, unnecessary_brace_in_string_interps final class _$HttpTestServiceVariable extends HttpTestServiceVariable { _$HttpTestServiceVariable([ChopperClient? client]) { if (client == null) return; diff --git a/chopper_generator/lib/src/generator.dart b/chopper_generator/lib/src/generator.dart index 81518109..e194f52c 100644 --- a/chopper_generator/lib/src/generator.dart +++ b/chopper_generator/lib/src/generator.dart @@ -97,6 +97,7 @@ final class ChopperGenerator 'always_put_control_body_on_new_line, ' 'always_specify_types, ' 'prefer_const_declarations, ' + 'unnecessary_string_interpolations, ' 'unnecessary_brace_in_string_interps'; final DartEmitter emitter = DartEmitter(); diff --git a/chopper_generator/test/test_service.chopper.dart b/chopper_generator/test/test_service.chopper.dart index d0379988..4df2dd03 100644 --- a/chopper_generator/test/test_service.chopper.dart +++ b/chopper_generator/test/test_service.chopper.dart @@ -6,7 +6,7 @@ part of 'test_service.dart'; // ChopperGenerator // ************************************************************************** -// ignore_for_file: always_put_control_body_on_new_line, always_specify_types, prefer_const_declarations, unnecessary_brace_in_string_interps +// ignore_for_file: always_put_control_body_on_new_line, always_specify_types, prefer_const_declarations, unnecessary_string_interpolations, unnecessary_brace_in_string_interps final class _$HttpTestService extends HttpTestService { _$HttpTestService([ChopperClient? client]) { if (client == null) return; diff --git a/chopper_generator/test/test_service_variable.chopper.dart b/chopper_generator/test/test_service_variable.chopper.dart index c4423e71..038868a7 100644 --- a/chopper_generator/test/test_service_variable.chopper.dart +++ b/chopper_generator/test/test_service_variable.chopper.dart @@ -6,7 +6,7 @@ part of 'test_service_variable.dart'; // ChopperGenerator // ************************************************************************** -// ignore_for_file: always_put_control_body_on_new_line, always_specify_types, prefer_const_declarations, unnecessary_brace_in_string_interps +// ignore_for_file: always_put_control_body_on_new_line, always_specify_types, prefer_const_declarations, unnecessary_string_interpolations, unnecessary_brace_in_string_interps final class _$HttpTestServiceVariable extends HttpTestServiceVariable { _$HttpTestServiceVariable([ChopperClient? client]) { if (client == null) return; From 3ff8359a67efa9e820b6325bec769bd87b78b0ee Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Fri, 8 Sep 2023 11:43:53 +0100 Subject: [PATCH 102/171] :twisted_rightwards_arrows: post release branch sync (#503) --- chopper_generator/CHANGELOG.md | 4 ++++ chopper_generator/pubspec.yaml | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/chopper_generator/CHANGELOG.md b/chopper_generator/CHANGELOG.md index 8697a281..0b7c9f39 100644 --- a/chopper_generator/CHANGELOG.md +++ b/chopper_generator/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 7.0.4 + +- Ignore unnecessary_string_interpolations ([#501](https://github.com/lejard-h/chopper/pull/501)) + ## 7.0.3 - The @ChopperApi annotation's baseUrl property can be used as a top level constant string variable ([#493](https://github.com/lejard-h/chopper/pull/493)) diff --git a/chopper_generator/pubspec.yaml b/chopper_generator/pubspec.yaml index 4a10721f..5cfb749d 100644 --- a/chopper_generator/pubspec.yaml +++ b/chopper_generator/pubspec.yaml @@ -1,6 +1,6 @@ name: chopper_generator description: Chopper is an http client generator using source_gen, inspired by Retrofit -version: 7.0.3 +version: 7.0.4 documentation: https://hadrien-lejard.gitbook.io/chopper repository: https://github.com/lejard-h/chopper From 14bc48f093ce992e7dbe6ace67819777aec7b707 Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Sat, 7 Oct 2023 07:57:38 +0100 Subject: [PATCH 103/171] :rotating_light: correct static analysis suppression of *.chopper.dart files (#507) --- chopper/analysis_options.yaml | 1 - chopper/example/definition.chopper.dart | 2 +- chopper/test/test_service.chopper.dart | 2 +- chopper/test/test_service_variable.chopper.dart | 2 +- chopper_built_value/analysis_options.yaml | 1 - chopper_generator/Makefile | 4 ++++ chopper_generator/analysis_options.yaml | 1 - chopper_generator/lib/src/generator.dart | 7 +------ chopper_generator/test/test_service.chopper.dart | 2 +- chopper_generator/test/test_service_variable.chopper.dart | 2 +- example/analysis_options.yaml | 1 - example/lib/built_value_resource.chopper.dart | 2 +- example/lib/json_serializable.chopper.dart | 2 +- 13 files changed, 12 insertions(+), 17 deletions(-) diff --git a/chopper/analysis_options.yaml b/chopper/analysis_options.yaml index 6f56a451..4fd79467 100644 --- a/chopper/analysis_options.yaml +++ b/chopper/analysis_options.yaml @@ -3,7 +3,6 @@ include: package:lints/recommended.yaml analyzer: exclude: - "**.g.dart" - - "**.chopper.dart" - "**.mocks.dart" - "example/**" diff --git a/chopper/example/definition.chopper.dart b/chopper/example/definition.chopper.dart index a2684115..577547fe 100644 --- a/chopper/example/definition.chopper.dart +++ b/chopper/example/definition.chopper.dart @@ -6,7 +6,7 @@ part of 'definition.dart'; // ChopperGenerator // ************************************************************************** -// ignore_for_file: always_put_control_body_on_new_line, always_specify_types, prefer_const_declarations, unnecessary_string_interpolations, unnecessary_brace_in_string_interps +// ignore_for_file: type=lint final class _$MyService extends MyService { _$MyService([ChopperClient? client]) { if (client == null) return; diff --git a/chopper/test/test_service.chopper.dart b/chopper/test/test_service.chopper.dart index 4df2dd03..f070c8a8 100644 --- a/chopper/test/test_service.chopper.dart +++ b/chopper/test/test_service.chopper.dart @@ -6,7 +6,7 @@ part of 'test_service.dart'; // ChopperGenerator // ************************************************************************** -// ignore_for_file: always_put_control_body_on_new_line, always_specify_types, prefer_const_declarations, unnecessary_string_interpolations, unnecessary_brace_in_string_interps +// ignore_for_file: type=lint final class _$HttpTestService extends HttpTestService { _$HttpTestService([ChopperClient? client]) { if (client == null) return; diff --git a/chopper/test/test_service_variable.chopper.dart b/chopper/test/test_service_variable.chopper.dart index 038868a7..1f709494 100644 --- a/chopper/test/test_service_variable.chopper.dart +++ b/chopper/test/test_service_variable.chopper.dart @@ -6,7 +6,7 @@ part of 'test_service_variable.dart'; // ChopperGenerator // ************************************************************************** -// ignore_for_file: always_put_control_body_on_new_line, always_specify_types, prefer_const_declarations, unnecessary_string_interpolations, unnecessary_brace_in_string_interps +// ignore_for_file: type=lint final class _$HttpTestServiceVariable extends HttpTestServiceVariable { _$HttpTestServiceVariable([ChopperClient? client]) { if (client == null) return; diff --git a/chopper_built_value/analysis_options.yaml b/chopper_built_value/analysis_options.yaml index 6f56a451..4fd79467 100644 --- a/chopper_built_value/analysis_options.yaml +++ b/chopper_built_value/analysis_options.yaml @@ -3,7 +3,6 @@ include: package:lints/recommended.yaml analyzer: exclude: - "**.g.dart" - - "**.chopper.dart" - "**.mocks.dart" - "example/**" diff --git a/chopper_generator/Makefile b/chopper_generator/Makefile index 444451d2..7552c051 100644 --- a/chopper_generator/Makefile +++ b/chopper_generator/Makefile @@ -41,6 +41,10 @@ install: @# Help: Install all the project's packages dart pub get +sure: + @# Help: Analyze the project's Dart code, check the formatting one or more Dart files and run unit tests for the current project. + make check_style && make tests + tests: @# Help: Run Dart unit and widget tests for the current project. dart test diff --git a/chopper_generator/analysis_options.yaml b/chopper_generator/analysis_options.yaml index 6f56a451..4fd79467 100644 --- a/chopper_generator/analysis_options.yaml +++ b/chopper_generator/analysis_options.yaml @@ -3,7 +3,6 @@ include: package:lints/recommended.yaml analyzer: exclude: - "**.g.dart" - - "**.chopper.dart" - "**.mocks.dart" - "example/**" diff --git a/chopper_generator/lib/src/generator.dart b/chopper_generator/lib/src/generator.dart index e194f52c..3a6b36ef 100644 --- a/chopper_generator/lib/src/generator.dart +++ b/chopper_generator/lib/src/generator.dart @@ -93,12 +93,7 @@ final class ChopperGenerator )); }); - const String ignore = '// ignore_for_file: ' - 'always_put_control_body_on_new_line, ' - 'always_specify_types, ' - 'prefer_const_declarations, ' - 'unnecessary_string_interpolations, ' - 'unnecessary_brace_in_string_interps'; + const String ignore = '// ignore_for_file: type=lint'; final DartEmitter emitter = DartEmitter(); return DartFormatter().format('$ignore\n${classBuilder.accept(emitter)}'); diff --git a/chopper_generator/test/test_service.chopper.dart b/chopper_generator/test/test_service.chopper.dart index 4df2dd03..f070c8a8 100644 --- a/chopper_generator/test/test_service.chopper.dart +++ b/chopper_generator/test/test_service.chopper.dart @@ -6,7 +6,7 @@ part of 'test_service.dart'; // ChopperGenerator // ************************************************************************** -// ignore_for_file: always_put_control_body_on_new_line, always_specify_types, prefer_const_declarations, unnecessary_string_interpolations, unnecessary_brace_in_string_interps +// ignore_for_file: type=lint final class _$HttpTestService extends HttpTestService { _$HttpTestService([ChopperClient? client]) { if (client == null) return; diff --git a/chopper_generator/test/test_service_variable.chopper.dart b/chopper_generator/test/test_service_variable.chopper.dart index 038868a7..1f709494 100644 --- a/chopper_generator/test/test_service_variable.chopper.dart +++ b/chopper_generator/test/test_service_variable.chopper.dart @@ -6,7 +6,7 @@ part of 'test_service_variable.dart'; // ChopperGenerator // ************************************************************************** -// ignore_for_file: always_put_control_body_on_new_line, always_specify_types, prefer_const_declarations, unnecessary_string_interpolations, unnecessary_brace_in_string_interps +// ignore_for_file: type=lint final class _$HttpTestServiceVariable extends HttpTestServiceVariable { _$HttpTestServiceVariable([ChopperClient? client]) { if (client == null) return; diff --git a/example/analysis_options.yaml b/example/analysis_options.yaml index 8ed20e77..1e603f80 100644 --- a/example/analysis_options.yaml +++ b/example/analysis_options.yaml @@ -3,7 +3,6 @@ include: package:lints/recommended.yaml analyzer: exclude: - "**.g.dart" - - "**.chopper.dart" - "**.mocks.dart" linter: diff --git a/example/lib/built_value_resource.chopper.dart b/example/lib/built_value_resource.chopper.dart index af0b742d..32264219 100644 --- a/example/lib/built_value_resource.chopper.dart +++ b/example/lib/built_value_resource.chopper.dart @@ -6,7 +6,7 @@ part of 'built_value_resource.dart'; // ChopperGenerator // ************************************************************************** -// ignore_for_file: always_put_control_body_on_new_line, always_specify_types, prefer_const_declarations, unnecessary_brace_in_string_interps +// ignore_for_file: type=lint final class _$MyService extends MyService { _$MyService([ChopperClient? client]) { if (client == null) return; diff --git a/example/lib/json_serializable.chopper.dart b/example/lib/json_serializable.chopper.dart index 1aa9352e..e6164985 100644 --- a/example/lib/json_serializable.chopper.dart +++ b/example/lib/json_serializable.chopper.dart @@ -6,7 +6,7 @@ part of 'json_serializable.dart'; // ChopperGenerator // ************************************************************************** -// ignore_for_file: always_put_control_body_on_new_line, always_specify_types, prefer_const_declarations, unnecessary_brace_in_string_interps +// ignore_for_file: type=lint final class _$MyService extends MyService { _$MyService([ChopperClient? client]) { if (client == null) return; From b48066df44e69a52f5ff12e7514d19d3fb52b188 Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Sat, 7 Oct 2023 08:40:18 +0100 Subject: [PATCH 104/171] :bug: remove charset from http request headers when the body is in bytes (#508) --- chopper/lib/src/request.dart | 19 +++++++++++++------ chopper/test/base_test.dart | 20 ++++++++++++++++++++ 2 files changed, 33 insertions(+), 6 deletions(-) diff --git a/chopper/lib/src/request.dart b/chopper/lib/src/request.dart index 1441ef3d..0f134f7d 100644 --- a/chopper/lib/src/request.dart +++ b/chopper/lib/src/request.dart @@ -138,16 +138,23 @@ base class Request extends http.BaseRequest with EquatableMixin { @visibleForTesting http.Request toHttpRequest() { final http.Request request = http.Request(method, url) - ..followRedirects = followRedirects - ..headers.addAll(headers); + ..followRedirects = followRedirects; - if (body != null) { + if (body == null) { + request.headers.addAll(headers); + } else { if (body is String) { - request.body = body; + request + ..headers.addAll(headers) + ..body = body; } else if (body is List) { - request.bodyBytes = body; + request + ..bodyBytes = body + ..headers.addAll(headers); } else if (body is Map) { - request.bodyFields = body; + request + ..headers.addAll(headers) + ..bodyFields = body; } else { throw ArgumentError.value('$body', 'body'); } diff --git a/chopper/test/base_test.dart b/chopper/test/base_test.dart index dee04012..f1e76dad 100644 --- a/chopper/test/base_test.dart +++ b/chopper/test/base_test.dart @@ -7,6 +7,7 @@ import 'package:chopper/chopper.dart'; import 'package:http/http.dart' as http; import 'package:http/testing.dart'; import 'package:test/test.dart'; +import 'package:transparent_image/transparent_image.dart'; import 'test_service.dart'; import 'test_service_variable.dart'; @@ -763,6 +764,25 @@ void main() { expect(request.bodyBytes, equals([1, 2, 3])); }); + test('BodyBytes does not have charset header', () { + final request = Request( + HttpMethod.Post, + Uri.parse('https://foo/'), + Uri.parse(''), + headers: { + 'authorization': 'Bearer fooBarBaz', + 'x-foo': 'bar', + }, + body: kTransparentImage, + ).toHttpRequest(); + + expect(request.headers['authorization'], equals('Bearer fooBarBaz')); + expect(request.headers['x-foo'], equals('bar')); + expect(request.headers['content-type'], isNull); + expect(request.headers['content-type'], isNot(contains('charset='))); + expect(request.bodyBytes, equals(kTransparentImage)); + }); + test('BodyFields', () { final request = Request( HttpMethod.Post, From cbb4a92953042117a83229f8724f89a0c585f3ba Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Sat, 7 Oct 2023 09:09:30 +0100 Subject: [PATCH 105/171] :construction_worker: update mono_repo (#509) --- .github/workflows/dart.yml | 10 +++++----- tool/ci.sh | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/dart.yml b/.github/workflows/dart.yml index 0a20e47e..e74815fe 100644 --- a/.github/workflows/dart.yml +++ b/.github/workflows/dart.yml @@ -1,4 +1,4 @@ -# Created with package:mono_repo v6.5.7 +# Created with package:mono_repo v6.6.0 name: Dart CI on: push: @@ -37,7 +37,7 @@ jobs: name: Checkout repository uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab - name: mono_repo self validate - run: dart pub global activate mono_repo 6.5.7 + run: dart pub global activate mono_repo 6.6.0 - name: mono_repo self validate run: dart pub global run mono_repo generate --validate job_002: @@ -133,7 +133,7 @@ jobs: if: "always() && steps.chopper_pub_upgrade.conclusion == 'success'" working-directory: chopper - name: Upload coverage to codecov.io - uses: codecov/codecov-action@main + uses: codecov/codecov-action@v3 with: files: chopper/coverage/lcov.info fail_ci_if_error: true @@ -148,7 +148,7 @@ jobs: if: "always() && steps.chopper_built_value_pub_upgrade.conclusion == 'success'" working-directory: chopper_built_value - name: Upload coverage to codecov.io - uses: codecov/codecov-action@main + uses: codecov/codecov-action@v3 with: files: chopper_built_value/coverage/lcov.info fail_ci_if_error: true @@ -163,7 +163,7 @@ jobs: if: "always() && steps.chopper_generator_pub_upgrade.conclusion == 'success'" working-directory: chopper_generator - name: Upload coverage to codecov.io - uses: codecov/codecov-action@main + uses: codecov/codecov-action@v3 with: files: chopper_generator/coverage/lcov.info fail_ci_if_error: true diff --git a/tool/ci.sh b/tool/ci.sh index 9c1ac4c1..0af37047 100755 --- a/tool/ci.sh +++ b/tool/ci.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Created with package:mono_repo v6.5.7 +# Created with package:mono_repo v6.6.0 # Support built in commands on windows out of the box. # When it is a flutter repo (check the pubspec.yaml for "sdk: flutter") From ce0836d34630a9b42788cc483fd1770e3310a554 Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Sat, 7 Oct 2023 09:54:58 +0100 Subject: [PATCH 106/171] :construction_worker: create Github release when publishing package (#510) --- .github/workflows/publish.yml | 34 ++++++++++++++++++++------- .github/workflows/publish_dry_run.yml | 30 ++++++++++++++++------- 2 files changed, 48 insertions(+), 16 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 32ee9bc4..6a17c681 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -33,9 +33,10 @@ jobs: - run: git checkout HEAD^ - name: Load base version id: load_base_version + working-directory: ${{ matrix.package }} run: | set -e - echo "BASE_VERSION_${{ matrix.package }}=$(awk '/^version: / {print $2}' ${{ matrix.package }}/pubspec.yaml)" >> $GITHUB_OUTPUT + echo "BASE_VERSION_${{ matrix.package }}=$(yq -r '.version' pubspec.yaml)" >> $GITHUB_OUTPUT publish: name: "Publish" needs: get_base_version @@ -53,26 +54,26 @@ jobs: uses: actions/checkout@v3 - name: Load this version id: load_this_version + working-directory: ${{ matrix.package }} run: | set -e - echo "THIS_VERSION=$(awk '/^version: / {print $2}' ${{ matrix.package }}/pubspec.yaml)" >> $GITHUB_ENV + echo "THIS_VERSION=$(yq -r '.version' pubspec.yaml)" >> $GITHUB_ENV - name: Compare versions id: compare_versions env: BASE_VERSION_chopper: ${{ needs.get_base_version.outputs.BASE_VERSION_chopper }} BASE_VERSION_chopper_generator: ${{ needs.get_base_version.outputs.BASE_VERSION_chopper_generator }} BASE_VERSION_chopper_built_value: ${{ needs.get_base_version.outputs.BASE_VERSION_chopper_built_value }} + working-directory: tool run: | set -e - pushd tool || exit dart pub get echo "IS_VERSION_GREATER=$(dart run compare_versions.dart $THIS_VERSION $BASE_VERSION_${{ matrix.package }})" >> $GITHUB_ENV - popd || exit - name: Validate pub.dev topics id: validate_pub_dev_topics + working-directory: ${{ matrix.package }} run: | set -e - pushd ${{ matrix.package }} || exit pattern="^[a-z][a-z0-9-]*[a-z0-9]$" for topic in $(yq -r '.topics[]' pubspec.yaml); do if [[ ! $topic =~ $pattern ]]; then @@ -80,7 +81,16 @@ jobs: exit 1 fi done - popd || exit + - name: Create release-specific CHANGELOG + id: create_changelog + if: ${{ env.IS_VERSION_GREATER == 1 }} + working-directory: ${{ matrix.package }} + run: | + set -e + CHANGELOG_PATH=$RUNNER_TEMP/CHANGELOG.md + awk '/^##[[:space:]].*/ { if (count == 1) exit; count++; print } count == 1 && !/^##[[:space:]].*/ { print }' CHANGELOG.md | sed -e :a -e '/^\n*$/{$d;N;ba' -e '}' > $CHANGELOG_PATH + echo -en "\n[https://pub.dev/packages/${{ matrix.package }}/versions/$THIS_VERSION](https://pub.dev/packages/${{ matrix.package }}/versions/$THIS_VERSION)" >> $CHANGELOG_PATH + echo "CHANGELOG_PATH=$CHANGELOG_PATH" >> $GITHUB_ENV - name: Set up pub credentials id: credentials if: ${{ env.IS_VERSION_GREATER == 1 }} @@ -91,12 +101,19 @@ jobs: - name: Publish id: publish if: ${{ env.IS_VERSION_GREATER == 1 }} + working-directory: ${{ matrix.package }} run: | set -e - pushd ${{ matrix.package }} || exit yq -i 'del(.dependency_overrides)' pubspec.yaml dart pub publish --force - popd || exit + - name: Github release + id: github_release + if: ${{ env.IS_VERSION_GREATER == 1 }} + uses: softprops/action-gh-release@v1 + with: + name: ${{ format('{0}-v{1}', matrix.package, env.THIS_VERSION) }} + tag_name: ${{ format('{0}-v{1}', matrix.package, env.THIS_VERSION) }} + body_path: ${{ env.CHANGELOG_PATH }} - name: Skip publish id: skip_publish if: ${{ env.IS_VERSION_GREATER == 0 }} @@ -106,3 +123,4 @@ jobs: if: ${{ always() }} run: | rm -rf $XDG_CONFIG_HOME/dart/pub-credentials.json + rm -rf $CHANGELOG_PATH diff --git a/.github/workflows/publish_dry_run.yml b/.github/workflows/publish_dry_run.yml index 6095578e..bbd60cc2 100644 --- a/.github/workflows/publish_dry_run.yml +++ b/.github/workflows/publish_dry_run.yml @@ -32,9 +32,10 @@ jobs: ref: ${{ github.event.pull_request.base.ref }} - name: Load base version id: load_base_version + working-directory: ${{ matrix.package }} run: | set -e - echo "BASE_VERSION_${{ matrix.package }}=$(awk '/^version: / {print $2}' ${{ matrix.package }}/pubspec.yaml)" >> $GITHUB_OUTPUT + echo "BASE_VERSION_${{ matrix.package }}=$(yq -r '.version' pubspec.yaml)" >> $GITHUB_OUTPUT publish_dry_run: name: "Publish DRY RUN" needs: get_base_version @@ -52,26 +53,26 @@ jobs: uses: actions/checkout@v3 - name: Load this version id: load_this_version + working-directory: ${{ matrix.package }} run: | set -e - echo "THIS_VERSION=$(awk '/^version: / {print $2}' ${{ matrix.package }}/pubspec.yaml)" >> $GITHUB_ENV + echo "THIS_VERSION=$(yq -r '.version' pubspec.yaml)" >> $GITHUB_ENV - name: Compare versions id: compare_versions env: BASE_VERSION_chopper: ${{ needs.get_base_version.outputs.BASE_VERSION_chopper }} BASE_VERSION_chopper_generator: ${{ needs.get_base_version.outputs.BASE_VERSION_chopper_generator }} BASE_VERSION_chopper_built_value: ${{ needs.get_base_version.outputs.BASE_VERSION_chopper_built_value }} + working-directory: tool run: | set -e - pushd tool || exit dart pub get echo "IS_VERSION_GREATER=$(dart run compare_versions.dart $THIS_VERSION $BASE_VERSION_${{ matrix.package }})" >> $GITHUB_ENV - popd || exit - name: Validate pub.dev topics id: validate_pub_dev_topics + working-directory: ${{ matrix.package }} run: | set -e - pushd ${{ matrix.package }} || exit pattern="^[a-z][a-z0-9-]*[a-z0-9]$" for topic in $(yq -r '.topics[]' pubspec.yaml); do if [[ ! $topic =~ $pattern ]]; then @@ -79,17 +80,30 @@ jobs: exit 1 fi done - popd || exit + - name: Create release-specific CHANGELOG + id: create_changelog + if: ${{ env.IS_VERSION_GREATER == 1 }} + working-directory: ${{ matrix.package }} + run: | + set -e + CHANGELOG_PATH=$RUNNER_TEMP/CHANGELOG.md + awk '/^##[[:space:]].*/ { if (count == 1) exit; count++; print } count == 1 && !/^##[[:space:]].*/ { print }' CHANGELOG.md | sed -e :a -e '/^\n*$/{$d;N;ba' -e '}' > $CHANGELOG_PATH + echo -en "\n[https://pub.dev/packages/${{ matrix.package }}/versions/$THIS_VERSION](https://pub.dev/packages/${{ matrix.package }}/versions/$THIS_VERSION)" >> $CHANGELOG_PATH + echo "CHANGELOG_PATH=$CHANGELOG_PATH" >> $GITHUB_ENV - name: Publish (dry run) id: publish_dry_run if: ${{ env.IS_VERSION_GREATER == 1 }} + working-directory: ${{ matrix.package }} run: | set -e - pushd ${{ matrix.package }} || exit yq -i 'del(.dependency_overrides)' pubspec.yaml dart pub publish --dry-run - popd || exit - name: Skip publish (dry run) id: skip_publish_dry_run if: ${{ env.IS_VERSION_GREATER == 0 }} run: echo "Skipping publish (dry run) for ${{ matrix.package }} because the version is not greater than the one on pub.dev" + - name: Cleanup + id: cleanup + if: ${{ always() }} + run: | + rm -rf $CHANGELOG_PATH From fedb899ff5d791c27af74c4a5b5c3da8dfcfdf03 Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Sat, 7 Oct 2023 10:45:21 +0100 Subject: [PATCH 107/171] :twisted_rightwards_arrows: post release branch sync (#514) --- .github/workflows/publish.yml | 2 ++ chopper/CHANGELOG.md | 8 ++++++++ chopper/pubspec.yaml | 2 +- chopper_built_value/CHANGELOG.md | 4 ++++ chopper_built_value/pubspec.yaml | 2 +- chopper_generator/CHANGELOG.md | 8 ++++++++ chopper_generator/pubspec.yaml | 2 +- 7 files changed, 25 insertions(+), 3 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 6a17c681..8d6278d2 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -41,6 +41,8 @@ jobs: name: "Publish" needs: get_base_version runs-on: ubuntu-latest + permissions: + contents: write strategy: matrix: package: [ chopper, chopper_generator, chopper_built_value ] diff --git a/chopper/CHANGELOG.md b/chopper/CHANGELOG.md index a2753721..b46c66c5 100644 --- a/chopper/CHANGELOG.md +++ b/chopper/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## 7.0.7+1 + +- Fix Github release workflow permissions ([#512](https://github.com/lejard-h/chopper/pull/512)) + +## 7.0.7 + +- Remove charset from http request headers when the body is in bytes ([#508](https://github.com/lejard-h/chopper/pull/508)) + ## 7.0.6 - The @ChopperApi annotation's baseUrl property can be used as a top level constant string variable ([#493](https://github.com/lejard-h/chopper/pull/493)) diff --git a/chopper/pubspec.yaml b/chopper/pubspec.yaml index 6ec0d139..c339bc4c 100644 --- a/chopper/pubspec.yaml +++ b/chopper/pubspec.yaml @@ -1,6 +1,6 @@ name: chopper description: Chopper is an http client generator using source_gen, inspired by Retrofit -version: 7.0.6 +version: 7.0.7+1 documentation: https://hadrien-lejard.gitbook.io/chopper repository: https://github.com/lejard-h/chopper diff --git a/chopper_built_value/CHANGELOG.md b/chopper_built_value/CHANGELOG.md index 16e4dacf..e32d5782 100644 --- a/chopper_built_value/CHANGELOG.md +++ b/chopper_built_value/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 2.0.1+2 + +- Fix Github release workflow permissions ([#512](https://github.com/lejard-h/chopper/pull/512)) + ## 2.0.1+1 - Fix pub.dev topic in package metadata ([#498](https://github.com/lejard-h/chopper/pull/498)) diff --git a/chopper_built_value/pubspec.yaml b/chopper_built_value/pubspec.yaml index 1b15b2b0..ab34497d 100644 --- a/chopper_built_value/pubspec.yaml +++ b/chopper_built_value/pubspec.yaml @@ -1,6 +1,6 @@ name: chopper_built_value description: A built_value based Converter for Chopper. -version: 2.0.1+1 +version: 2.0.1+2 documentation: https://hadrien-lejard.gitbook.io/chopper/converters/built-value-converter repository: https://github.com/lejard-h/chopper diff --git a/chopper_generator/CHANGELOG.md b/chopper_generator/CHANGELOG.md index 0b7c9f39..945efac1 100644 --- a/chopper_generator/CHANGELOG.md +++ b/chopper_generator/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## 7.0.5+1 + +- Fix Github release workflow permissions ([#512](https://github.com/lejard-h/chopper/pull/512)) + +## 7.0.5 + +- Correct static analysis suppression of *.chopper.dart files ([#507](https://github.com/lejard-h/chopper/pull/507)) + ## 7.0.4 - Ignore unnecessary_string_interpolations ([#501](https://github.com/lejard-h/chopper/pull/501)) diff --git a/chopper_generator/pubspec.yaml b/chopper_generator/pubspec.yaml index 5cfb749d..62eea89c 100644 --- a/chopper_generator/pubspec.yaml +++ b/chopper_generator/pubspec.yaml @@ -1,6 +1,6 @@ name: chopper_generator description: Chopper is an http client generator using source_gen, inspired by Retrofit -version: 7.0.4 +version: 7.0.5+1 documentation: https://hadrien-lejard.gitbook.io/chopper repository: https://github.com/lejard-h/chopper From 52133f647fd0b6d110538388100bcfb4f48afc81 Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Wed, 11 Oct 2023 07:03:23 +0100 Subject: [PATCH 108/171] :sparkles: encode DateTime as ISO8601 (#516) --- chopper/lib/src/utils.dart | 6 +- chopper/test/base_test.dart | 78 ++++++++++++++++++++++++-- chopper/test/test_service.chopper.dart | 13 +++++ chopper/test/test_service.dart | 5 ++ 4 files changed, 97 insertions(+), 5 deletions(-) diff --git a/chopper/lib/src/utils.dart b/chopper/lib/src/utils.dart index ea4995ff..63eeba6e 100644 --- a/chopper/lib/src/utils.dart +++ b/chopper/lib/src/utils.dart @@ -129,7 +129,11 @@ Iterable<_Pair> _iterableToQuery( ), ); -String _normalizeValue(value) => Uri.encodeComponent(value?.toString() ?? ''); +String _normalizeValue(value) => Uri.encodeComponent( + value is DateTime + ? value.toUtc().toIso8601String() + : value?.toString() ?? '', + ); final class _Pair with EquatableMixin { final A first; diff --git a/chopper/test/base_test.dart b/chopper/test/base_test.dart index f1e76dad..5175453f 100644 --- a/chopper/test/base_test.dart +++ b/chopper/test/base_test.dart @@ -1292,6 +1292,8 @@ void main() { }); test('Map query param using default dot QueryMapSeparator', () async { + final DateTime now = DateTime.now(); + final httpClient = MockClient((request) async { expect( request.url.toString(), @@ -1304,7 +1306,8 @@ void main() { '&value.etc.mno.uvw=xyz' '&value.etc.mno.list=a' '&value.etc.mno.list=123' - '&value.etc.mno.list=false'), + '&value.etc.mno.list=false' + '&value.etc.dt=${Uri.encodeComponent(now.toUtc().toIso8601String())}'), ); expect(request.method, equals('GET')); @@ -1325,6 +1328,7 @@ void main() { 'uvw': 'xyz', 'list': ['a', 123, false], }, + 'dt': now, }, }); @@ -1335,6 +1339,8 @@ void main() { }); test('Map query param with brackets QueryMapSeparator', () async { + final DateTime now = DateTime.now(); + final httpClient = MockClient((request) async { expect( request.url.toString(), @@ -1347,7 +1353,8 @@ void main() { '&value%5Betc%5D%5Bmno%5D%5Buvw%5D=xyz' '&value%5Betc%5D%5Bmno%5D%5Blist%5D%5B%5D=a' '&value%5Betc%5D%5Bmno%5D%5Blist%5D%5B%5D=123' - '&value%5Betc%5D%5Bmno%5D%5Blist%5D%5B%5D=false'), + '&value%5Betc%5D%5Bmno%5D%5Blist%5D%5B%5D=false' + '&value%5Betc%5D%5Bdt%5D=${Uri.encodeComponent(now.toUtc().toIso8601String())}'), ); expect(request.method, equals('GET')); @@ -1369,6 +1376,7 @@ void main() { 'uvw': 'xyz', 'list': ['a', 123, false], }, + 'dt': now, }, }); @@ -1379,6 +1387,8 @@ void main() { }); test('Map query param without including null query vars', () async { + final DateTime now = DateTime.now(); + final httpClient = MockClient((request) async { expect( request.url.toString(), @@ -1388,7 +1398,8 @@ void main() { '&value.etc.mno.opq=rst' '&value.etc.mno.list=a' '&value.etc.mno.list=123' - '&value.etc.mno.list=false'), + '&value.etc.mno.list=false' + '&value.etc.dt=${Uri.encodeComponent(now.toUtc().toIso8601String())}'), ); expect(request.method, equals('GET')); @@ -1409,6 +1420,7 @@ void main() { 'uvw': null, 'list': ['a', 123, false], }, + 'dt': now, }, }); @@ -1419,6 +1431,8 @@ void main() { }); test('Map query param including null query vars', () async { + final DateTime now = DateTime.now(); + final httpClient = MockClient((request) async { expect( request.url.toString(), @@ -1431,7 +1445,8 @@ void main() { '&value.etc.mno.uvw=' '&value.etc.mno.list=a' '&value.etc.mno.list=123' - '&value.etc.mno.list=false'), + '&value.etc.mno.list=false' + '&value.etc.dt=${Uri.encodeComponent(now.toUtc().toIso8601String())}'), ); expect(request.method, equals('GET')); @@ -1453,6 +1468,7 @@ void main() { 'uvw': null, 'list': ['a', 123, false], }, + 'dt': now, }, }); @@ -1510,4 +1526,58 @@ void main() { ), ); }); + + { + DateTime.utc(2023, 1, 1): '2023-01-01T00%3A00%3A00.000Z', + DateTime.utc(2023, 1, 1, 12, 34, 56): '2023-01-01T12%3A34%3A56.000Z', + DateTime.utc(2023, 1, 1, 12, 34, 56, 789): '2023-01-01T12%3A34%3A56.789Z', + }.forEach((DateTime dateTime, String expected) { + test('DateTime is encoded as ISO8601', () async { + final httpClient = MockClient((request) async { + expect( + request.url.toString(), + equals('$baseUrl/test/date_time?value=$expected'), + ); + expect(request.method, equals('GET')); + + return http.Response('get response', 200); + }); + + final chopper = buildClient(httpClient, JsonConverter()); + final service = chopper.getService(); + + final response = await service.getDateTime(dateTime); + + expect(response.body, equals('get response')); + expect(response.statusCode, equals(200)); + + httpClient.close(); + }); + }); + + test('Local DateTime is encoded as UTC ISO8601', () async { + final DateTime dateTime = DateTime.now(); + final String expected = + Uri.encodeComponent(dateTime.toUtc().toIso8601String()); + + final httpClient = MockClient((request) async { + expect( + request.url.toString(), + equals('$baseUrl/test/date_time?value=$expected'), + ); + expect(request.method, equals('GET')); + + return http.Response('get response', 200); + }); + + final chopper = buildClient(httpClient, JsonConverter()); + final service = chopper.getService(); + + final response = await service.getDateTime(dateTime); + + expect(response.body, equals('get response')); + expect(response.statusCode, equals(200)); + + httpClient.close(); + }); } diff --git a/chopper/test/test_service.chopper.dart b/chopper/test/test_service.chopper.dart index f070c8a8..028a7310 100644 --- a/chopper/test/test_service.chopper.dart +++ b/chopper/test/test_service.chopper.dart @@ -636,4 +636,17 @@ final class _$HttpTestService extends HttpTestService { ); return client.send($request); } + + @override + Future> getDateTime(DateTime value) { + final Uri $url = Uri.parse('/test/date_time'); + final Map $params = {'value': value}; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + ); + return client.send($request); + } } diff --git a/chopper/test/test_service.dart b/chopper/test/test_service.dart index c0baf1a4..f1895716 100644 --- a/chopper/test/test_service.dart +++ b/chopper/test/test_service.dart @@ -188,6 +188,11 @@ abstract class HttpTestService extends ChopperService { Future> getUsingMapQueryParamWithBrackets( @Query('value') Map value, ); + + @Get(path: '/date_time') + Future> getDateTime( + @Query('value') DateTime value, + ); } Request customConvertRequest(Request req) { From 24179ab0d8f306bfc74ba69450cc1e6367cd5cbc Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Wed, 11 Oct 2023 09:20:27 +0100 Subject: [PATCH 109/171] :twisted_rightwards_arrows: post release branch sync (#518) --- chopper/CHANGELOG.md | 4 ++++ chopper/pubspec.yaml | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/chopper/CHANGELOG.md b/chopper/CHANGELOG.md index b46c66c5..4237bb54 100644 --- a/chopper/CHANGELOG.md +++ b/chopper/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 7.0.8 + +- Encode DateTime query parameters in ISO8601 format ([#516](https://github.com/lejard-h/chopper/pull/516)) + ## 7.0.7+1 - Fix Github release workflow permissions ([#512](https://github.com/lejard-h/chopper/pull/512)) diff --git a/chopper/pubspec.yaml b/chopper/pubspec.yaml index c339bc4c..79716f73 100644 --- a/chopper/pubspec.yaml +++ b/chopper/pubspec.yaml @@ -1,6 +1,6 @@ name: chopper description: Chopper is an http client generator using source_gen, inspired by Retrofit -version: 7.0.7+1 +version: 7.0.8 documentation: https://hadrien-lejard.gitbook.io/chopper repository: https://github.com/lejard-h/chopper From 4860d3aab4439870344945b25affd8259f83c7e3 Mon Sep 17 00:00:00 2001 From: Martin Alejandro Escobar Espinel <56127727+martinale14@users.noreply.github.com> Date: Thu, 12 Oct 2023 06:29:40 -0500 Subject: [PATCH 110/171] Fix incorrect url generation when using new baseurl (#520) * Changing replace all to ignore http:// and https:// * Adding fix to generator * change on pull request number * change implementation --------- Co-authored-by: Martin Alejandro Escobar Espinel --- chopper/test/base_test.dart | 36 +++++ chopper/test/ensure_build_test.dart | 10 +- .../test/test_service_base_url.chopper.dart | 148 ++++++++++++++++++ chopper/test/test_service_base_url.dart | 83 ++++++++++ chopper_generator/lib/src/generator.dart | 17 +- 5 files changed, 286 insertions(+), 8 deletions(-) create mode 100644 chopper/test/test_service_base_url.chopper.dart create mode 100644 chopper/test/test_service_base_url.dart diff --git a/chopper/test/base_test.dart b/chopper/test/base_test.dart index 5175453f..fe22ee71 100644 --- a/chopper/test/base_test.dart +++ b/chopper/test/base_test.dart @@ -10,6 +10,7 @@ import 'package:test/test.dart'; import 'package:transparent_image/transparent_image.dart'; import 'test_service.dart'; +import 'test_service_base_url.dart'; import 'test_service_variable.dart'; final baseUrl = Uri.parse('http://localhost:8000'); @@ -26,6 +27,7 @@ void main() { // the generated service HttpTestService.create(), HttpTestServiceVariable.create(), + HttpTestServiceBaseUrl.create(), ], client: httpClient, errorConverter: errorConverter, @@ -1164,6 +1166,23 @@ void main() { await service.getAll(); }); + test('Empty path gives no trailing slash new base url', () async { + final httpClient = MockClient((request) async { + expect( + request.url.toString(), + equals('$testEnv/test'), + ); + expect(request.method, equals('GET')); + + return http.Response('get response', 200); + }); + + final chopper = buildClient(httpClient); + final service = chopper.getService(); + + await service.getAll(); + }); + test('Slash in path gives a trailing slash', () async { final httpClient = MockClient((request) async { expect( @@ -1181,6 +1200,23 @@ void main() { await service.getAllWithTrailingSlash(); }); + test('Slash in path gives a trailing slash new base url', () async { + final httpClient = MockClient((request) async { + expect( + request.url.toString(), + equals('$testEnv/test/'), + ); + expect(request.method, equals('GET')); + + return http.Response('get response', 200); + }); + + final chopper = buildClient(httpClient); + final service = chopper.getService(); + + await service.getAllWithTrailingSlash(); + }); + test('timeout', () async { final httpClient = MockClient((http.Request req) async { await Future.delayed(const Duration(minutes: 1)); diff --git a/chopper/test/ensure_build_test.dart b/chopper/test/ensure_build_test.dart index a599c5d2..0cf64d5f 100644 --- a/chopper/test/ensure_build_test.dart +++ b/chopper/test/ensure_build_test.dart @@ -6,17 +6,13 @@ import 'package:test/test.dart'; void main() { test( 'ensure_build', - () { - expectBuildClean( + () async { + await expectBuildClean( packageRelativeDirectory: 'chopper', gitDiffPathArguments: [ 'test/test_service.chopper.dart', - ], - ); - expectBuildClean( - packageRelativeDirectory: 'chopper', - gitDiffPathArguments: [ 'test/test_service_variable.chopper.dart', + 'test/test_service_base_url.chopper.dart', ], ); }, diff --git a/chopper/test/test_service_base_url.chopper.dart b/chopper/test/test_service_base_url.chopper.dart new file mode 100644 index 00000000..1b77e188 --- /dev/null +++ b/chopper/test/test_service_base_url.chopper.dart @@ -0,0 +1,148 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'test_service_base_url.dart'; + +// ************************************************************************** +// ChopperGenerator +// ************************************************************************** + +// ignore_for_file: type=lint +final class _$HttpTestServiceBaseUrl extends HttpTestServiceBaseUrl { + _$HttpTestServiceBaseUrl([ChopperClient? client]) { + if (client == null) return; + this.client = client; + } + + @override + final definitionType = HttpTestServiceBaseUrl; + + @override + Future> getAll() { + final Uri $url = Uri.parse('https://localhost:4000/test'); + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + ); + return client.send($request); + } + + @override + Future> getAllWithTrailingSlash() { + final Uri $url = Uri.parse('https://localhost:4000/test/'); + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + ); + return client.send($request); + } + + @override + Future>> listString() { + final Uri $url = Uri.parse('https://localhost:4000/test/list/string'); + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + ); + return client.send, String>($request); + } + + @override + Future> getUsingQueryParamIncludeNullQueryVars({ + String? foo, + String? bar, + String? baz, + }) { + final Uri $url = Uri.parse( + 'https://localhost:4000/test/query_param_include_null_query_vars'); + final Map $params = { + 'foo': foo, + 'bar': bar, + 'baz': baz, + }; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + includeNullQueryVars: true, + ); + return client.send($request); + } + + @override + Future> getUsingListQueryParam(List value) { + final Uri $url = Uri.parse('https://localhost:4000/test/list_query_param'); + final Map $params = {'value': value}; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + ); + return client.send($request); + } + + @override + Future> getUsingListQueryParamWithBrackets( + List value) { + final Uri $url = + Uri.parse('https://localhost:4000/test/list_query_param_with_brackets'); + final Map $params = {'value': value}; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + useBrackets: true, + ); + return client.send($request); + } + + @override + Future> getUsingMapQueryParam(Map value) { + final Uri $url = Uri.parse('https://localhost:4000/test/map_query_param'); + final Map $params = {'value': value}; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + ); + return client.send($request); + } + + @override + Future> getUsingMapQueryParamIncludeNullQueryVars( + Map value) { + final Uri $url = Uri.parse( + 'https://localhost:4000/test/map_query_param_include_null_query_vars'); + final Map $params = {'value': value}; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + includeNullQueryVars: true, + ); + return client.send($request); + } + + @override + Future> getUsingMapQueryParamWithBrackets( + Map value) { + final Uri $url = + Uri.parse('https://localhost:4000/test/map_query_param_with_brackets'); + final Map $params = {'value': value}; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + useBrackets: true, + ); + return client.send($request); + } +} diff --git a/chopper/test/test_service_base_url.dart b/chopper/test/test_service_base_url.dart new file mode 100644 index 00000000..02de8bcf --- /dev/null +++ b/chopper/test/test_service_base_url.dart @@ -0,0 +1,83 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:chopper/chopper.dart'; + +part 'test_service_base_url.chopper.dart'; + +@ChopperApi(baseUrl: 'https://localhost:4000/test') +abstract class HttpTestServiceBaseUrl extends ChopperService { + static HttpTestServiceBaseUrl create([ChopperClient? client]) => + _$HttpTestServiceBaseUrl(client); + + @Get(path: '') + Future getAll(); + + @Get(path: '/') + Future getAllWithTrailingSlash(); + + @Get(path: '/list/string') + Future>> listString(); + + @Get(path: '/query_param_include_null_query_vars', includeNullQueryVars: true) + Future> getUsingQueryParamIncludeNullQueryVars({ + @Query('foo') String? foo, + @Query('bar') String? bar, + @Query('baz') String? baz, + }); + + @Get(path: '/list_query_param') + Future> getUsingListQueryParam( + @Query('value') List value, + ); + + @Get(path: '/list_query_param_with_brackets', useBrackets: true) + Future> getUsingListQueryParamWithBrackets( + @Query('value') List value, + ); + + @Get(path: '/map_query_param') + Future> getUsingMapQueryParam( + @Query('value') Map value, + ); + + @Get( + path: '/map_query_param_include_null_query_vars', + includeNullQueryVars: true, + ) + Future> getUsingMapQueryParamIncludeNullQueryVars( + @Query('value') Map value, + ); + + @Get(path: '/map_query_param_with_brackets', useBrackets: true) + Future> getUsingMapQueryParamWithBrackets( + @Query('value') Map value, + ); +} + +Request customConvertRequest(Request req) { + final r = JsonConverter().convertRequest(req); + + return applyHeader(r, 'customConverter', 'true'); +} + +Response customConvertResponse(Response res) => + res.copyWith(body: json.decode(res.body)); + +Request convertForm(Request req) { + req = applyHeader(req, contentTypeKey, formEncodedHeaders); + + if (req.body is Map) { + final body = {}; + + req.body.forEach((key, val) { + if (val != null) { + body[key.toString()] = val.toString(); + } + }); + + req = req.copyWith(body: body); + } + + return req; +} diff --git a/chopper_generator/lib/src/generator.dart b/chopper_generator/lib/src/generator.dart index 3a6b36ef..007649bf 100644 --- a/chopper_generator/lib/src/generator.dart +++ b/chopper_generator/lib/src/generator.dart @@ -550,7 +550,22 @@ final class ChopperGenerator } } - return _generateUri('$finalBaseUrl$path'.replaceAll('//', '/')); + if (finalBaseUrl.startsWith('http://') || + finalBaseUrl.startsWith('https://')) { + final tempUri = Uri.tryParse(finalBaseUrl); + + if (tempUri != null) { + final urlNoScheme = + '${tempUri.authority}${tempUri.path}$path'.replaceAll('//', '/'); + return _generateUri( + '${tempUri.scheme}://$urlNoScheme', + ); + } + } + + return _generateUri( + '$finalBaseUrl$path'.replaceAll('//', '/'), + ); } static Expression _generateUri(String url) => From 8dd5a84b9bf122edcb495605775c23903abd998d Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Fri, 13 Oct 2023 19:31:24 +0100 Subject: [PATCH 111/171] :twisted_rightwards_arrows: post release branch sync (#522) --- chopper_generator/CHANGELOG.md | 4 ++++ chopper_generator/pubspec.yaml | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/chopper_generator/CHANGELOG.md b/chopper_generator/CHANGELOG.md index 945efac1..e65b713a 100644 --- a/chopper_generator/CHANGELOG.md +++ b/chopper_generator/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 7.0.6 + +- Fix incorrect url generation when using new baseUrl ([#520](https://github.com/lejard-h/chopper/pull/520)) + ## 7.0.5+1 - Fix Github release workflow permissions ([#512](https://github.com/lejard-h/chopper/pull/512)) diff --git a/chopper_generator/pubspec.yaml b/chopper_generator/pubspec.yaml index 62eea89c..aaea18f4 100644 --- a/chopper_generator/pubspec.yaml +++ b/chopper_generator/pubspec.yaml @@ -1,6 +1,6 @@ name: chopper_generator description: Chopper is an http client generator using source_gen, inspired by Retrofit -version: 7.0.5+1 +version: 7.0.6 documentation: https://hadrien-lejard.gitbook.io/chopper repository: https://github.com/lejard-h/chopper From 08ee1511713cfeb53236f6cf3cb36a495d9c6e83 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Oct 2023 15:30:13 +0100 Subject: [PATCH 112/171] :arrow_up: Bump lints from 2.1.1 to 3.0.0 in /chopper (#525) Bumps [lints](https://github.com/dart-lang/lints) from 2.1.1 to 3.0.0. - [Release notes](https://github.com/dart-lang/lints/releases) - [Changelog](https://github.com/dart-lang/lints/blob/main/CHANGELOG.md) - [Commits](https://github.com/dart-lang/lints/compare/v2.1.1...v3.0.0) --- updated-dependencies: - dependency-name: lints dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- chopper/pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chopper/pubspec.yaml b/chopper/pubspec.yaml index 79716f73..46e34310 100644 --- a/chopper/pubspec.yaml +++ b/chopper/pubspec.yaml @@ -22,7 +22,7 @@ dev_dependencies: data_fixture_dart: ^2.2.0 faker: ^2.1.0 http_parser: ^4.0.2 - lints: ^2.1.1 + lints: ">=2.1.1 <4.0.0" test: ^1.24.4 transparent_image: ^2.0.1 chopper_generator: ^7.0.0 From 723b5fc90f209b52adb6dbfe1040154ce32f4364 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Oct 2023 15:30:27 +0100 Subject: [PATCH 113/171] :arrow_up: Bump lints from 2.1.1 to 3.0.0 in /chopper_built_value (#524) Bumps [lints](https://github.com/dart-lang/lints) from 2.1.1 to 3.0.0. - [Release notes](https://github.com/dart-lang/lints/releases) - [Changelog](https://github.com/dart-lang/lints/blob/main/CHANGELOG.md) - [Commits](https://github.com/dart-lang/lints/compare/v2.1.1...v3.0.0) --- updated-dependencies: - dependency-name: lints dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- chopper_built_value/pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chopper_built_value/pubspec.yaml b/chopper_built_value/pubspec.yaml index ab34497d..f5b48036 100644 --- a/chopper_built_value/pubspec.yaml +++ b/chopper_built_value/pubspec.yaml @@ -18,7 +18,7 @@ dev_dependencies: build_runner: ^2.4.6 build_test: ^2.2.0 built_value_generator: ^8.6.1 - lints: ^2.1.1 + lints: ">=2.1.1 <4.0.0" dependency_overrides: chopper: From f06135580b8dbeefcc906541d75d0c812e1b0829 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Oct 2023 15:30:41 +0100 Subject: [PATCH 114/171] :arrow_up: Bump lints from 2.1.1 to 3.0.0 in /chopper_generator (#523) Bumps [lints](https://github.com/dart-lang/lints) from 2.1.1 to 3.0.0. - [Release notes](https://github.com/dart-lang/lints/releases) - [Changelog](https://github.com/dart-lang/lints/blob/main/CHANGELOG.md) - [Commits](https://github.com/dart-lang/lints/compare/v2.1.1...v3.0.0) --- updated-dependencies: - dependency-name: lints dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- chopper_generator/pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chopper_generator/pubspec.yaml b/chopper_generator/pubspec.yaml index aaea18f4..bb06b46d 100644 --- a/chopper_generator/pubspec.yaml +++ b/chopper_generator/pubspec.yaml @@ -22,7 +22,7 @@ dev_dependencies: build_runner: ^2.4.6 build_verify: ^3.1.0 http: ^1.1.0 - lints: ^2.1.1 + lints: ">=2.1.1 <4.0.0" test: ^1.24.4 dependency_overrides: From e5417d1f00cdb77de8bc483b142fe814e8e17a60 Mon Sep 17 00:00:00 2001 From: Diego Tori Date: Thu, 26 Oct 2023 15:09:12 -0400 Subject: [PATCH 115/171] Add mock mixins of Chopper components. (#529) --- chopper/lib/src/base.dart | 11 +++++++++ chopper/lib/src/chopper_log_record.dart | 12 +++++++++ chopper/lib/src/request.dart | 33 +++++++++++++++++++++++++ chopper/lib/src/response.dart | 11 +++++++++ 4 files changed, 67 insertions(+) diff --git a/chopper/lib/src/base.dart b/chopper/lib/src/base.dart index 504590e2..baf93319 100644 --- a/chopper/lib/src/base.dart +++ b/chopper/lib/src/base.dart @@ -515,6 +515,17 @@ base class ChopperClient { Stream get onResponse => _responseController.stream; } +/// +/// [ChopperClient] mixin for the purposes of creating mocks +/// using a mocking framework such as Mockito or Mocktail. +/// +/// ```dart +/// base class MockChopperClient extends Mock with MockChopperClientMixin {} +/// ``` +/// +@visibleForTesting +base mixin MockChopperClientMixin implements ChopperClient {} + /// A marker and helper class used by `chopper_generator` to generate network /// call implementations. /// diff --git a/chopper/lib/src/chopper_log_record.dart b/chopper/lib/src/chopper_log_record.dart index 1f02a036..3f2d11f2 100644 --- a/chopper/lib/src/chopper_log_record.dart +++ b/chopper/lib/src/chopper_log_record.dart @@ -1,5 +1,6 @@ import 'package:chopper/src/request.dart'; import 'package:chopper/src/response.dart'; +import 'package:meta/meta.dart'; final class ChopperLogRecord { const ChopperLogRecord(this.message, {this.request, this.response}); @@ -11,3 +12,14 @@ final class ChopperLogRecord { @override String toString() => message; } + +/// +/// [ChopperLogRecord] mixin for the purposes of creating mocks +/// using a mocking framework such as Mockito or Mocktail. +/// +/// ```dart +/// base class MockChopperLogRecord extends Mock with MockChopperLogRecordMixin {} +/// ``` +/// +@visibleForTesting +base mixin MockChopperLogRecordMixin implements ChopperLogRecord {} diff --git a/chopper/lib/src/request.dart b/chopper/lib/src/request.dart index 0f134f7d..e07dbe3b 100644 --- a/chopper/lib/src/request.dart +++ b/chopper/lib/src/request.dart @@ -237,6 +237,17 @@ base class Request extends http.BaseRequest with EquatableMixin { ]; } +/// +/// [Request] mixin for the purposes of creating mocks +/// using a mocking framework such as Mockito or Mocktail. +/// +/// ```dart +/// base class MockRequest extends Mock with MockRequestMixin {} +/// ``` +/// +@visibleForTesting +base mixin MockRequestMixin implements Request {} + /// Represents a part in a multipart request. @immutable final class PartValue with EquatableMixin { @@ -263,8 +274,30 @@ final class PartValue with EquatableMixin { ]; } +/// +/// [PartValue] mixin for the purposes of creating mocks +/// using a mocking framework such as Mockito or Mocktail. +/// +/// ```dart +/// base class MockPartValue extends Mock with MockPartValueMixin {} +/// ``` +/// +@visibleForTesting +base mixin MockPartValueMixin implements PartValue {} + /// Represents a file [PartValue] in a multipart request. @immutable final class PartValueFile extends PartValue { const PartValueFile(super.name, super.value); } + +/// +/// [PartValueFile] mixin for the purposes of creating mocks +/// using a mocking framework such as Mockito or Mocktail. +/// +/// ```dart +/// base class MockPartValueFile extends Mock with MockPartValueFileMixin {} +/// ``` +/// +@visibleForTesting +base mixin MockPartValueFileMixin implements PartValueFile {} diff --git a/chopper/lib/src/response.dart b/chopper/lib/src/response.dart index 3ec5c499..92d1a8bb 100644 --- a/chopper/lib/src/response.dart +++ b/chopper/lib/src/response.dart @@ -74,3 +74,14 @@ base class Response with EquatableMixin { error, ]; } + +/// +/// [Response] mixin for the purposes of creating mocks +/// using a mocking framework such as Mockito or Mocktail. +/// +/// ```dart +/// base class MockResponse extends Mock with MockResponseMixin {} +/// ``` +/// +@visibleForTesting +base mixin MockResponseMixin implements Response {} From e34feea36697e81d4691b734fbd649d48cc49ce9 Mon Sep 17 00:00:00 2001 From: Diego Tori Date: Thu, 26 Oct 2023 16:30:31 -0400 Subject: [PATCH 116/171] Add success/failure callback hooks to Authenticator. (#527) --- chopper/lib/src/authenticator.dart | 54 +++++- chopper/lib/src/base.dart | 5 +- chopper/test/authenticator_test.dart | 255 +++++++++++++++++++++++++++ chopper/test/fake_authenticator.dart | 45 ++++- 4 files changed, 355 insertions(+), 4 deletions(-) diff --git a/chopper/lib/src/authenticator.dart b/chopper/lib/src/authenticator.dart index d69e6d76..1d0ba176 100644 --- a/chopper/lib/src/authenticator.dart +++ b/chopper/lib/src/authenticator.dart @@ -2,12 +2,62 @@ import 'dart:async'; import 'package:chopper/chopper.dart'; -/// This method should return a [Request] that includes credentials to satisfy an authentication challenge received in -/// [response]. It should return `null` if the challenge cannot be satisfied. +/// +/// Callback that is called when an authentication challenge is received +/// based on the given [request], [response], and optionally the +/// [originalRequest]. +/// +typedef AuthenticationCallback = FutureOr Function( + Request request, + Response response, [ + Request? originalRequest, +]); + +/// +/// Handles authentication challenges raised by the [ChopperClient]. +/// +/// Optionally, you can override either [onAuthenticationSuccessful] or +/// [onAuthenticationFailed] in order to listen to when a particular +/// authentication request succeeds or fails. +/// +/// For example, you can use these in order to reset or mutate your +/// instance's internal state for the purposes of keeping track of +/// the number of retries made to authenticate a request. +/// +/// Furthermore, you can use these callbacks to determine whether +/// your authentication [Request] from [authenticate] actually succeeded +/// or failed. +/// abstract class Authenticator { + /// + /// Returns a [Request] that includes credentials to satisfy + /// an authentication challenge received in [response], based on + /// the incoming [request] or optionally, the [originalRequest] + /// (which was not modified with any previous [RequestInterceptor]s). + /// + /// Otherwise, return `null` if the challenge cannot be satisfied. + /// FutureOr authenticate( Request request, Response response, [ Request? originalRequest, ]); + + /// + /// Optional callback called by [ChopperClient] when the outgoing + /// request from [authenticate] was successful. + /// + /// You can use this to determine whether that request actually succeeded + /// in authenticating the user. + /// + AuthenticationCallback? get onAuthenticationSuccessful => null; + + /// + /// Optional callback called by [ChopperClient] when the outgoing + /// request from [authenticate] failed to authenticate. + /// + /// You can use this to determine whether that request failed to recover + /// the user's session. + /// + AuthenticationCallback? get onAuthenticationFailed => null; } diff --git a/chopper/lib/src/base.dart b/chopper/lib/src/base.dart index baf93319..066f9cc7 100644 --- a/chopper/lib/src/base.dart +++ b/chopper/lib/src/base.dart @@ -320,10 +320,13 @@ base class ChopperClient { ); // To prevent double call with typed response if (_responseIsSuccessful(res.statusCode)) { + await authenticator!.onAuthenticationSuccessful + ?.call(updatedRequest, res, request); return _processResponse(res); } else { res = await _handleErrorResponse(res); - + await authenticator!.onAuthenticationFailed + ?.call(updatedRequest, res, request); return _processResponse(res); } } diff --git a/chopper/test/authenticator_test.dart b/chopper/test/authenticator_test.dart index 8f4ebb20..7b454462 100644 --- a/chopper/test/authenticator_test.dart +++ b/chopper/test/authenticator_test.dart @@ -85,6 +85,7 @@ void main() async { }); final chopper = buildClient(httpClient); + final authenticator = chopper.authenticator as FakeAuthenticator; final response = await chopper.get( Uri( path: '/test/get', @@ -97,6 +98,59 @@ void main() async { expect(response.statusCode, equals(200)); expect(tested['authenticated'], equals(true)); expect(tested['unauthenticated'], equals(true)); + expect(authenticator.capturedRequest, + authenticator.capturedAuthenticateRequest); + expect(authenticator.capturedOriginalRequest, + authenticator.capturedAuthenticateOriginalRequest); + expect(authenticator.capturedResponse, response); + expect(authenticator.onAuthenticationSuccessfulCalled, isTrue); + + httpClient.close(); + }); + + test('unauthorized total failure', () async { + final httpClient = MockClient((request) async { + expect( + request.url.toString(), + equals('$baseUrl/test/get?key=val'), + ); + expect(request.method, equals('GET')); + expect(request.headers['foo'], equals('bar')); + expect(request.headers['int'], equals('42')); + + if (!authenticated) { + tested['unauthenticated'] = true; + authenticated = true; + + return http.Response('unauthorized', 401); + } else { + tested['authenticated'] = true; + expect(request.headers['authorization'], equals('some_fake_token')); + } + + return http.Response('Access Denied', 403); + }); + + final chopper = buildClient(httpClient); + final authenticator = chopper.authenticator as FakeAuthenticator; + final response = await chopper.get( + Uri( + path: '/test/get', + queryParameters: {'key': 'val'}, + ), + headers: {'int': '42'}, + ); + + expect(response.body, anyOf(isNull, isEmpty)); + expect(response.statusCode, equals(403)); + expect(tested['authenticated'], equals(true)); + expect(tested['unauthenticated'], equals(true)); + expect(authenticator.capturedRequest, + authenticator.capturedAuthenticateRequest); + expect(authenticator.capturedOriginalRequest, + authenticator.capturedAuthenticateOriginalRequest); + expect(authenticator.capturedResponse, response); + expect(authenticator.onAuthenticationFailedCalled, isTrue); httpClient.close(); }); @@ -177,6 +231,7 @@ void main() async { }); final chopper = buildClient(httpClient); + final authenticator = chopper.authenticator as FakeAuthenticator; final response = await chopper.post( Uri( path: '/test/post', @@ -193,6 +248,72 @@ void main() async { expect(response.statusCode, equals(200)); expect(tested['authenticated'], equals(true)); expect(tested['unauthenticated'], equals(true)); + expect(authenticator.capturedRequest, + authenticator.capturedAuthenticateRequest); + expect(authenticator.capturedOriginalRequest, + authenticator.capturedAuthenticateOriginalRequest); + expect(authenticator.capturedResponse, response); + expect(authenticator.onAuthenticationSuccessfulCalled, isTrue); + + httpClient.close(); + }); + + test('unauthorized total failure', () async { + final httpClient = MockClient((request) async { + expect( + request.url.toString(), + equals('$baseUrl/test/post?key=val'), + ); + expect(request.method, equals('POST')); + expect(request.headers['foo'], equals('bar')); + expect(request.headers['int'], equals('42')); + expect( + request.body, + jsonEncode( + { + 'name': 'john', + 'surname': 'doe', + }, + ), + ); + + if (!authenticated) { + tested['unauthenticated'] = true; + authenticated = true; + + return http.Response('unauthorized', 401); + } else { + tested['authenticated'] = true; + expect(request.headers['authorization'], equals('some_fake_token')); + } + + return http.Response('Access Denied', 403); + }); + + final chopper = buildClient(httpClient); + final authenticator = chopper.authenticator as FakeAuthenticator; + final response = await chopper.post( + Uri( + path: '/test/post', + queryParameters: {'key': 'val'}, + ), + headers: {'int': '42'}, + body: { + 'name': 'john', + 'surname': 'doe', + }, + ); + + expect(response.body, anyOf(isNull, isEmpty)); + expect(response.statusCode, equals(403)); + expect(tested['authenticated'], equals(true)); + expect(tested['unauthenticated'], equals(true)); + expect(authenticator.capturedRequest, + authenticator.capturedAuthenticateRequest); + expect(authenticator.capturedOriginalRequest, + authenticator.capturedAuthenticateOriginalRequest); + expect(authenticator.capturedResponse, response); + expect(authenticator.onAuthenticationFailedCalled, isTrue); httpClient.close(); }); @@ -273,6 +394,7 @@ void main() async { }); final chopper = buildClient(httpClient); + final authenticator = chopper.authenticator as FakeAuthenticator; final response = await chopper.put( Uri( path: '/test/put', @@ -289,6 +411,72 @@ void main() async { expect(response.statusCode, equals(200)); expect(tested['authenticated'], equals(true)); expect(tested['unauthenticated'], equals(true)); + expect(authenticator.capturedRequest, + authenticator.capturedAuthenticateRequest); + expect(authenticator.capturedOriginalRequest, + authenticator.capturedAuthenticateOriginalRequest); + expect(authenticator.capturedResponse, response); + expect(authenticator.onAuthenticationSuccessfulCalled, isTrue); + + httpClient.close(); + }); + + test('unauthorized total failure', () async { + final httpClient = MockClient((request) async { + expect( + request.url.toString(), + equals('$baseUrl/test/put?key=val'), + ); + expect(request.method, equals('PUT')); + expect(request.headers['foo'], equals('bar')); + expect(request.headers['int'], equals('42')); + expect( + request.body, + jsonEncode( + { + 'name': 'john', + 'surname': 'doe', + }, + ), + ); + + if (!authenticated) { + tested['unauthenticated'] = true; + authenticated = true; + + return http.Response('unauthorized', 401); + } else { + tested['authenticated'] = true; + expect(request.headers['authorization'], equals('some_fake_token')); + } + + return http.Response('Access Denied', 403); + }); + + final chopper = buildClient(httpClient); + final authenticator = chopper.authenticator as FakeAuthenticator; + final response = await chopper.put( + Uri( + path: '/test/put', + queryParameters: {'key': 'val'}, + ), + headers: {'int': '42'}, + body: { + 'name': 'john', + 'surname': 'doe', + }, + ); + + expect(response.body, anyOf(isNull, isEmpty)); + expect(response.statusCode, equals(403)); + expect(tested['authenticated'], equals(true)); + expect(tested['unauthenticated'], equals(true)); + expect(authenticator.capturedRequest, + authenticator.capturedAuthenticateRequest); + expect(authenticator.capturedOriginalRequest, + authenticator.capturedAuthenticateOriginalRequest); + expect(authenticator.capturedResponse, response); + expect(authenticator.onAuthenticationFailedCalled, isTrue); httpClient.close(); }); @@ -369,6 +557,7 @@ void main() async { }); final chopper = buildClient(httpClient); + final authenticator = chopper.authenticator as FakeAuthenticator; final response = await chopper.patch( Uri( path: '/test/patch', @@ -385,6 +574,72 @@ void main() async { expect(response.statusCode, equals(200)); expect(tested['authenticated'], equals(true)); expect(tested['unauthenticated'], equals(true)); + expect(authenticator.capturedRequest, + authenticator.capturedAuthenticateRequest); + expect(authenticator.capturedResponse, response); + expect(authenticator.capturedOriginalRequest, + authenticator.capturedAuthenticateOriginalRequest); + expect(authenticator.onAuthenticationSuccessfulCalled, isTrue); + + httpClient.close(); + }); + + test('unauthorized total failure', () async { + final httpClient = MockClient((request) async { + expect( + request.url.toString(), + equals('$baseUrl/test/patch?key=val'), + ); + expect(request.method, equals('PATCH')); + expect(request.headers['foo'], equals('bar')); + expect(request.headers['int'], equals('42')); + expect( + request.body, + jsonEncode( + { + 'name': 'john', + 'surname': 'doe', + }, + ), + ); + + if (!authenticated) { + tested['unauthenticated'] = true; + authenticated = true; + + return http.Response('unauthorized', 401); + } else { + tested['authenticated'] = true; + expect(request.headers['authorization'], equals('some_fake_token')); + } + + return http.Response('Access Denied', 403); + }); + + final chopper = buildClient(httpClient); + final authenticator = chopper.authenticator as FakeAuthenticator; + final response = await chopper.patch( + Uri( + path: '/test/patch', + queryParameters: {'key': 'val'}, + ), + headers: {'int': '42'}, + body: { + 'name': 'john', + 'surname': 'doe', + }, + ); + + expect(response.body, anyOf(isNull, isEmpty)); + expect(response.statusCode, equals(403)); + expect(tested['authenticated'], equals(true)); + expect(tested['unauthenticated'], equals(true)); + expect(authenticator.capturedRequest, + authenticator.capturedAuthenticateRequest); + expect(authenticator.capturedResponse, response); + expect(authenticator.capturedOriginalRequest, + authenticator.capturedAuthenticateOriginalRequest); + expect(authenticator.onAuthenticationFailedCalled, isTrue); httpClient.close(); }); diff --git a/chopper/test/fake_authenticator.dart b/chopper/test/fake_authenticator.dart index 22de9356..57237dae 100644 --- a/chopper/test/fake_authenticator.dart +++ b/chopper/test/fake_authenticator.dart @@ -3,6 +3,22 @@ import 'dart:async' show FutureOr; import 'package:chopper/chopper.dart'; class FakeAuthenticator extends Authenticator { + Request? capturedRequest; + + Response? capturedResponse; + + Request? capturedOriginalRequest; + + Request? capturedAuthenticateRequest; + + Response? capturedAuthenticateResponse; + + Request? capturedAuthenticateOriginalRequest; + + bool onAuthenticationSuccessfulCalled = false; + + bool onAuthenticationFailedCalled = false; + @override FutureOr authenticate( Request request, @@ -10,14 +26,41 @@ class FakeAuthenticator extends Authenticator { Request? originalRequest, ]) async { if (response.statusCode == 401) { - return request.copyWith( + capturedAuthenticateResponse = response; + capturedAuthenticateOriginalRequest = originalRequest; + capturedAuthenticateRequest = request.copyWith( headers: { ...request.headers, 'authorization': 'some_fake_token', }, ); + return capturedAuthenticateRequest; } return null; } + + @override + AuthenticationCallback? get onAuthenticationSuccessful => ( + Request request, + Response response, [ + Request? originalRequest, + ]) { + onAuthenticationSuccessfulCalled = true; + capturedRequest = request; + capturedResponse = response; + capturedOriginalRequest = originalRequest; + }; + + @override + AuthenticationCallback? get onAuthenticationFailed => ( + Request request, + Response response, [ + Request? originalRequest, + ]) { + onAuthenticationFailedCalled = true; + capturedRequest = request; + capturedResponse = response; + capturedOriginalRequest = originalRequest; + }; } From 4178d7dbba8c825edbaa40da7eeec46f8cf32930 Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Sun, 29 Oct 2023 08:14:22 +0000 Subject: [PATCH 117/171] :bug: fix Makefile show_test_coverage (#530) --- chopper/Makefile | 2 +- chopper_built_value/Makefile | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/chopper/Makefile b/chopper/Makefile index d52b82d2..facd10dd 100644 --- a/chopper/Makefile +++ b/chopper/Makefile @@ -48,7 +48,7 @@ sure: show_test_coverage: @# Help: Run Dart unit tests for the current project and show the coverage. dart pub global activate coverage && dart pub global run coverage:test_with_coverage - lcov --remove coverage/lcov.info '**.g.dart' '**.mock.dart' '**.chopper.dart' -o coverage/lcov_without_generated_code.info + lcov --remove coverage/lcov.info '**.g.dart' '**.mock.dart' '**.chopper.dart' -o coverage/lcov_without_generated_code.info --ignore-errors unused genhtml coverage/lcov_without_generated_code.info -o coverage/html source ../tool/makefile_helpers.sh && open_link "coverage/html/index.html" diff --git a/chopper_built_value/Makefile b/chopper_built_value/Makefile index d52b82d2..facd10dd 100644 --- a/chopper_built_value/Makefile +++ b/chopper_built_value/Makefile @@ -48,7 +48,7 @@ sure: show_test_coverage: @# Help: Run Dart unit tests for the current project and show the coverage. dart pub global activate coverage && dart pub global run coverage:test_with_coverage - lcov --remove coverage/lcov.info '**.g.dart' '**.mock.dart' '**.chopper.dart' -o coverage/lcov_without_generated_code.info + lcov --remove coverage/lcov.info '**.g.dart' '**.mock.dart' '**.chopper.dart' -o coverage/lcov_without_generated_code.info --ignore-errors unused genhtml coverage/lcov_without_generated_code.info -o coverage/html source ../tool/makefile_helpers.sh && open_link "coverage/html/index.html" From 9d392a13dc63118c8b41c8b0c10a7b57aa76f3d5 Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Wed, 1 Nov 2023 13:17:27 +0000 Subject: [PATCH 118/171] :twisted_rightwards_arrows: post release branch sync (#532) --- chopper/CHANGELOG.md | 5 +++++ chopper/pubspec.yaml | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/chopper/CHANGELOG.md b/chopper/CHANGELOG.md index 4237bb54..5f5b19cc 100644 --- a/chopper/CHANGELOG.md +++ b/chopper/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## 7.0.9 + +- Add success/failure callback hooks to Authenticator ([#527](https://github.com/lejard-h/chopper/pull/527)) +- Add mock mixins of Chopper components ([#529](https://github.com/lejard-h/chopper/pull/529)) + ## 7.0.8 - Encode DateTime query parameters in ISO8601 format ([#516](https://github.com/lejard-h/chopper/pull/516)) diff --git a/chopper/pubspec.yaml b/chopper/pubspec.yaml index 46e34310..a6a4c0d0 100644 --- a/chopper/pubspec.yaml +++ b/chopper/pubspec.yaml @@ -1,6 +1,6 @@ name: chopper description: Chopper is an http client generator using source_gen, inspired by Retrofit -version: 7.0.8 +version: 7.0.9 documentation: https://hadrien-lejard.gitbook.io/chopper repository: https://github.com/lejard-h/chopper From eb3f6c936ac339025e358eb9004851db88f45bdd Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Thu, 16 Nov 2023 08:57:44 +0000 Subject: [PATCH 119/171] :lipstick: cosmetic refactor (#534) --- chopper/example/definition.chopper.dart | 3 +- chopper/test/test_service.chopper.dart | 3 +- .../test/test_service_base_url.chopper.dart | 3 +- .../test/test_service_variable.chopper.dart | 3 +- chopper_generator/lib/src/generator.dart | 170 ++++++++++-------- .../test/test_service.chopper.dart | 3 +- .../test/test_service_variable.chopper.dart | 3 +- 7 files changed, 106 insertions(+), 82 deletions(-) diff --git a/chopper/example/definition.chopper.dart b/chopper/example/definition.chopper.dart index 577547fe..4ee6e510 100644 --- a/chopper/example/definition.chopper.dart +++ b/chopper/example/definition.chopper.dart @@ -6,6 +6,7 @@ part of 'definition.dart'; // ChopperGenerator // ************************************************************************** +// coverage:ignore-file // ignore_for_file: type=lint final class _$MyService extends MyService { _$MyService([ChopperClient? client]) { @@ -14,7 +15,7 @@ final class _$MyService extends MyService { } @override - final definitionType = MyService; + final Type definitionType = MyService; @override Future> getResource(String id) { diff --git a/chopper/test/test_service.chopper.dart b/chopper/test/test_service.chopper.dart index 028a7310..491d718c 100644 --- a/chopper/test/test_service.chopper.dart +++ b/chopper/test/test_service.chopper.dart @@ -6,6 +6,7 @@ part of 'test_service.dart'; // ChopperGenerator // ************************************************************************** +// coverage:ignore-file // ignore_for_file: type=lint final class _$HttpTestService extends HttpTestService { _$HttpTestService([ChopperClient? client]) { @@ -14,7 +15,7 @@ final class _$HttpTestService extends HttpTestService { } @override - final definitionType = HttpTestService; + final Type definitionType = HttpTestService; @override Future> getTest( diff --git a/chopper/test/test_service_base_url.chopper.dart b/chopper/test/test_service_base_url.chopper.dart index 1b77e188..6b859b03 100644 --- a/chopper/test/test_service_base_url.chopper.dart +++ b/chopper/test/test_service_base_url.chopper.dart @@ -6,6 +6,7 @@ part of 'test_service_base_url.dart'; // ChopperGenerator // ************************************************************************** +// coverage:ignore-file // ignore_for_file: type=lint final class _$HttpTestServiceBaseUrl extends HttpTestServiceBaseUrl { _$HttpTestServiceBaseUrl([ChopperClient? client]) { @@ -14,7 +15,7 @@ final class _$HttpTestServiceBaseUrl extends HttpTestServiceBaseUrl { } @override - final definitionType = HttpTestServiceBaseUrl; + final Type definitionType = HttpTestServiceBaseUrl; @override Future> getAll() { diff --git a/chopper/test/test_service_variable.chopper.dart b/chopper/test/test_service_variable.chopper.dart index 1f709494..e3611022 100644 --- a/chopper/test/test_service_variable.chopper.dart +++ b/chopper/test/test_service_variable.chopper.dart @@ -6,6 +6,7 @@ part of 'test_service_variable.dart'; // ChopperGenerator // ************************************************************************** +// coverage:ignore-file // ignore_for_file: type=lint final class _$HttpTestServiceVariable extends HttpTestServiceVariable { _$HttpTestServiceVariable([ChopperClient? client]) { @@ -14,7 +15,7 @@ final class _$HttpTestServiceVariable extends HttpTestServiceVariable { } @override - final definitionType = HttpTestServiceVariable; + final Type definitionType = HttpTestServiceVariable; @override Future> getTest( diff --git a/chopper_generator/lib/src/generator.dart b/chopper_generator/lib/src/generator.dart index 007649bf..b8707caa 100644 --- a/chopper_generator/lib/src/generator.dart +++ b/chopper_generator/lib/src/generator.dart @@ -44,6 +44,7 @@ final class ChopperGenerator static Field _buildDefinitionTypeMethod(String superType) => Field( (method) => method ..annotations.add(refer('override')) + ..type = refer('Type') ..name = 'definitionType' ..modifier = FieldModifier.final$ ..assignment = Code(superType), @@ -69,12 +70,13 @@ final class ChopperGenerator TopLevelVariableElement? baseUrlVariableElement; - final VariableElement? posibleBaseUrl = baseUrlReader?.objectValue.variable; + final VariableElement? possibleBaseUrl = + baseUrlReader?.objectValue.variable; - if (posibleBaseUrl is TopLevelVariableElement && - posibleBaseUrl.type.isDartCoreString && - posibleBaseUrl.isConst) { - baseUrlVariableElement = posibleBaseUrl; + if (possibleBaseUrl is TopLevelVariableElement && + possibleBaseUrl.type.isDartCoreString && + possibleBaseUrl.isConst) { + baseUrlVariableElement = possibleBaseUrl; } final String baseUrl = baseUrlReader?.stringValue ?? ''; @@ -93,26 +95,32 @@ final class ChopperGenerator )); }); - const String ignore = '// ignore_for_file: type=lint'; - final DartEmitter emitter = DartEmitter(); + const String ignore = '// coverage:ignore-file\n' + '// ignore_for_file: type=lint'; + final DartEmitter emitter = DartEmitter(useNullSafetySyntax: true); return DartFormatter().format('$ignore\n${classBuilder.accept(emitter)}'); } static Constructor _generateConstructor() => Constructor( - (ConstructorBuilder constructorBuilder) { - constructorBuilder.optionalParameters.add( - Parameter((paramBuilder) { - paramBuilder.name = Vars.client.toString(); - paramBuilder.type = refer('${chopper.ChopperClient}?'); - }), - ); - - constructorBuilder.body = Code( - 'if (${Vars.client} == null) return;\n' - 'this.${Vars.client} = ${Vars.client};', - ); - }, + (ConstructorBuilder constructorBuilder) => constructorBuilder + ..optionalParameters.add( + Parameter( + (paramBuilder) => paramBuilder + ..name = Vars.client.toString() + ..type = TypeReference( + (typeBuilder) => typeBuilder + ..symbol = '${chopper.ChopperClient}' + ..isNullable = true, + ), + ), + ) + ..body = Code( + [ + 'if (${Vars.client} == null) return;', + 'this.${Vars.client} = ${Vars.client};' + ].join('\n'), + ), ); static Iterable _parseMethods( @@ -175,37 +183,33 @@ final class ChopperGenerator final DartType? responseInnerType = _getResponseInnerType(m.returnType) ?? responseType; - return Method((MethodBuilder b) { - b.annotations.add(refer('override')); - b.name = m.displayName; - - /// We don't support returning null Type - b.returns = Reference( - m.returnType.getDisplayString(withNullability: false), - ); - - /// And null Typed parameters - b.types.addAll( - m.typeParameters.map( - (t) => Reference(t.getDisplayString(withNullability: false)), - ), - ); - - b.requiredParameters.addAll( - m.parameters - .where((p) => p.isRequiredPositional) - .map(Utils.buildRequiredPositionalParam), - ); - - b.optionalParameters.addAll( - m.parameters - .where((p) => p.isOptionalPositional) - .map(Utils.buildOptionalPositionalParam), - ); - - b.optionalParameters.addAll( - m.parameters.where((p) => p.isNamed).map(Utils.buildNamedParam), - ); + return Method((MethodBuilder methodBuilder) { + methodBuilder + ..annotations.add(refer('override')) + ..name = m.displayName + // We don't support returning null Type + ..returns = refer( + m.returnType.getDisplayString(withNullability: false), + ) + // And null Typed parameters + ..types.addAll( + m.typeParameters.map( + (t) => refer(t.getDisplayString(withNullability: false)), + ), + ) + ..requiredParameters.addAll( + m.parameters + .where((p) => p.isRequiredPositional) + .map(Utils.buildRequiredPositionalParam), + ) + ..optionalParameters.addAll( + m.parameters + .where((p) => p.isOptionalPositional) + .map(Utils.buildOptionalPositionalParam), + ) + ..optionalParameters.addAll( + m.parameters.where((p) => p.isNamed).map(Utils.buildNamedParam), + ); final List blocks = [ declareFinal(Vars.url.toString(), type: refer('Uri')) @@ -231,14 +235,17 @@ final class ChopperGenerator final bool hasQueryMap = queryMap.isNotEmpty; if (hasQueryMap) { if (queries.isNotEmpty) { - blocks.add(refer('${Vars.parameters}.addAll').call( - [ - /// Check if the parameter is nullable - optionalNullableParameters.contains(queryMap.keys.first) - ? refer(queryMap.keys.first).ifNullThen(refer('const {}')) - : refer(queryMap.keys.first), - ], - ).statement); + blocks.add( + refer(Vars.parameters.toString()).property('addAll').call( + [ + /// Check if the parameter is nullable + if (optionalNullableParameters.contains(queryMap.keys.first)) + refer(queryMap.keys.first).ifNullThen(literalConstMap({})) + else + refer(queryMap.keys.first), + ], + ).statement, + ); } else { blocks.add( declareFinal( @@ -248,7 +255,8 @@ final class ChopperGenerator .assign( /// Check if the parameter is nullable optionalNullableParameters.contains(queryMap.keys.first) - ? refer(queryMap.keys.first).ifNullThen(refer('const {}')) + ? refer(queryMap.keys.first) + .ifNullThen(literalConstMap({})) : refer(queryMap.keys.first), ) .statement, @@ -285,9 +293,11 @@ final class ChopperGenerator final bool hasFieldMap = fieldMap.isNotEmpty; if (hasFieldMap) { if (hasBody) { - blocks.add(refer('${Vars.body}.addAll').call( - [refer(fieldMap.keys.first)], - ).statement); + blocks.add( + refer(Vars.body.toString()).property('addAll').call( + [refer(fieldMap.keys.first)], + ).statement, + ); } else { blocks.add( declareFinal(Vars.body.toString()) @@ -312,7 +322,7 @@ final class ChopperGenerator if (hasPartMap) { if (hasParts) { blocks.add( - refer('${Vars.parts}.addAll').call( + refer(Vars.parts.toString()).property('addAll').call( [refer(partMap.keys.first)], ).statement, ); @@ -329,7 +339,7 @@ final class ChopperGenerator if (hasFileFilesMap) { if (hasParts || hasPartMap) { blocks.add( - refer('${Vars.parts}.addAll').call( + refer(Vars.parts.toString()).property('addAll').call( [refer(fileFieldMap.keys.first)], ).statement, ); @@ -394,19 +404,25 @@ final class ChopperGenerator final List typeArguments = []; if (responseType != null) { - typeArguments - .add(refer(responseType.getDisplayString(withNullability: false))); - typeArguments.add( + typeArguments.addAll([ + refer(responseType.getDisplayString(withNullability: false)), refer(responseInnerType!.getDisplayString(withNullability: false)), - ); + ]); } - blocks.add(refer('${Vars.client}.send') - .call([refer(Vars.request.toString())], namedArguments, typeArguments) - .returned - .statement); + blocks.add( + refer(Vars.client.toString()) + .property('send') + .call( + [refer(Vars.request.toString())], + namedArguments, + typeArguments, + ) + .returned + .statement, + ); - b.body = Block.of(blocks); + methodBuilder.body = Block.of(blocks); }); } @@ -414,7 +430,9 @@ final class ChopperGenerator // ignore: deprecated_member_use function.enclosingElement is ClassElement // ignore: deprecated_member_use - ? '${function.enclosingElement!.name}.${function.name}' + ? refer(function.enclosingElement!.name!) + .property(function.name!) + .toString() : function.name!; static Map _getAnnotation( @@ -584,7 +602,7 @@ final class ChopperGenerator [ literal(Utils.getMethodName(method)), refer(Vars.url.toString()), - refer('${Vars.client}.${Vars.baseUrl}'), + refer(Vars.client.toString()).property(Vars.baseUrl.toString()), ], { if (hasBody) 'body': refer(Vars.body.toString()), diff --git a/chopper_generator/test/test_service.chopper.dart b/chopper_generator/test/test_service.chopper.dart index f070c8a8..2474ce6f 100644 --- a/chopper_generator/test/test_service.chopper.dart +++ b/chopper_generator/test/test_service.chopper.dart @@ -6,6 +6,7 @@ part of 'test_service.dart'; // ChopperGenerator // ************************************************************************** +// coverage:ignore-file // ignore_for_file: type=lint final class _$HttpTestService extends HttpTestService { _$HttpTestService([ChopperClient? client]) { @@ -14,7 +15,7 @@ final class _$HttpTestService extends HttpTestService { } @override - final definitionType = HttpTestService; + final Type definitionType = HttpTestService; @override Future> getTest( diff --git a/chopper_generator/test/test_service_variable.chopper.dart b/chopper_generator/test/test_service_variable.chopper.dart index 1f709494..e3611022 100644 --- a/chopper_generator/test/test_service_variable.chopper.dart +++ b/chopper_generator/test/test_service_variable.chopper.dart @@ -6,6 +6,7 @@ part of 'test_service_variable.dart'; // ChopperGenerator // ************************************************************************** +// coverage:ignore-file // ignore_for_file: type=lint final class _$HttpTestServiceVariable extends HttpTestServiceVariable { _$HttpTestServiceVariable([ChopperClient? client]) { @@ -14,7 +15,7 @@ final class _$HttpTestServiceVariable extends HttpTestServiceVariable { } @override - final definitionType = HttpTestServiceVariable; + final Type definitionType = HttpTestServiceVariable; @override Future> getTest( From 33cb73ac82db438e6e00ce0bc5af0b7c214b98a5 Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Sat, 18 Nov 2023 20:47:31 +0000 Subject: [PATCH 120/171] :construction_worker: update CI (#535) --- .github/workflows/dart.yml | 4 ++-- .github/workflows/publish.yml | 4 ++-- .github/workflows/publish_dry_run.yml | 4 ++-- tool/ci.sh | 18 +++++++----------- 4 files changed, 13 insertions(+), 17 deletions(-) diff --git a/.github/workflows/dart.yml b/.github/workflows/dart.yml index e74815fe..91f883dd 100644 --- a/.github/workflows/dart.yml +++ b/.github/workflows/dart.yml @@ -1,4 +1,4 @@ -# Created with package:mono_repo v6.6.0 +# Created with package:mono_repo v6.6.1 name: Dart CI on: push: @@ -37,7 +37,7 @@ jobs: name: Checkout repository uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab - name: mono_repo self validate - run: dart pub global activate mono_repo 6.6.0 + run: dart pub global activate mono_repo 6.6.1 - name: mono_repo self validate run: dart pub global run mono_repo generate --validate job_002: diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 8d6278d2..f7835d30 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -27,7 +27,7 @@ jobs: with: sdk: stable - id: checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: fetch-depth: 2 - run: git checkout HEAD^ @@ -53,7 +53,7 @@ jobs: with: sdk: stable - id: checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Load this version id: load_this_version working-directory: ${{ matrix.package }} diff --git a/.github/workflows/publish_dry_run.yml b/.github/workflows/publish_dry_run.yml index bbd60cc2..8a23a866 100644 --- a/.github/workflows/publish_dry_run.yml +++ b/.github/workflows/publish_dry_run.yml @@ -27,7 +27,7 @@ jobs: with: sdk: stable - id: checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: ref: ${{ github.event.pull_request.base.ref }} - name: Load base version @@ -50,7 +50,7 @@ jobs: with: sdk: stable - id: checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Load this version id: load_this_version working-directory: ${{ matrix.package }} diff --git a/tool/ci.sh b/tool/ci.sh index 0af37047..cc85b15d 100755 --- a/tool/ci.sh +++ b/tool/ci.sh @@ -1,9 +1,10 @@ #!/bin/bash -# Created with package:mono_repo v6.6.0 +# Created with package:mono_repo v6.6.1 # Support built in commands on windows out of the box. + # When it is a flutter repo (check the pubspec.yaml for "sdk: flutter") -# then "flutter" is called instead of "pub". +# then "flutter pub" is called instead of "dart pub". # This assumes that the Flutter SDK has been installed in a previous step. function pub() { if grep -Fq "sdk: flutter" "${PWD}/pubspec.yaml"; then @@ -12,18 +13,13 @@ function pub() { command dart pub "$@" fi } -# When it is a flutter repo (check the pubspec.yaml for "sdk: flutter") -# then "flutter" is called instead of "pub". -# This assumes that the Flutter SDK has been installed in a previous step. + function format() { - if grep -Fq "sdk: flutter" "${PWD}/pubspec.yaml"; then - command flutter format "$@" - else - command dart format "$@" - fi + command dart format "$@" } + # When it is a flutter repo (check the pubspec.yaml for "sdk: flutter") -# then "flutter" is called instead of "pub". +# then "flutter analyze" is called instead of "dart analyze". # This assumes that the Flutter SDK has been installed in a previous step. function analyze() { if grep -Fq "sdk: flutter" "${PWD}/pubspec.yaml"; then From e84d077cfb6d8805dc9b113343cdab33dee8bdbe Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Sat, 30 Dec 2023 09:19:17 +0100 Subject: [PATCH 121/171] :sparkles: call .toString() on non-String headers (#538) --- chopper/test/base_test.dart | 33 +++++++++++++++++++ chopper/test/fixtures/example_enum.dart | 8 +++++ chopper/test/test_service.chopper.dart | 25 ++++++++++++++ chopper/test/test_service.dart | 11 +++++++ chopper_generator/lib/src/generator.dart | 18 ++++++---- .../test/test_service.chopper.dart | 25 ++++++++++++++ chopper_generator/test/test_service.dart | 18 ++++++++++ 7 files changed, 132 insertions(+), 6 deletions(-) create mode 100644 chopper/test/fixtures/example_enum.dart diff --git a/chopper/test/base_test.dart b/chopper/test/base_test.dart index fe22ee71..8de582a8 100644 --- a/chopper/test/base_test.dart +++ b/chopper/test/base_test.dart @@ -9,6 +9,7 @@ import 'package:http/testing.dart'; import 'package:test/test.dart'; import 'package:transparent_image/transparent_image.dart'; +import 'fixtures/example_enum.dart'; import 'test_service.dart'; import 'test_service_base_url.dart'; import 'test_service_variable.dart'; @@ -1616,4 +1617,36 @@ void main() { httpClient.close(); }); + + test('headers are always stringified', () async { + final httpClient = MockClient((request) async { + expect( + request.url.toString(), + equals('$baseUrl/test/headers'), + ); + expect(request.method, equals('GET')); + expect(request.headers['x-string'], equals('lorem')); + expect(request.headers['x-boolean'], equals('true')); + expect(request.headers['x-int'], equals('42')); + expect(request.headers['x-double'], equals('42.42')); + expect(request.headers['x-enum'], equals('baz')); + + return http.Response('get response', 200); + }); + + final chopper = buildClient(httpClient, JsonConverter()); + final service = chopper.getService(); + + final response = await service.getHeaders( + stringHeader: 'lorem', + boolHeader: true, + intHeader: 42, + doubleHeader: 42.42, + enumHeader: ExampleEnum.baz, + ); + + expect(response.statusCode, equals(200)); + + httpClient.close(); + }); } diff --git a/chopper/test/fixtures/example_enum.dart b/chopper/test/fixtures/example_enum.dart new file mode 100644 index 00000000..ca7be351 --- /dev/null +++ b/chopper/test/fixtures/example_enum.dart @@ -0,0 +1,8 @@ +enum ExampleEnum { + foo, + bar, + baz; + + @override + String toString() => name; +} diff --git a/chopper/test/test_service.chopper.dart b/chopper/test/test_service.chopper.dart index 491d718c..0db60af9 100644 --- a/chopper/test/test_service.chopper.dart +++ b/chopper/test/test_service.chopper.dart @@ -650,4 +650,29 @@ final class _$HttpTestService extends HttpTestService { ); return client.send($request); } + + @override + Future> getHeaders({ + required String stringHeader, + bool? boolHeader, + int? intHeader, + double? doubleHeader, + ExampleEnum? enumHeader, + }) { + final Uri $url = Uri.parse('/test/headers'); + final Map $headers = { + 'x-string': stringHeader, + if (boolHeader != null) 'x-boolean': boolHeader.toString(), + if (intHeader != null) 'x-int': intHeader.toString(), + if (doubleHeader != null) 'x-double': doubleHeader.toString(), + if (enumHeader != null) 'x-enum': enumHeader.toString(), + }; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + headers: $headers, + ); + return client.send($request); + } } diff --git a/chopper/test/test_service.dart b/chopper/test/test_service.dart index f1895716..1ed74368 100644 --- a/chopper/test/test_service.dart +++ b/chopper/test/test_service.dart @@ -4,6 +4,8 @@ import 'dart:convert'; import 'package:chopper/chopper.dart'; import 'package:http/http.dart' show MultipartFile; +import 'fixtures/example_enum.dart'; + part 'test_service.chopper.dart'; @ChopperApi(baseUrl: '/test') @@ -193,6 +195,15 @@ abstract class HttpTestService extends ChopperService { Future> getDateTime( @Query('value') DateTime value, ); + + @Get(path: 'headers') + Future> getHeaders({ + @Header('x-string') required String stringHeader, + @Header('x-boolean') bool? boolHeader, + @Header('x-int') int? intHeader, + @Header('x-double') double? doubleHeader, + @Header('x-enum') ExampleEnum? enumHeader, + }); } Request customConvertRequest(Request req) { diff --git a/chopper_generator/lib/src/generator.dart b/chopper_generator/lib/src/generator.dart index b8707caa..915b49f8 100644 --- a/chopper_generator/lib/src/generator.dart +++ b/chopper_generator/lib/src/generator.dart @@ -676,18 +676,24 @@ final class ChopperGenerator ) { final StringBuffer codeBuffer = StringBuffer('')..writeln('{'); - /// Search for @Header anotation in method parameters + /// Search for @Header annotation in method parameters final Map annotations = _getAnnotations(methodElement, chopper.Header); - annotations.forEach((parameter, ConstantReader annotation) { + annotations.forEach(( + ParameterElement parameter, + ConstantReader annotation, + ) { final String paramName = parameter.displayName; final String name = annotation.peek('name')?.stringValue ?? paramName; - + final String headerValue = switch (parameter.type.isDartCoreString) { + true => "'$name': $paramName,", + false => "'$name': $paramName.toString(),", + }; if (parameter.type.isNullable) { - codeBuffer.writeln('if ($paramName != null) \'$name\': $paramName,'); + codeBuffer.writeln('if ($paramName != null) $headerValue'); } else { - codeBuffer.writeln('\'$name\': $paramName,'); + codeBuffer.writeln(headerValue); } }); @@ -700,7 +706,7 @@ final class ChopperGenerator methodAnnotations.forEach((headerName, headerValue) { if (headerName != null && headerValue != null) { codeBuffer.writeln( - '\'${headerName.toStringValue()}\': ${literal(headerValue.toStringValue())},', + "'${headerName.toStringValue()}': ${literal(headerValue.toStringValue())},", ); } }); diff --git a/chopper_generator/test/test_service.chopper.dart b/chopper_generator/test/test_service.chopper.dart index 2474ce6f..b94c2e9b 100644 --- a/chopper_generator/test/test_service.chopper.dart +++ b/chopper_generator/test/test_service.chopper.dart @@ -637,4 +637,29 @@ final class _$HttpTestService extends HttpTestService { ); return client.send($request); } + + @override + Future> getHeaders({ + required String stringHeader, + bool? boolHeader, + int? intHeader, + double? doubleHeader, + ExampleEnum? enumHeader, + }) { + final Uri $url = Uri.parse('/test/headers'); + final Map $headers = { + 'x-string': stringHeader, + if (boolHeader != null) 'x-boolean': boolHeader.toString(), + if (intHeader != null) 'x-int': intHeader.toString(), + if (doubleHeader != null) 'x-double': doubleHeader.toString(), + if (enumHeader != null) 'x-enum': enumHeader.toString(), + }; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + headers: $headers, + ); + return client.send($request); + } } diff --git a/chopper_generator/test/test_service.dart b/chopper_generator/test/test_service.dart index c0baf1a4..30d6a451 100644 --- a/chopper_generator/test/test_service.dart +++ b/chopper_generator/test/test_service.dart @@ -188,6 +188,15 @@ abstract class HttpTestService extends ChopperService { Future> getUsingMapQueryParamWithBrackets( @Query('value') Map value, ); + + @Get(path: 'headers') + Future> getHeaders({ + @Header('x-string') required String stringHeader, + @Header('x-boolean') bool? boolHeader, + @Header('x-int') int? intHeader, + @Header('x-double') double? doubleHeader, + @Header('x-enum') ExampleEnum? enumHeader, + }); } Request customConvertRequest(Request req) { @@ -216,3 +225,12 @@ Request convertForm(Request req) { return req; } + +enum ExampleEnum { + foo, + bar, + baz; + + @override + String toString() => name; +} From 8938d985f368d8364a75205adda4c48cfe56b753 Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Tue, 2 Jan 2024 08:56:51 +0100 Subject: [PATCH 122/171] :twisted_rightwards_arrows: post release branch sync (#542) --- chopper/CHANGELOG.md | 4 ++++ chopper/pubspec.yaml | 2 +- chopper_generator/CHANGELOG.md | 4 ++++ chopper_generator/pubspec.yaml | 2 +- 4 files changed, 10 insertions(+), 2 deletions(-) diff --git a/chopper/CHANGELOG.md b/chopper/CHANGELOG.md index 5f5b19cc..b15c91b9 100644 --- a/chopper/CHANGELOG.md +++ b/chopper/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 7.0.10 + +- Enable the user to specify non-String type header values by calling `.toString()` on any non-String Dart type. ([#538](https://github.com/lejard-h/chopper/pull/538)) + ## 7.0.9 - Add success/failure callback hooks to Authenticator ([#527](https://github.com/lejard-h/chopper/pull/527)) diff --git a/chopper/pubspec.yaml b/chopper/pubspec.yaml index a6a4c0d0..9bd3a192 100644 --- a/chopper/pubspec.yaml +++ b/chopper/pubspec.yaml @@ -1,6 +1,6 @@ name: chopper description: Chopper is an http client generator using source_gen, inspired by Retrofit -version: 7.0.9 +version: 7.0.10 documentation: https://hadrien-lejard.gitbook.io/chopper repository: https://github.com/lejard-h/chopper diff --git a/chopper_generator/CHANGELOG.md b/chopper_generator/CHANGELOG.md index e65b713a..2d30f9e0 100644 --- a/chopper_generator/CHANGELOG.md +++ b/chopper_generator/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 7.0.7 + +- Enable the user to specify non-String type header values by calling `.toString()` on any non-String Dart type. ([#538](https://github.com/lejard-h/chopper/pull/538)) + ## 7.0.6 - Fix incorrect url generation when using new baseUrl ([#520](https://github.com/lejard-h/chopper/pull/520)) diff --git a/chopper_generator/pubspec.yaml b/chopper_generator/pubspec.yaml index bb06b46d..6ee77f1c 100644 --- a/chopper_generator/pubspec.yaml +++ b/chopper_generator/pubspec.yaml @@ -1,6 +1,6 @@ name: chopper_generator description: Chopper is an http client generator using source_gen, inspired by Retrofit -version: 7.0.6 +version: 7.0.7 documentation: https://hadrien-lejard.gitbook.io/chopper repository: https://github.com/lejard-h/chopper From 4ba5a83654388a33f70cacf7d8619880cd573523 Mon Sep 17 00:00:00 2001 From: Job Guldemeester Date: Tue, 2 Jan 2024 11:21:18 +0100 Subject: [PATCH 123/171] Helper function for fetching errors of specific type (#543) --- chopper/lib/src/response.dart | 9 +++++ chopper/test/base_test.dart | 49 +++++++++++++++++++++++ chopper/test/fixtures/error_fixtures.dart | 3 ++ 3 files changed, 61 insertions(+) create mode 100644 chopper/test/fixtures/error_fixtures.dart diff --git a/chopper/lib/src/response.dart b/chopper/lib/src/response.dart index 92d1a8bb..1dd2d7ab 100644 --- a/chopper/lib/src/response.dart +++ b/chopper/lib/src/response.dart @@ -67,6 +67,15 @@ base class Response with EquatableMixin { String get bodyString => base is http.Response ? (base as http.Response).body : ''; + /// Check if the response is an error and if the error is of type [ErrorType] and casts the error to [ErrorType]. Otherwise it returns null. + ErrorType? errorWhereType() { + if (error != null && error is ErrorType) { + return error as ErrorType; + } else { + return null; + } + } + @override List get props => [ base, diff --git a/chopper/test/base_test.dart b/chopper/test/base_test.dart index 8de582a8..597bf952 100644 --- a/chopper/test/base_test.dart +++ b/chopper/test/base_test.dart @@ -9,6 +9,7 @@ import 'package:http/testing.dart'; import 'package:test/test.dart'; import 'package:transparent_image/transparent_image.dart'; +import 'fixtures/error_fixtures.dart'; import 'fixtures/example_enum.dart'; import 'test_service.dart'; import 'test_service_base_url.dart'; @@ -1649,4 +1650,52 @@ void main() { httpClient.close(); }); + + group('Response error casting test', () { + test('Response is succesfull, [returns null]', () { + final base = http.Response('Foobar', 200); + + final response = Response(base, 'Foobar'); + + final result = response.errorWhereType(); + + expect(result, isNull); + }); + + test('Response is unsuccessful and has no error object, [returns null]', + () { + final base = http.Response('Foobar', 400); + + final response = Response(base, ''); + + final result = response.errorWhereType(); + + expect(result, isNull); + }); + + test( + 'Response is unsuccessful and has error object of different type, [returns null]', + () { + final base = http.Response('Foobar', 400); + + final response = Response(base, '', error: 'Foobar'); + + final result = response.errorWhereType(); + + expect(result, isNull); + }); + + test( + 'Response is unsuccessful and has error object of specified type, [returns error as ErrorType]', + () { + final base = http.Response('Foobar', 400); + + final response = Response(base, 'Foobar', error: FooErrorType()); + + final result = response.errorWhereType(); + + expect(result, isNotNull); + expect(result, isA()); + }); + }); } diff --git a/chopper/test/fixtures/error_fixtures.dart b/chopper/test/fixtures/error_fixtures.dart new file mode 100644 index 00000000..2c728333 --- /dev/null +++ b/chopper/test/fixtures/error_fixtures.dart @@ -0,0 +1,3 @@ +class FooErrorType { + const FooErrorType(); +} From 6420778134914a428bfefc08811434c99f6f3437 Mon Sep 17 00:00:00 2001 From: Job Guldemeester Date: Fri, 5 Jan 2024 09:15:04 +0100 Subject: [PATCH 124/171] :sparkles: Feature: Omit Response in service (#545) --- chopper/lib/src/chopper_http_exception.dart | 13 + chopper/lib/src/response.dart | 15 + chopper/test/base_test.dart | 49 -- chopper/test/chopper_http_exception_test.dart | 21 + chopper/test/ensure_build_test.dart | 1 + chopper/test/response_test.dart | 114 +++ chopper/test/test_service.chopper.dart | 4 +- chopper/test/test_service.dart | 2 +- .../test/test_service_variable.chopper.dart | 4 +- chopper/test/test_service_variable.dart | 2 +- ...test_without_response_service.chopper.dart | 705 ++++++++++++++++++ .../test/test_without_response_service.dart | 236 ++++++ chopper_generator/lib/src/generator.dart | 79 +- chopper_generator/lib/src/vars.dart | 1 + chopper_generator/test/ensure_build_test.dart | 11 +- .../test/test_service.chopper.dart | 4 +- chopper_generator/test/test_service.dart | 2 +- .../test/test_service_variable.chopper.dart | 4 +- .../test/test_service_variable.dart | 2 +- ...test_without_response_service.chopper.dart | 705 ++++++++++++++++++ .../test/test_without_response_service.dart | 236 ++++++ 21 files changed, 2125 insertions(+), 85 deletions(-) create mode 100644 chopper/lib/src/chopper_http_exception.dart create mode 100644 chopper/test/chopper_http_exception_test.dart create mode 100644 chopper/test/response_test.dart create mode 100644 chopper/test/test_without_response_service.chopper.dart create mode 100644 chopper/test/test_without_response_service.dart create mode 100644 chopper_generator/test/test_without_response_service.chopper.dart create mode 100644 chopper_generator/test/test_without_response_service.dart diff --git a/chopper/lib/src/chopper_http_exception.dart b/chopper/lib/src/chopper_http_exception.dart new file mode 100644 index 00000000..cae57ce2 --- /dev/null +++ b/chopper/lib/src/chopper_http_exception.dart @@ -0,0 +1,13 @@ +import 'package:chopper/src/response.dart'; + +/// An exception thrown when a [Response] is unsuccessful < 200 or > 300. +class ChopperHttpException implements Exception { + ChopperHttpException(this.response); + + final Response response; + + @override + String toString() { + return 'Could not fetch the response for ${response.base.request}. Status code: ${response.statusCode}, error: ${response.error}'; + } +} diff --git a/chopper/lib/src/response.dart b/chopper/lib/src/response.dart index 1dd2d7ab..e5a18d4e 100644 --- a/chopper/lib/src/response.dart +++ b/chopper/lib/src/response.dart @@ -1,5 +1,6 @@ import 'dart:typed_data'; +import 'package:chopper/src/chopper_http_exception.dart'; import 'package:equatable/equatable.dart' show EquatableMixin; import 'package:http/http.dart' as http; import 'package:meta/meta.dart'; @@ -76,6 +77,20 @@ base class Response with EquatableMixin { } } + /// Returns the response body if [Response] [isSuccessful] and [body] is not null. + /// Otherwise it throws an [HttpException] with the response status code and error object. + /// If the error object is an [Exception], it will be thrown instead. + BodyType get bodyOrThrow { + if (isSuccessful && body != null) { + return body!; + } else { + if (error is Exception) { + throw error!; + } + throw ChopperHttpException(this); + } + } + @override List get props => [ base, diff --git a/chopper/test/base_test.dart b/chopper/test/base_test.dart index 597bf952..8de582a8 100644 --- a/chopper/test/base_test.dart +++ b/chopper/test/base_test.dart @@ -9,7 +9,6 @@ import 'package:http/testing.dart'; import 'package:test/test.dart'; import 'package:transparent_image/transparent_image.dart'; -import 'fixtures/error_fixtures.dart'; import 'fixtures/example_enum.dart'; import 'test_service.dart'; import 'test_service_base_url.dart'; @@ -1650,52 +1649,4 @@ void main() { httpClient.close(); }); - - group('Response error casting test', () { - test('Response is succesfull, [returns null]', () { - final base = http.Response('Foobar', 200); - - final response = Response(base, 'Foobar'); - - final result = response.errorWhereType(); - - expect(result, isNull); - }); - - test('Response is unsuccessful and has no error object, [returns null]', - () { - final base = http.Response('Foobar', 400); - - final response = Response(base, ''); - - final result = response.errorWhereType(); - - expect(result, isNull); - }); - - test( - 'Response is unsuccessful and has error object of different type, [returns null]', - () { - final base = http.Response('Foobar', 400); - - final response = Response(base, '', error: 'Foobar'); - - final result = response.errorWhereType(); - - expect(result, isNull); - }); - - test( - 'Response is unsuccessful and has error object of specified type, [returns error as ErrorType]', - () { - final base = http.Response('Foobar', 400); - - final response = Response(base, 'Foobar', error: FooErrorType()); - - final result = response.errorWhereType(); - - expect(result, isNotNull); - expect(result, isA()); - }); - }); } diff --git a/chopper/test/chopper_http_exception_test.dart b/chopper/test/chopper_http_exception_test.dart new file mode 100644 index 00000000..37417e7a --- /dev/null +++ b/chopper/test/chopper_http_exception_test.dart @@ -0,0 +1,21 @@ +import 'package:chopper/src/chopper_http_exception.dart'; +import 'package:chopper/src/response.dart'; +import 'package:http/http.dart' as http; +import 'package:test/test.dart'; + +void main() { + test('ChopperHttpException toString prints available information', () { + final request = http.Request('GET', Uri.parse('http://localhost:8000')); + final base = http.Response('Foobar', 400, request: request); + final response = Response(base, 'Foobar', error: 'FooError'); + + final exception = ChopperHttpException(response); + + final result = exception.toString(); + + expect( + result, + 'Could not fetch the response for GET http://localhost:8000. Status code: 400, error: FooError', + ); + }); +} diff --git a/chopper/test/ensure_build_test.dart b/chopper/test/ensure_build_test.dart index 0cf64d5f..77fb9a96 100644 --- a/chopper/test/ensure_build_test.dart +++ b/chopper/test/ensure_build_test.dart @@ -12,6 +12,7 @@ void main() { gitDiffPathArguments: [ 'test/test_service.chopper.dart', 'test/test_service_variable.chopper.dart', + 'test/test_without_response_service.chopper.dart', 'test/test_service_base_url.chopper.dart', ], ); diff --git a/chopper/test/response_test.dart b/chopper/test/response_test.dart new file mode 100644 index 00000000..201d3cb4 --- /dev/null +++ b/chopper/test/response_test.dart @@ -0,0 +1,114 @@ +import 'package:chopper/src/chopper_http_exception.dart'; +import 'package:chopper/src/response.dart'; +import 'package:http/http.dart' as http; +import 'package:test/test.dart'; + +import 'fixtures/error_fixtures.dart'; + +void main() { + group('Response error casting test', () { + test('Response is succesfull, [returns null]', () { + final base = http.Response('Foobar', 200); + + final response = Response(base, 'Foobar'); + + final result = response.errorWhereType(); + + expect(result, isNull); + }); + + test('Response is unsuccessful and has no error object, [returns null]', + () { + final base = http.Response('Foobar', 400); + + final response = Response(base, ''); + + final result = response.errorWhereType(); + + expect(result, isNull); + }); + + test( + 'Response is unsuccessful and has error object of different type, [returns null]', + () { + final base = http.Response('Foobar', 400); + + final response = Response(base, '', error: 'Foobar'); + + final result = response.errorWhereType(); + + expect(result, isNull); + }); + + test( + 'Response is unsuccessful and has error object of specified type, [returns error as ErrorType]', + () { + final base = http.Response('Foobar', 400); + + final response = Response(base, 'Foobar', error: FooErrorType()); + + final result = response.errorWhereType(); + + expect(result, isNotNull); + expect(result, isA()); + }); + }); + + group('bodyOrThrow tests', () { + test('Response is successful and has body, [bodyOrThrow returns body]', () { + final base = http.Response('Foobar', 200); + final response = Response(base, {'Foo': 'Bar'}); + + final result = response.bodyOrThrow; + + expect(result, isNotNull); + expect(result, {'Foo': 'Bar'}); + }); + + test( + 'Response is unsuccessful and has Exception as error, [bodyOrThrow throws error]', + () { + final base = http.Response('Foobar', 400); + final response = Response(base, '', error: Exception('Error occurred')); + + expect(() => response.bodyOrThrow, throwsA(isA())); + }); + + test( + 'Response is unsuccessful and has non-exception object as error, [bodyOrThrow throws error]', + () { + final base = http.Response('Foobar', 400); + final response = Response(base, '', error: 'Error occurred'); + + expect(() => response.bodyOrThrow, throwsA(isA())); + }); + + test( + 'Response is unsuccessful and has no error, [bodyOrThrow throws ChopperHttpException]', + () { + final base = http.Response('Foobar', 400); + final response = Response(base, ''); + + expect(() => response.bodyOrThrow, throwsA(isA())); + }); + + test( + 'Response is successful and has no body, [bodyOrThrow throws ChopperHttpException]', + () { + final base = http.Response('Foobar', 200); + final Response response = Response(base, null); + + expect(() => response.bodyOrThrow, throwsA(isA())); + }); + + test('Response is successful and has void body, [bodyOrThrow returns void]', + () { + final base = http.Response('Foobar', 200); + // Ignoring void checks for testing purposes + //ignore: void_checks + final Response response = Response(base, ''); + + expect(() => response.bodyOrThrow, returnsNormally); + }); + }); +} diff --git a/chopper/test/test_service.chopper.dart b/chopper/test/test_service.chopper.dart index 0db60af9..ed9a98ed 100644 --- a/chopper/test/test_service.chopper.dart +++ b/chopper/test/test_service.chopper.dart @@ -513,14 +513,14 @@ final class _$HttpTestService extends HttpTestService { } @override - Future fullUrl() { + Future> fullUrl() { final Uri $url = Uri.parse('https://test.com'); final Request $request = Request( 'GET', $url, client.baseUrl, ); - return client.send($request); + return client.send($request); } @override diff --git a/chopper/test/test_service.dart b/chopper/test/test_service.dart index 1ed74368..ff34b785 100644 --- a/chopper/test/test_service.dart +++ b/chopper/test/test_service.dart @@ -148,7 +148,7 @@ abstract class HttpTestService extends ChopperService { }); @Get(path: 'https://test.com') - Future fullUrl(); + Future fullUrl(); @Get(path: '/list/string') Future>> listString(); diff --git a/chopper/test/test_service_variable.chopper.dart b/chopper/test/test_service_variable.chopper.dart index e3611022..9c69ffa0 100644 --- a/chopper/test/test_service_variable.chopper.dart +++ b/chopper/test/test_service_variable.chopper.dart @@ -513,14 +513,14 @@ final class _$HttpTestServiceVariable extends HttpTestServiceVariable { } @override - Future fullUrl() { + Future> fullUrl() { final Uri $url = Uri.parse('https://test.com'); final Request $request = Request( 'GET', $url, client.baseUrl, ); - return client.send($request); + return client.send($request); } @override diff --git a/chopper/test/test_service_variable.dart b/chopper/test/test_service_variable.dart index 81532976..251b48cb 100644 --- a/chopper/test/test_service_variable.dart +++ b/chopper/test/test_service_variable.dart @@ -148,7 +148,7 @@ abstract class HttpTestServiceVariable extends ChopperService { }); @Get(path: 'https://test.com') - Future fullUrl(); + Future fullUrl(); @Get(path: '/list/string') Future>> listString(); diff --git a/chopper/test/test_without_response_service.chopper.dart b/chopper/test/test_without_response_service.chopper.dart new file mode 100644 index 00000000..c7b8462a --- /dev/null +++ b/chopper/test/test_without_response_service.chopper.dart @@ -0,0 +1,705 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'test_without_response_service.dart'; + +// ************************************************************************** +// ChopperGenerator +// ************************************************************************** + +// coverage:ignore-file +// ignore_for_file: type=lint +final class _$HttpTestService extends HttpTestService { + _$HttpTestService([ChopperClient? client]) { + if (client == null) return; + this.client = client; + } + + @override + final Type definitionType = HttpTestService; + + @override + Future getTest( + String id, { + required String dynamicHeader, + }) async { + final Uri $url = Uri.parse('/test/get/${id}'); + final Map $headers = { + 'test': dynamicHeader, + }; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + headers: $headers, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + + @override + Future headTest() async { + final Uri $url = Uri.parse('/test/head'); + final Request $request = Request( + 'HEAD', + $url, + client.baseUrl, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + + @override + Future optionsTest() async { + final Uri $url = Uri.parse('/test/options'); + final Request $request = Request( + 'OPTIONS', + $url, + client.baseUrl, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + + @override + Future>> getStreamTest() async { + final Uri $url = Uri.parse('/test/get'); + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + ); + final Response $response = + await client.send>, int>($request); + return $response.bodyOrThrow; + } + + @override + Future getAll() async { + final Uri $url = Uri.parse('/test'); + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + + @override + Future getAllWithTrailingSlash() async { + final Uri $url = Uri.parse('/test/'); + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + + @override + Future getQueryTest({ + String name = '', + int? number, + int? def = 42, + }) async { + final Uri $url = Uri.parse('/test/query'); + final Map $params = { + 'name': name, + 'int': number, + 'default_value': def, + }; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + + @override + Future getQueryMapTest(Map query) async { + final Uri $url = Uri.parse('/test/query_map'); + final Map $params = query; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + + @override + Future getQueryMapTest2( + Map query, { + bool? test, + }) async { + final Uri $url = Uri.parse('/test/query_map'); + final Map $params = {'test': test}; + $params.addAll(query); + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + + @override + Future getQueryMapTest3({ + String name = '', + int? number, + Map filters = const {}, + }) async { + final Uri $url = Uri.parse('/test/query_map'); + final Map $params = { + 'name': name, + 'number': number, + }; + $params.addAll(filters); + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + + @override + Future getQueryMapTest4({ + String name = '', + int? number, + Map? filters, + }) async { + final Uri $url = Uri.parse('/test/query_map'); + final Map $params = { + 'name': name, + 'number': number, + }; + $params.addAll(filters ?? const {}); + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + + @override + Future getQueryMapTest5({Map? filters}) async { + final Uri $url = Uri.parse('/test/query_map'); + final Map $params = filters ?? const {}; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + + @override + Future getBody(dynamic body) async { + final Uri $url = Uri.parse('/test/get_body'); + final $body = body; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + body: $body, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + + @override + Future postTest(String data) async { + final Uri $url = Uri.parse('/test/post'); + final $body = data; + final Request $request = Request( + 'POST', + $url, + client.baseUrl, + body: $body, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + + @override + Future postStreamTest(Stream> byteStream) async { + final Uri $url = Uri.parse('/test/post'); + final $body = byteStream; + final Request $request = Request( + 'POST', + $url, + client.baseUrl, + body: $body, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + + @override + Future putTest( + String test, + String data, + ) async { + final Uri $url = Uri.parse('/test/put/${test}'); + final $body = data; + final Request $request = Request( + 'PUT', + $url, + client.baseUrl, + body: $body, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + + @override + Future deleteTest(String id) async { + final Uri $url = Uri.parse('/test/delete/${id}'); + final Map $headers = { + 'foo': 'bar', + }; + final Request $request = Request( + 'DELETE', + $url, + client.baseUrl, + headers: $headers, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + + @override + Future patchTest( + String id, + String data, + ) async { + final Uri $url = Uri.parse('/test/patch/${id}'); + final $body = data; + final Request $request = Request( + 'PATCH', + $url, + client.baseUrl, + body: $body, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + + @override + Future mapTest(Map map) async { + final Uri $url = Uri.parse('/test/map'); + final $body = map; + final Request $request = Request( + 'POST', + $url, + client.baseUrl, + body: $body, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + + @override + Future postForm(Map fields) async { + final Uri $url = Uri.parse('/test/form/body'); + final $body = fields; + final Request $request = Request( + 'POST', + $url, + client.baseUrl, + body: $body, + ); + final Response $response = await client.send( + $request, + requestConverter: convertForm, + ); + return $response.bodyOrThrow; + } + + @override + Future postFormUsingHeaders(Map fields) async { + final Uri $url = Uri.parse('/test/form/body'); + final Map $headers = { + 'content-type': 'application/x-www-form-urlencoded', + }; + final $body = fields; + final Request $request = Request( + 'POST', + $url, + client.baseUrl, + body: $body, + headers: $headers, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + + @override + Future postFormFields( + String foo, + int bar, + ) async { + final Uri $url = Uri.parse('/test/form/body/fields'); + final $body = { + 'foo': foo, + 'bar': bar, + }; + final Request $request = Request( + 'POST', + $url, + client.baseUrl, + body: $body, + ); + final Response $response = await client.send( + $request, + requestConverter: convertForm, + ); + return $response.bodyOrThrow; + } + + @override + Future forceJsonTest(Map map) async { + final Uri $url = Uri.parse('/test/map/json'); + final $body = map; + final Request $request = Request( + 'POST', + $url, + client.baseUrl, + body: $body, + ); + final Response $response = await client.send( + $request, + requestConverter: customConvertRequest, + responseConverter: customConvertResponse, + ); + return $response.bodyOrThrow; + } + + @override + Future postResources( + Map a, + Map b, + ) async { + final Uri $url = Uri.parse('/test/multi'); + final List $parts = [ + PartValue>( + '1', + a, + ), + PartValue>( + '2', + b, + ), + ]; + final Request $request = Request( + 'POST', + $url, + client.baseUrl, + parts: $parts, + multipart: true, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + + @override + Future postFile(List bytes) async { + final Uri $url = Uri.parse('/test/file'); + final List $parts = [ + PartValueFile>( + 'file', + bytes, + ) + ]; + final Request $request = Request( + 'POST', + $url, + client.baseUrl, + parts: $parts, + multipart: true, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + + @override + Future postImage(List imageData) async { + final Uri $url = Uri.parse('/test/image'); + final List $parts = [ + PartValueFile>( + 'image', + imageData, + ) + ]; + final Request $request = Request( + 'POST', + $url, + client.baseUrl, + parts: $parts, + multipart: true, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + + @override + Future postMultipartFile( + MultipartFile file, { + String? id, + }) async { + final Uri $url = Uri.parse('/test/file'); + final List $parts = [ + PartValue( + 'id', + id, + ), + PartValueFile( + 'file', + file, + ), + ]; + final Request $request = Request( + 'POST', + $url, + client.baseUrl, + parts: $parts, + multipart: true, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + + @override + Future postListFiles(List files) async { + final Uri $url = Uri.parse('/test/files'); + final List $parts = [ + PartValueFile>( + 'files', + files, + ) + ]; + final Request $request = Request( + 'POST', + $url, + client.baseUrl, + parts: $parts, + multipart: true, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + + @override + Future postMultipartList({ + required List ints, + required List doubles, + required List nums, + required List strings, + }) async { + final Uri $url = Uri.parse('/test/multipart_list'); + final List $parts = [ + PartValue>( + 'ints', + ints, + ), + PartValue>( + 'doubles', + doubles, + ), + PartValue>( + 'nums', + nums, + ), + PartValue>( + 'strings', + strings, + ), + ]; + final Request $request = Request( + 'POST', + $url, + client.baseUrl, + parts: $parts, + multipart: true, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + + @override + Future fullUrl() async { + final Uri $url = Uri.parse('https://test.com'); + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + + @override + Future> listString() async { + final Uri $url = Uri.parse('/test/list/string'); + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + ); + final Response $response = + await client.send, String>($request); + return $response.bodyOrThrow; + } + + @override + Future noBody() async { + final Uri $url = Uri.parse('/test/no-body'); + final Request $request = Request( + 'POST', + $url, + client.baseUrl, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + + @override + Future getUsingQueryParamIncludeNullQueryVars({ + String? foo, + String? bar, + String? baz, + }) async { + final Uri $url = Uri.parse('/test/query_param_include_null_query_vars'); + final Map $params = { + 'foo': foo, + 'bar': bar, + 'baz': baz, + }; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + includeNullQueryVars: true, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + + @override + Future getUsingListQueryParam(List value) async { + final Uri $url = Uri.parse('/test/list_query_param'); + final Map $params = {'value': value}; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + + @override + Future getUsingListQueryParamWithBrackets(List value) async { + final Uri $url = Uri.parse('/test/list_query_param_with_brackets'); + final Map $params = {'value': value}; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + useBrackets: true, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + + @override + Future getUsingMapQueryParam(Map value) async { + final Uri $url = Uri.parse('/test/map_query_param'); + final Map $params = {'value': value}; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + + @override + Future getUsingMapQueryParamIncludeNullQueryVars( + Map value) async { + final Uri $url = Uri.parse('/test/map_query_param_include_null_query_vars'); + final Map $params = {'value': value}; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + includeNullQueryVars: true, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + + @override + Future getUsingMapQueryParamWithBrackets( + Map value) async { + final Uri $url = Uri.parse('/test/map_query_param_with_brackets'); + final Map $params = {'value': value}; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + useBrackets: true, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + + @override + Future getHeaders({ + required String stringHeader, + bool? boolHeader, + int? intHeader, + double? doubleHeader, + ExampleEnum? enumHeader, + }) async { + final Uri $url = Uri.parse('/test/headers'); + final Map $headers = { + 'x-string': stringHeader, + if (boolHeader != null) 'x-boolean': boolHeader.toString(), + if (intHeader != null) 'x-int': intHeader.toString(), + if (doubleHeader != null) 'x-double': doubleHeader.toString(), + if (enumHeader != null) 'x-enum': enumHeader.toString(), + }; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + headers: $headers, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } +} diff --git a/chopper/test/test_without_response_service.dart b/chopper/test/test_without_response_service.dart new file mode 100644 index 00000000..d8a95417 --- /dev/null +++ b/chopper/test/test_without_response_service.dart @@ -0,0 +1,236 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:chopper/chopper.dart'; +import 'package:http/http.dart' show MultipartFile; + +part 'test_without_response_service.chopper.dart'; + +@ChopperApi(baseUrl: '/test') +abstract class HttpTestService extends ChopperService { + static HttpTestService create([ChopperClient? client]) => + _$HttpTestService(client); + + @Get(path: 'get/{id}') + Future getTest( + @Path() String id, { + @Header('test') required String dynamicHeader, + }); + + @Head(path: 'head') + Future headTest(); + + @Options(path: 'options') + Future optionsTest(); + + @Get(path: 'get') + Future>> getStreamTest(); + + @Get(path: '') + Future getAll(); + + @Get(path: '/') + Future getAllWithTrailingSlash(); + + @Get(path: 'query') + Future getQueryTest({ + @Query('name') String name = '', + @Query('int') int? number, + @Query('default_value') int? def = 42, + }); + + @Get(path: 'query_map') + Future getQueryMapTest(@QueryMap() Map query); + + @Get(path: 'query_map') + Future getQueryMapTest2( + @QueryMap() Map query, { + @Query('test') bool? test, + }); + + @Get(path: 'query_map') + Future getQueryMapTest3({ + @Query('name') String name = '', + @Query('number') int? number, + @QueryMap() Map filters = const {}, + }); + + @Get(path: 'query_map') + Future getQueryMapTest4({ + @Query('name') String name = '', + @Query('number') int? number, + @QueryMap() Map? filters, + }); + + @Get(path: 'query_map') + Future getQueryMapTest5({ + @QueryMap() Map? filters, + }); + + @Get(path: 'get_body') + Future getBody(@Body() dynamic body); + + @Post(path: 'post') + Future postTest(@Body() String data); + + @Post(path: 'post') + Future postStreamTest(@Body() Stream> byteStream); + + @Put(path: 'put/{id}') + Future putTest(@Path('id') String test, @Body() String data); + + @Delete(path: 'delete/{id}', headers: {'foo': 'bar'}) + Future deleteTest(@Path() String id); + + @Patch(path: 'patch/{id}') + Future patchTest(@Path() String id, @Body() String data); + + @Post(path: 'map') + Future mapTest(@Body() Map map); + + @FactoryConverter(request: convertForm) + @Post(path: 'form/body') + Future postForm(@Body() Map fields); + + @Post(path: 'form/body', headers: {contentTypeKey: formEncodedHeaders}) + Future postFormUsingHeaders(@Body() Map fields); + + @FactoryConverter(request: convertForm) + @Post(path: 'form/body/fields') + Future postFormFields(@Field() String foo, @Field() int bar); + + @Post(path: 'map/json') + @FactoryConverter( + request: customConvertRequest, + response: customConvertResponse, + ) + Future forceJsonTest(@Body() Map map); + + @Post(path: 'multi') + @multipart + Future postResources( + @Part('1') Map a, + @Part('2') Map b, + ); + + @Post(path: 'file') + @multipart + Future postFile( + @PartFile('file') List bytes, + ); + + @Post(path: 'image') + @multipart + Future postImage( + @PartFile('image') List imageData, + ); + + @Post(path: 'file') + @multipart + Future postMultipartFile( + @PartFile() MultipartFile file, { + @Part() String? id, + }); + + @Post(path: 'files') + @multipart + Future postListFiles(@PartFile() List files); + + @Post(path: 'multipart_list') + @multipart + Future postMultipartList({ + @Part('ints') required List ints, + @Part('doubles') required List doubles, + @Part('nums') required List nums, + @Part('strings') required List strings, + }); + + @Get(path: 'https://test.com') + Future fullUrl(); + + @Get(path: '/list/string') + Future> listString(); + + @Post(path: 'no-body') + Future noBody(); + + @Get(path: '/query_param_include_null_query_vars', includeNullQueryVars: true) + Future getUsingQueryParamIncludeNullQueryVars({ + @Query('foo') String? foo, + @Query('bar') String? bar, + @Query('baz') String? baz, + }); + + @Get(path: '/list_query_param') + Future getUsingListQueryParam( + @Query('value') List value, + ); + + @Get(path: '/list_query_param_with_brackets', useBrackets: true) + Future getUsingListQueryParamWithBrackets( + @Query('value') List value, + ); + + @Get(path: '/map_query_param') + Future getUsingMapQueryParam( + @Query('value') Map value, + ); + + @Get( + path: '/map_query_param_include_null_query_vars', + includeNullQueryVars: true, + ) + Future getUsingMapQueryParamIncludeNullQueryVars( + @Query('value') Map value, + ); + + @Get(path: '/map_query_param_with_brackets', useBrackets: true) + Future getUsingMapQueryParamWithBrackets( + @Query('value') Map value, + ); + + @Get(path: 'headers') + Future getHeaders({ + @Header('x-string') required String stringHeader, + @Header('x-boolean') bool? boolHeader, + @Header('x-int') int? intHeader, + @Header('x-double') double? doubleHeader, + @Header('x-enum') ExampleEnum? enumHeader, + }); +} + +Request customConvertRequest(Request req) { + final r = JsonConverter().convertRequest(req); + + return applyHeader(r, 'customConverter', 'true'); +} + +Response customConvertResponse(Response res) => + res.copyWith(body: json.decode(res.body)); + +Request convertForm(Request req) { + req = applyHeader(req, contentTypeKey, formEncodedHeaders); + + if (req.body is Map) { + final body = {}; + + req.body.forEach((key, val) { + if (val != null) { + body[key.toString()] = val.toString(); + } + }); + + req = req.copyWith(body: body); + } + + return req; +} + +enum ExampleEnum { + foo, + bar, + baz; + + @override + String toString() => name; +} diff --git a/chopper_generator/lib/src/generator.dart b/chopper_generator/lib/src/generator.dart index 915b49f8..a39c1181 100644 --- a/chopper_generator/lib/src/generator.dart +++ b/chopper_generator/lib/src/generator.dart @@ -179,18 +179,34 @@ final class ChopperGenerator baseUrl, baseUrlVariableElement, ); - final DartType? responseType = _getResponseType(m.returnType); + + // Check if Response is present in the return type + final bool isResponseObject = _isResponse(m.returnType); + final DartType? responseType = + _getResponseType(m.returnType, isResponseObject); final DartType? responseInnerType = _getResponseInnerType(m.returnType) ?? responseType; + // Set Response with generic types + final Reference responseTypeReference = refer( + responseType?.getDisplayString(withNullability: false) ?? + responseType?.getDisplayString(withNullability: false) ?? + 'dynamic'); + // Set the return type + final returnType = isResponseObject + ? refer(m.returnType.getDisplayString(withNullability: false)) + : TypeReference( + (b) => b + ..symbol = 'Future' + ..types.add(responseTypeReference), + ); + return Method((MethodBuilder methodBuilder) { methodBuilder ..annotations.add(refer('override')) ..name = m.displayName // We don't support returning null Type - ..returns = refer( - m.returnType.getDisplayString(withNullability: false), - ) + ..returns = returnType // And null Typed parameters ..types.addAll( m.typeParameters.map( @@ -211,6 +227,12 @@ final class ChopperGenerator m.parameters.where((p) => p.isNamed).map(Utils.buildNamedParam), ); + // Make method async if Response is omitted. + // We need the await the response in order to return the body. + if (!isResponseObject) { + methodBuilder.modifier = MethodModifier.async; + } + final List blocks = [ declareFinal(Vars.url.toString(), type: refer('Uri')) .assign(url) @@ -410,18 +432,36 @@ final class ChopperGenerator ]); } - blocks.add( - refer(Vars.client.toString()) - .property('send') - .call( - [refer(Vars.request.toString())], - namedArguments, - typeArguments, - ) - .returned - .statement, + final returnStatement = + refer(Vars.client.toString()).property('send').call( + [refer(Vars.request.toString())], + namedArguments, + typeArguments, ); + if (isResponseObject) { + // Return the response object directly from chopper.send + blocks.add(returnStatement.returned.statement); + } else { + // Await the response object from chopper.send + blocks.add( + // generic types are not passed in the code_builder at the moment. + declareFinal( + Vars.response.toString(), + type: TypeReference( + (b) => b + ..symbol = 'Response' + ..types.add(responseTypeReference), + ), + ).assign(returnStatement.awaited).statement, + ); + // Return the body of the response object + blocks.add(refer(Vars.response.toString()) + .property('bodyOrThrow') + .returned + .statement); + } + methodBuilder.body = Block.of(blocks); }); } @@ -508,8 +548,15 @@ final class ChopperGenerator ? type.typeArguments.first : null; - static DartType? _getResponseType(DartType type) => - _genericOf(_genericOf(type)); + static bool _isResponse(DartType type) { + final DartType? responseType = _genericOf(type); + if (responseType == null) return false; + + return _typeChecker(chopper.Response).isExactlyType(responseType); + } + + static DartType? _getResponseType(DartType type, bool isResponseObject) => + isResponseObject ? _genericOf(_genericOf(type)) : _genericOf(type); static DartType? _getResponseInnerType(DartType type) { final DartType? generic = _genericOf(type); diff --git a/chopper_generator/lib/src/vars.dart b/chopper_generator/lib/src/vars.dart index cb7c5fdd..77562cec 100644 --- a/chopper_generator/lib/src/vars.dart +++ b/chopper_generator/lib/src/vars.dart @@ -1,5 +1,6 @@ enum Vars { client('client'), + response(r'$response'), baseUrl('baseUrl'), parameters(r'$params'), headers(r'$headers'), diff --git a/chopper_generator/test/ensure_build_test.dart b/chopper_generator/test/ensure_build_test.dart index c708d8d1..84c677fc 100644 --- a/chopper_generator/test/ensure_build_test.dart +++ b/chopper_generator/test/ensure_build_test.dart @@ -6,18 +6,13 @@ import 'package:test/test.dart'; void main() { test( 'ensure_build', - () { - expectBuildClean( + () async { + await expectBuildClean( packageRelativeDirectory: 'chopper_generator', gitDiffPathArguments: [ 'test/test_service.chopper.dart', - ], - ); - - expectBuildClean( - packageRelativeDirectory: 'chopper_generator', - gitDiffPathArguments: [ 'test/test_service_variable.chopper.dart', + 'test/test_without_response_service.chopper.dart', ], ); }, diff --git a/chopper_generator/test/test_service.chopper.dart b/chopper_generator/test/test_service.chopper.dart index b94c2e9b..16004b15 100644 --- a/chopper_generator/test/test_service.chopper.dart +++ b/chopper_generator/test/test_service.chopper.dart @@ -513,14 +513,14 @@ final class _$HttpTestService extends HttpTestService { } @override - Future fullUrl() { + Future> fullUrl() { final Uri $url = Uri.parse('https://test.com'); final Request $request = Request( 'GET', $url, client.baseUrl, ); - return client.send($request); + return client.send($request); } @override diff --git a/chopper_generator/test/test_service.dart b/chopper_generator/test/test_service.dart index 30d6a451..58dce35a 100644 --- a/chopper_generator/test/test_service.dart +++ b/chopper_generator/test/test_service.dart @@ -146,7 +146,7 @@ abstract class HttpTestService extends ChopperService { }); @Get(path: 'https://test.com') - Future fullUrl(); + Future fullUrl(); @Get(path: '/list/string') Future>> listString(); diff --git a/chopper_generator/test/test_service_variable.chopper.dart b/chopper_generator/test/test_service_variable.chopper.dart index e3611022..9c69ffa0 100644 --- a/chopper_generator/test/test_service_variable.chopper.dart +++ b/chopper_generator/test/test_service_variable.chopper.dart @@ -513,14 +513,14 @@ final class _$HttpTestServiceVariable extends HttpTestServiceVariable { } @override - Future fullUrl() { + Future> fullUrl() { final Uri $url = Uri.parse('https://test.com'); final Request $request = Request( 'GET', $url, client.baseUrl, ); - return client.send($request); + return client.send($request); } @override diff --git a/chopper_generator/test/test_service_variable.dart b/chopper_generator/test/test_service_variable.dart index 81532976..251b48cb 100644 --- a/chopper_generator/test/test_service_variable.dart +++ b/chopper_generator/test/test_service_variable.dart @@ -148,7 +148,7 @@ abstract class HttpTestServiceVariable extends ChopperService { }); @Get(path: 'https://test.com') - Future fullUrl(); + Future fullUrl(); @Get(path: '/list/string') Future>> listString(); diff --git a/chopper_generator/test/test_without_response_service.chopper.dart b/chopper_generator/test/test_without_response_service.chopper.dart new file mode 100644 index 00000000..c7b8462a --- /dev/null +++ b/chopper_generator/test/test_without_response_service.chopper.dart @@ -0,0 +1,705 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'test_without_response_service.dart'; + +// ************************************************************************** +// ChopperGenerator +// ************************************************************************** + +// coverage:ignore-file +// ignore_for_file: type=lint +final class _$HttpTestService extends HttpTestService { + _$HttpTestService([ChopperClient? client]) { + if (client == null) return; + this.client = client; + } + + @override + final Type definitionType = HttpTestService; + + @override + Future getTest( + String id, { + required String dynamicHeader, + }) async { + final Uri $url = Uri.parse('/test/get/${id}'); + final Map $headers = { + 'test': dynamicHeader, + }; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + headers: $headers, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + + @override + Future headTest() async { + final Uri $url = Uri.parse('/test/head'); + final Request $request = Request( + 'HEAD', + $url, + client.baseUrl, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + + @override + Future optionsTest() async { + final Uri $url = Uri.parse('/test/options'); + final Request $request = Request( + 'OPTIONS', + $url, + client.baseUrl, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + + @override + Future>> getStreamTest() async { + final Uri $url = Uri.parse('/test/get'); + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + ); + final Response $response = + await client.send>, int>($request); + return $response.bodyOrThrow; + } + + @override + Future getAll() async { + final Uri $url = Uri.parse('/test'); + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + + @override + Future getAllWithTrailingSlash() async { + final Uri $url = Uri.parse('/test/'); + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + + @override + Future getQueryTest({ + String name = '', + int? number, + int? def = 42, + }) async { + final Uri $url = Uri.parse('/test/query'); + final Map $params = { + 'name': name, + 'int': number, + 'default_value': def, + }; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + + @override + Future getQueryMapTest(Map query) async { + final Uri $url = Uri.parse('/test/query_map'); + final Map $params = query; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + + @override + Future getQueryMapTest2( + Map query, { + bool? test, + }) async { + final Uri $url = Uri.parse('/test/query_map'); + final Map $params = {'test': test}; + $params.addAll(query); + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + + @override + Future getQueryMapTest3({ + String name = '', + int? number, + Map filters = const {}, + }) async { + final Uri $url = Uri.parse('/test/query_map'); + final Map $params = { + 'name': name, + 'number': number, + }; + $params.addAll(filters); + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + + @override + Future getQueryMapTest4({ + String name = '', + int? number, + Map? filters, + }) async { + final Uri $url = Uri.parse('/test/query_map'); + final Map $params = { + 'name': name, + 'number': number, + }; + $params.addAll(filters ?? const {}); + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + + @override + Future getQueryMapTest5({Map? filters}) async { + final Uri $url = Uri.parse('/test/query_map'); + final Map $params = filters ?? const {}; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + + @override + Future getBody(dynamic body) async { + final Uri $url = Uri.parse('/test/get_body'); + final $body = body; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + body: $body, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + + @override + Future postTest(String data) async { + final Uri $url = Uri.parse('/test/post'); + final $body = data; + final Request $request = Request( + 'POST', + $url, + client.baseUrl, + body: $body, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + + @override + Future postStreamTest(Stream> byteStream) async { + final Uri $url = Uri.parse('/test/post'); + final $body = byteStream; + final Request $request = Request( + 'POST', + $url, + client.baseUrl, + body: $body, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + + @override + Future putTest( + String test, + String data, + ) async { + final Uri $url = Uri.parse('/test/put/${test}'); + final $body = data; + final Request $request = Request( + 'PUT', + $url, + client.baseUrl, + body: $body, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + + @override + Future deleteTest(String id) async { + final Uri $url = Uri.parse('/test/delete/${id}'); + final Map $headers = { + 'foo': 'bar', + }; + final Request $request = Request( + 'DELETE', + $url, + client.baseUrl, + headers: $headers, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + + @override + Future patchTest( + String id, + String data, + ) async { + final Uri $url = Uri.parse('/test/patch/${id}'); + final $body = data; + final Request $request = Request( + 'PATCH', + $url, + client.baseUrl, + body: $body, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + + @override + Future mapTest(Map map) async { + final Uri $url = Uri.parse('/test/map'); + final $body = map; + final Request $request = Request( + 'POST', + $url, + client.baseUrl, + body: $body, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + + @override + Future postForm(Map fields) async { + final Uri $url = Uri.parse('/test/form/body'); + final $body = fields; + final Request $request = Request( + 'POST', + $url, + client.baseUrl, + body: $body, + ); + final Response $response = await client.send( + $request, + requestConverter: convertForm, + ); + return $response.bodyOrThrow; + } + + @override + Future postFormUsingHeaders(Map fields) async { + final Uri $url = Uri.parse('/test/form/body'); + final Map $headers = { + 'content-type': 'application/x-www-form-urlencoded', + }; + final $body = fields; + final Request $request = Request( + 'POST', + $url, + client.baseUrl, + body: $body, + headers: $headers, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + + @override + Future postFormFields( + String foo, + int bar, + ) async { + final Uri $url = Uri.parse('/test/form/body/fields'); + final $body = { + 'foo': foo, + 'bar': bar, + }; + final Request $request = Request( + 'POST', + $url, + client.baseUrl, + body: $body, + ); + final Response $response = await client.send( + $request, + requestConverter: convertForm, + ); + return $response.bodyOrThrow; + } + + @override + Future forceJsonTest(Map map) async { + final Uri $url = Uri.parse('/test/map/json'); + final $body = map; + final Request $request = Request( + 'POST', + $url, + client.baseUrl, + body: $body, + ); + final Response $response = await client.send( + $request, + requestConverter: customConvertRequest, + responseConverter: customConvertResponse, + ); + return $response.bodyOrThrow; + } + + @override + Future postResources( + Map a, + Map b, + ) async { + final Uri $url = Uri.parse('/test/multi'); + final List $parts = [ + PartValue>( + '1', + a, + ), + PartValue>( + '2', + b, + ), + ]; + final Request $request = Request( + 'POST', + $url, + client.baseUrl, + parts: $parts, + multipart: true, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + + @override + Future postFile(List bytes) async { + final Uri $url = Uri.parse('/test/file'); + final List $parts = [ + PartValueFile>( + 'file', + bytes, + ) + ]; + final Request $request = Request( + 'POST', + $url, + client.baseUrl, + parts: $parts, + multipart: true, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + + @override + Future postImage(List imageData) async { + final Uri $url = Uri.parse('/test/image'); + final List $parts = [ + PartValueFile>( + 'image', + imageData, + ) + ]; + final Request $request = Request( + 'POST', + $url, + client.baseUrl, + parts: $parts, + multipart: true, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + + @override + Future postMultipartFile( + MultipartFile file, { + String? id, + }) async { + final Uri $url = Uri.parse('/test/file'); + final List $parts = [ + PartValue( + 'id', + id, + ), + PartValueFile( + 'file', + file, + ), + ]; + final Request $request = Request( + 'POST', + $url, + client.baseUrl, + parts: $parts, + multipart: true, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + + @override + Future postListFiles(List files) async { + final Uri $url = Uri.parse('/test/files'); + final List $parts = [ + PartValueFile>( + 'files', + files, + ) + ]; + final Request $request = Request( + 'POST', + $url, + client.baseUrl, + parts: $parts, + multipart: true, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + + @override + Future postMultipartList({ + required List ints, + required List doubles, + required List nums, + required List strings, + }) async { + final Uri $url = Uri.parse('/test/multipart_list'); + final List $parts = [ + PartValue>( + 'ints', + ints, + ), + PartValue>( + 'doubles', + doubles, + ), + PartValue>( + 'nums', + nums, + ), + PartValue>( + 'strings', + strings, + ), + ]; + final Request $request = Request( + 'POST', + $url, + client.baseUrl, + parts: $parts, + multipart: true, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + + @override + Future fullUrl() async { + final Uri $url = Uri.parse('https://test.com'); + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + + @override + Future> listString() async { + final Uri $url = Uri.parse('/test/list/string'); + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + ); + final Response $response = + await client.send, String>($request); + return $response.bodyOrThrow; + } + + @override + Future noBody() async { + final Uri $url = Uri.parse('/test/no-body'); + final Request $request = Request( + 'POST', + $url, + client.baseUrl, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + + @override + Future getUsingQueryParamIncludeNullQueryVars({ + String? foo, + String? bar, + String? baz, + }) async { + final Uri $url = Uri.parse('/test/query_param_include_null_query_vars'); + final Map $params = { + 'foo': foo, + 'bar': bar, + 'baz': baz, + }; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + includeNullQueryVars: true, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + + @override + Future getUsingListQueryParam(List value) async { + final Uri $url = Uri.parse('/test/list_query_param'); + final Map $params = {'value': value}; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + + @override + Future getUsingListQueryParamWithBrackets(List value) async { + final Uri $url = Uri.parse('/test/list_query_param_with_brackets'); + final Map $params = {'value': value}; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + useBrackets: true, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + + @override + Future getUsingMapQueryParam(Map value) async { + final Uri $url = Uri.parse('/test/map_query_param'); + final Map $params = {'value': value}; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + + @override + Future getUsingMapQueryParamIncludeNullQueryVars( + Map value) async { + final Uri $url = Uri.parse('/test/map_query_param_include_null_query_vars'); + final Map $params = {'value': value}; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + includeNullQueryVars: true, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + + @override + Future getUsingMapQueryParamWithBrackets( + Map value) async { + final Uri $url = Uri.parse('/test/map_query_param_with_brackets'); + final Map $params = {'value': value}; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + useBrackets: true, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + + @override + Future getHeaders({ + required String stringHeader, + bool? boolHeader, + int? intHeader, + double? doubleHeader, + ExampleEnum? enumHeader, + }) async { + final Uri $url = Uri.parse('/test/headers'); + final Map $headers = { + 'x-string': stringHeader, + if (boolHeader != null) 'x-boolean': boolHeader.toString(), + if (intHeader != null) 'x-int': intHeader.toString(), + if (doubleHeader != null) 'x-double': doubleHeader.toString(), + if (enumHeader != null) 'x-enum': enumHeader.toString(), + }; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + headers: $headers, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } +} diff --git a/chopper_generator/test/test_without_response_service.dart b/chopper_generator/test/test_without_response_service.dart new file mode 100644 index 00000000..d8a95417 --- /dev/null +++ b/chopper_generator/test/test_without_response_service.dart @@ -0,0 +1,236 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:chopper/chopper.dart'; +import 'package:http/http.dart' show MultipartFile; + +part 'test_without_response_service.chopper.dart'; + +@ChopperApi(baseUrl: '/test') +abstract class HttpTestService extends ChopperService { + static HttpTestService create([ChopperClient? client]) => + _$HttpTestService(client); + + @Get(path: 'get/{id}') + Future getTest( + @Path() String id, { + @Header('test') required String dynamicHeader, + }); + + @Head(path: 'head') + Future headTest(); + + @Options(path: 'options') + Future optionsTest(); + + @Get(path: 'get') + Future>> getStreamTest(); + + @Get(path: '') + Future getAll(); + + @Get(path: '/') + Future getAllWithTrailingSlash(); + + @Get(path: 'query') + Future getQueryTest({ + @Query('name') String name = '', + @Query('int') int? number, + @Query('default_value') int? def = 42, + }); + + @Get(path: 'query_map') + Future getQueryMapTest(@QueryMap() Map query); + + @Get(path: 'query_map') + Future getQueryMapTest2( + @QueryMap() Map query, { + @Query('test') bool? test, + }); + + @Get(path: 'query_map') + Future getQueryMapTest3({ + @Query('name') String name = '', + @Query('number') int? number, + @QueryMap() Map filters = const {}, + }); + + @Get(path: 'query_map') + Future getQueryMapTest4({ + @Query('name') String name = '', + @Query('number') int? number, + @QueryMap() Map? filters, + }); + + @Get(path: 'query_map') + Future getQueryMapTest5({ + @QueryMap() Map? filters, + }); + + @Get(path: 'get_body') + Future getBody(@Body() dynamic body); + + @Post(path: 'post') + Future postTest(@Body() String data); + + @Post(path: 'post') + Future postStreamTest(@Body() Stream> byteStream); + + @Put(path: 'put/{id}') + Future putTest(@Path('id') String test, @Body() String data); + + @Delete(path: 'delete/{id}', headers: {'foo': 'bar'}) + Future deleteTest(@Path() String id); + + @Patch(path: 'patch/{id}') + Future patchTest(@Path() String id, @Body() String data); + + @Post(path: 'map') + Future mapTest(@Body() Map map); + + @FactoryConverter(request: convertForm) + @Post(path: 'form/body') + Future postForm(@Body() Map fields); + + @Post(path: 'form/body', headers: {contentTypeKey: formEncodedHeaders}) + Future postFormUsingHeaders(@Body() Map fields); + + @FactoryConverter(request: convertForm) + @Post(path: 'form/body/fields') + Future postFormFields(@Field() String foo, @Field() int bar); + + @Post(path: 'map/json') + @FactoryConverter( + request: customConvertRequest, + response: customConvertResponse, + ) + Future forceJsonTest(@Body() Map map); + + @Post(path: 'multi') + @multipart + Future postResources( + @Part('1') Map a, + @Part('2') Map b, + ); + + @Post(path: 'file') + @multipart + Future postFile( + @PartFile('file') List bytes, + ); + + @Post(path: 'image') + @multipart + Future postImage( + @PartFile('image') List imageData, + ); + + @Post(path: 'file') + @multipart + Future postMultipartFile( + @PartFile() MultipartFile file, { + @Part() String? id, + }); + + @Post(path: 'files') + @multipart + Future postListFiles(@PartFile() List files); + + @Post(path: 'multipart_list') + @multipart + Future postMultipartList({ + @Part('ints') required List ints, + @Part('doubles') required List doubles, + @Part('nums') required List nums, + @Part('strings') required List strings, + }); + + @Get(path: 'https://test.com') + Future fullUrl(); + + @Get(path: '/list/string') + Future> listString(); + + @Post(path: 'no-body') + Future noBody(); + + @Get(path: '/query_param_include_null_query_vars', includeNullQueryVars: true) + Future getUsingQueryParamIncludeNullQueryVars({ + @Query('foo') String? foo, + @Query('bar') String? bar, + @Query('baz') String? baz, + }); + + @Get(path: '/list_query_param') + Future getUsingListQueryParam( + @Query('value') List value, + ); + + @Get(path: '/list_query_param_with_brackets', useBrackets: true) + Future getUsingListQueryParamWithBrackets( + @Query('value') List value, + ); + + @Get(path: '/map_query_param') + Future getUsingMapQueryParam( + @Query('value') Map value, + ); + + @Get( + path: '/map_query_param_include_null_query_vars', + includeNullQueryVars: true, + ) + Future getUsingMapQueryParamIncludeNullQueryVars( + @Query('value') Map value, + ); + + @Get(path: '/map_query_param_with_brackets', useBrackets: true) + Future getUsingMapQueryParamWithBrackets( + @Query('value') Map value, + ); + + @Get(path: 'headers') + Future getHeaders({ + @Header('x-string') required String stringHeader, + @Header('x-boolean') bool? boolHeader, + @Header('x-int') int? intHeader, + @Header('x-double') double? doubleHeader, + @Header('x-enum') ExampleEnum? enumHeader, + }); +} + +Request customConvertRequest(Request req) { + final r = JsonConverter().convertRequest(req); + + return applyHeader(r, 'customConverter', 'true'); +} + +Response customConvertResponse(Response res) => + res.copyWith(body: json.decode(res.body)); + +Request convertForm(Request req) { + req = applyHeader(req, contentTypeKey, formEncodedHeaders); + + if (req.body is Map) { + final body = {}; + + req.body.forEach((key, val) { + if (val != null) { + body[key.toString()] = val.toString(); + } + }); + + req = req.copyWith(body: body); + } + + return req; +} + +enum ExampleEnum { + foo, + bar, + baz; + + @override + String toString() => name; +} From 2dc868f68a21beff466bb1bf9713315228c46d6a Mon Sep 17 00:00:00 2001 From: Job Guldemeester Date: Fri, 5 Jan 2024 21:09:18 +0100 Subject: [PATCH 125/171] :memo: Improved documentation + added const annotations (#548) --- chopper/lib/src/annotations.dart | 127 +++++++++++ chopper/lib/src/http_logging_interceptor.dart | 3 + chopper/lib/src/interceptor.dart | 9 + chopper/lib/src/request.dart | 3 + chopper/lib/src/response.dart | 3 + faq.md | 44 +++- getting-started.md | 14 +- requests.md | 203 +++++++++++++----- 8 files changed, 348 insertions(+), 58 deletions(-) diff --git a/chopper/lib/src/annotations.dart b/chopper/lib/src/annotations.dart index 4fa5e492..7ee1552b 100644 --- a/chopper/lib/src/annotations.dart +++ b/chopper/lib/src/annotations.dart @@ -5,6 +5,7 @@ import 'package:chopper/src/request.dart'; import 'package:chopper/src/response.dart'; import 'package:meta/meta.dart'; +/// {@template ChopperApi} /// Defines a Chopper API. /// /// Must be used on an abstract class that extends the [ChopperService] class. @@ -19,6 +20,7 @@ import 'package:meta/meta.dart'; /// ``` /// /// See [Method] to define an HTTP request +/// {@endtemplate} @immutable final class ChopperApi { /// A part of a URL that every request defined inside a class annotated with [ChopperApi] will be prefixed with. @@ -26,11 +28,13 @@ final class ChopperApi { /// The `baseUrl` can be a top level constant string variable. final String baseUrl; + /// {@macro ChopperApi} const ChopperApi({ this.baseUrl = '', }); } +/// {@template Path} /// Provides a parameter in the url. /// /// Declared as follows inside the path String: @@ -43,6 +47,7 @@ final class ChopperApi { /// @Get(path: '/{param}') /// Future fetch(@Path() String param); /// ``` +/// {@endtemplate} @immutable final class Path { /// Name is used to bind a method parameter to @@ -53,9 +58,11 @@ final class Path { /// ``` final String? name; + /// {@macro Path} const Path([this.name]); } +/// {@template Query} /// Provides the query parameters of a request. /// /// [Query] is used to add query parameters after the request url. @@ -66,6 +73,7 @@ final class Path { /// ``` /// /// See [QueryMap] to pass an [Map] as value +/// {@endtemplate} @immutable final class Query { /// Name is used to bind a method parameter to @@ -76,9 +84,11 @@ final class Query { /// ``` final String? name; + /// {@macro Query} const Query([this.name]); } +/// {@template QueryMap} /// Provides query parameters of a request as [Map]. /// /// ```dart @@ -91,11 +101,14 @@ final class Query { /// fetch({'foo':'bar','list':[1,2]}); /// // something?foo=bar&list=1&list=2 /// ``` +/// {@endtemplate} @immutable final class QueryMap { + /// {@macro QueryMap} const QueryMap(); } +/// {@template Body} /// Declares the Body of [Post], [Put], and [Patch] requests /// /// ```dart @@ -105,11 +118,14 @@ final class QueryMap { /// /// The body can be of any type, but chopper does not automatically convert it to JSON. /// See [Converter] to apply conversion to the body. +/// {@endtemplate} @immutable final class Body { + /// {@macro Body} const Body(); } +/// {@template Header} /// Passes a value to the header of the request. /// /// Use the name of the method parameter or the name specified in the annotation. @@ -118,6 +134,7 @@ final class Body { /// @Get() /// Future fetch(@Header() String foo); /// ``` +/// {@endtemplate} @immutable final class Header { /// Name is used to bind a method parameter to @@ -128,9 +145,11 @@ final class Header { /// ``` final String? name; + /// {@macro Header} const Header([this.name]); } +/// {@template Method} /// Defines an HTTP method. /// /// Must be used inside a [ChopperApi] definition. @@ -148,6 +167,7 @@ final class Header { /// The [Response] type also supports typed parameters like `Future>`. /// However, chopper will not automatically convert the body response to your type. /// A [Converter] needs to be specified for conversion. +/// {@endtemplate} @immutable sealed class Method { /// HTTP method for the request @@ -197,6 +217,7 @@ sealed class Method { /// The above code produces hxxp://path/to/script&foo=foo_var&bar=&baz=baz_var final bool includeNullQueryVars; + /// {@macro Method} const Method( this.method, { this.optionalBody = false, @@ -207,9 +228,12 @@ sealed class Method { }); } +/// {@template Get} /// Defines a method as an HTTP GET request. +/// {@endtemplate} @immutable final class Get extends Method { + /// {@macro Get} const Get({ super.optionalBody = true, super.path, @@ -219,11 +243,14 @@ final class Get extends Method { }) : super(HttpMethod.Get); } +/// {@template Post} /// Defines a method as an HTTP POST request. /// /// Use the [Body] annotation to pass data to send. +/// {@endtemplate} @immutable final class Post extends Method { + /// {@macro Post} const Post({ super.optionalBody, super.path, @@ -233,9 +260,12 @@ final class Post extends Method { }) : super(HttpMethod.Post); } +/// {@template Delete} /// Defines a method as an HTTP DELETE request. +/// {@endtemplate} @immutable final class Delete extends Method { + /// {@macro Delete} const Delete({ super.optionalBody = true, super.path, @@ -245,11 +275,14 @@ final class Delete extends Method { }) : super(HttpMethod.Delete); } +/// {@template Put} /// Defines a method as an HTTP PUT request. /// /// Use the [Body] annotation to pass data to send. +/// {@endtemplate} @immutable final class Put extends Method { + /// {@macro Put} const Put({ super.optionalBody, super.path, @@ -259,10 +292,13 @@ final class Put extends Method { }) : super(HttpMethod.Put); } +/// {@template Patch} /// Defines a method as an HTTP PATCH request. /// Use the [Body] annotation to pass data to send. +/// {@endtemplate} @immutable final class Patch extends Method { + /// {@macro Patch} const Patch({ super.optionalBody, super.path, @@ -272,9 +308,12 @@ final class Patch extends Method { }) : super(HttpMethod.Patch); } +/// {@template Head} /// Defines a method as an HTTP HEAD request. +/// {@endtemplate} @immutable final class Head extends Method { + /// {@macro Head} const Head({ super.optionalBody = true, super.path, @@ -284,8 +323,12 @@ final class Head extends Method { }) : super(HttpMethod.Head); } +/// {@template Options} +/// Defines a method as an HTTP OPTIONS request. +/// {@endtemplate} @immutable final class Options extends Method { + /// {@macro Options} const Options({ super.optionalBody = true, super.path, @@ -302,6 +345,7 @@ typedef ConvertRequest = FutureOr Function(Request request); /// representation to a Dart object. typedef ConvertResponse = FutureOr Function(Response response); +/// {@template FactoryConverter} /// Defines custom [Converter] methods for a single network API endpoint. /// See [ConvertRequest], [ConvertResponse]. /// @@ -331,17 +375,20 @@ typedef ConvertResponse = FutureOr Function(Response response); /// Future> getTodo(@Path("id")); /// } /// ``` +/// {@endtemplate} @immutable final class FactoryConverter { final ConvertRequest? request; final ConvertResponse? response; + /// {@macro FactoryConverter} const FactoryConverter({ this.request, this.response, }); } +/// {@template Field} /// Defines a field for a `x-www-form-urlencoded` request. /// Automatically binds to the name of the method parameter. /// @@ -350,6 +397,7 @@ final class FactoryConverter { /// Future create(@Field() String name); /// ``` /// Will be converted to `{ 'name': value }`. +/// {@endtemplate} @immutable final class Field { /// Name can be use to specify the name of the field @@ -359,20 +407,25 @@ final class Field { /// ``` final String? name; + /// {@macro Field} const Field([this.name]); } +/// {@template FieldMap} /// Provides field parameters of a request as [Map]. /// /// ```dart /// @Post(path: '/something') /// Future fetch(@FieldMap List> query); /// ``` +/// {@endtemplate} @immutable final class FieldMap { + /// {@macro FieldMap} const FieldMap(); } +/// {@template Multipart} /// Defines a multipart request. /// /// ```dart @@ -383,23 +436,29 @@ final class FieldMap { /// /// Use [Part] annotation to send simple data. /// Use [PartFile] annotation to send `File` or `List`. +/// {@endtemplate} @immutable final class Multipart { + /// {@macro Multipart} const Multipart(); } +/// {@template Part} /// Use [Part] to define a part of a [Multipart] request. /// /// All values will be converted to [String] using their [toString] method. /// /// Also accepts `MultipartFile` (from package:http). +/// {@endtemplate} @immutable final class Part { final String? name; + /// {@macro Part} const Part([this.name]); } +/// {@template PartMap} /// Provides part parameters of a request as [PartValue]. /// /// ```dart @@ -407,11 +466,14 @@ final class Part { /// @Multipart /// Future fetch(@PartMap() List query); /// ``` +/// {@endtemplate} @immutable final class PartMap { + /// {@macro PartMap} const PartMap(); } +/// {@template PartFile} /// Use [PartFile] to define a file field for a [Multipart] request. /// /// ```dart @@ -424,13 +486,16 @@ final class PartMap { /// - `List` /// - [String] (path of your file) /// - `MultipartFile` (from package:http) +/// {@endtemplate} @immutable final class PartFile { final String? name; + /// {@macro PartFile} const PartFile([this.name]); } +/// {@template PartFileMap} /// Provides partFile parameters of a request as [PartValueFile]. /// /// ```dart @@ -438,10 +503,72 @@ final class PartFile { /// @Multipart /// Future fetch(@PartFileMap() List query); /// ``` +/// {@endtemplate} @immutable final class PartFileMap { + /// {@macro PartFileMap} const PartFileMap(); } +/// {@macro ChopperApi} +const chopperApi = ChopperApi(); + +/// {@macro Multipart} const multipart = Multipart(); + +/// {@macro Body} const body = Body(); + +/// {@macro Path} +const path = Path(); + +/// {@macro Query} +const query = Query(); + +/// {@macro QueryMap} +const queryMap = QueryMap(); + +/// {@macro Header} +const header = Header(); + +/// {@macro Get} +const get = Get(); + +/// {@macro Post} +const post = Post(); + +/// {@macro Delete} +const delete = Delete(); + +/// {@macro Put} +const put = Put(); + +/// {@macro Patch} +const patch = Patch(); + +/// {@macro Head} +const head = Head(); + +/// {@macro Options} +const options = Options(); + +/// {@macro FactoryConverter} +const factoryConverter = FactoryConverter(); + +/// {@macro Field} +const field = Field(); + +/// {@macro FieldMap} +const fieldMap = FieldMap(); + +/// {@macro Part} +const part = Part(); + +/// {@macro PartMap} +const partMap = PartMap(); + +/// {@macro PartFile} +const partFile = PartFile(); + +/// {@macro PartFileMap} +const partFileMap = PartFileMap(); diff --git a/chopper/lib/src/http_logging_interceptor.dart b/chopper/lib/src/http_logging_interceptor.dart index e2453673..bdf37b35 100644 --- a/chopper/lib/src/http_logging_interceptor.dart +++ b/chopper/lib/src/http_logging_interceptor.dart @@ -60,6 +60,7 @@ enum Level { body, } +/// {@template http_logging_interceptor} /// A [RequestInterceptor] and [ResponseInterceptor] implementation which logs /// HTTP request and response data. /// @@ -70,9 +71,11 @@ enum Level { /// leak sensitive information, such as `Authorization` headers and user data /// in response bodies. This interceptor should only be used in a controlled way /// or in a non-production environment. +/// {@endtemplate} @immutable class HttpLoggingInterceptor implements RequestInterceptor, ResponseInterceptor { + /// {@macro http_logging_interceptor} HttpLoggingInterceptor({this.level = Level.body, Logger? logger}) : _logger = logger ?? chopperLogger, _logBody = level == Level.body, diff --git a/chopper/lib/src/interceptor.dart b/chopper/lib/src/interceptor.dart index 0b11f53f..d76e2920 100644 --- a/chopper/lib/src/interceptor.dart +++ b/chopper/lib/src/interceptor.dart @@ -100,14 +100,17 @@ abstract interface class ErrorConverter { FutureOr convertError(Response response); } +/// {@template HeadersInterceptor} /// A [RequestInterceptor] that adds [headers] to every request. /// /// Note that this interceptor will overwrite existing headers having the same /// keys as [headers]. +/// {@endtemplate} @immutable class HeadersInterceptor implements RequestInterceptor { final Map headers; + /// {@macro HeadersInterceptor} const HeadersInterceptor(this.headers); @override @@ -163,6 +166,7 @@ class CurlInterceptor implements RequestInterceptor { } } +/// {@template JsonConverter} /// A [Converter] implementation that calls [json.encode] on [Request]s and /// [json.decode] on [Response]s using the [dart:convert](https://api.dart.dev/stable/2.10.3/dart-convert/dart-convert-library.html) /// package's [utf8] and [json] utilities. @@ -176,8 +180,10 @@ class CurlInterceptor implements RequestInterceptor { /// If content type header is modified (for example by using /// `@Post(headers: {'content-type': '...'})`), `JsonConverter` won't add the /// header and it won't call json.encode if content type is not JSON. +/// {@endtemplate} @immutable class JsonConverter implements Converter, ErrorConverter { + /// {@macro JsonConverter} const JsonConverter(); @override @@ -270,13 +276,16 @@ class JsonConverter implements Converter, ErrorConverter { } } +/// {@template FormUrlEncodedConverter} /// A [Converter] implementation that converts only [Request]s having a [Map] as their body. /// /// This `Converter` also adds the `content-type: application/x-www-form-urlencoded` /// header to each request, but only if the `content-type` header is not set in /// the original request. +/// {@endtemplate} @immutable class FormUrlEncodedConverter implements Converter, ErrorConverter { + /// {@macro FormUrlEncodedConverter} const FormUrlEncodedConverter(); @override diff --git a/chopper/lib/src/request.dart b/chopper/lib/src/request.dart index e07dbe3b..68d27331 100644 --- a/chopper/lib/src/request.dart +++ b/chopper/lib/src/request.dart @@ -6,7 +6,9 @@ import 'package:equatable/equatable.dart' show EquatableMixin; import 'package:http/http.dart' as http; import 'package:meta/meta.dart'; +/// {@template request} /// This class represents an HTTP request that can be made with Chopper. +/// {@endtemplate} base class Request extends http.BaseRequest with EquatableMixin { final Uri uri; final Uri baseUri; @@ -17,6 +19,7 @@ base class Request extends http.BaseRequest with EquatableMixin { final bool useBrackets; final bool includeNullQueryVars; + /// {@macro request} Request( String method, this.uri, diff --git a/chopper/lib/src/response.dart b/chopper/lib/src/response.dart index e5a18d4e..b9f8e06d 100644 --- a/chopper/lib/src/response.dart +++ b/chopper/lib/src/response.dart @@ -5,6 +5,7 @@ import 'package:equatable/equatable.dart' show EquatableMixin; import 'package:http/http.dart' as http; import 'package:meta/meta.dart'; +/// {@template response} /// A [http.BaseResponse] wrapper representing a response of a Chopper network call. /// /// ```dart @@ -16,6 +17,7 @@ import 'package:meta/meta.dart'; /// @Get(path: '/items/{id}') /// Future> fetchItem(); /// ``` +/// {@endtemplate} @immutable base class Response with EquatableMixin { /// The [http.BaseResponse] from `package:http` that this [Response] wraps. @@ -31,6 +33,7 @@ base class Response with EquatableMixin { /// The body of the response if [isSuccessful] is false. final Object? error; + /// {@macro response} const Response(this.base, this.body, {this.error}); /// Makes a copy of this Response, replacing original values with the given ones. diff --git a/faq.md b/faq.md index 796a1eac..a5ffeef3 100644 --- a/faq.md +++ b/faq.md @@ -172,7 +172,7 @@ The actual implementation of the algorithm above may vary based on how the backe ### Authorized HTTP requests using the special Authenticator interceptor Similar to OkHTTP's [authenticator](https://github.com/square/okhttp/blob/480c20e46bb1745e280e42607bbcc73b2c953d97/okhttp/src/main/kotlin/okhttp3/Authenticator.kt), -the idea here is to provide a reactive authentication in the event that an auth challenge is raised. It returns a +the idea here is to provide a reactive authentication in the event that an auth challenge is raised. It returns a nullable Request that contains a possible update to the original Request to satisfy the authentication challenge. ```dart @@ -223,7 +223,7 @@ final client = ChopperClient( ## Decoding JSON using Isolates Sometimes you want to decode JSON outside the main thread in order to reduce janking. In this example we're going to go -even further and implement a Worker Pool using [Squadron](https://pub.dev/packages/squadron/install) which can +even further and implement a Worker Pool using [Squadron](https://pub.dev/packages/squadron/install) which can dynamically spawn a maximum number of Workers as they become needed. #### Install the dependencies @@ -259,7 +259,7 @@ Extracted from the [full example here](example/lib/json_decode_service.dart). #### Write a custom JsonConverter -Using [json_serializable](https://pub.dev/packages/json_serializable) we'll create a [JsonConverter](https://github.com/lejard-h/chopper/blob/master/chopper/lib/src/interceptor.dart#L228) +Using [json_serializable](https://pub.dev/packages/json_serializable) we'll create a [JsonConverter](https://github.com/lejard-h/chopper/blob/master/chopper/lib/src/interceptor.dart#L228) which works with or without a [WorkerPool](https://github.com/d-markey/squadron#features). ```dart @@ -412,4 +412,40 @@ This barely scratches the surface. If you want to know more about [squadron](htt [squadron_builder](https://github.com/d-markey/squadron_builder) make sure to head over to their respective repositories. [David Markey](https://github.com/d-markey]), the author of squadron, was kind enough as to provide us with an [excellent Flutter example](https://github.com/d-markey/squadron_builder) using -both packages. \ No newline at end of file +both packages. + +## How to use Chopper with [Injectable](https://pub.dev/packages/injectable) + +### Create a module for your ChopperClient + +Define a module for your ChopperClient. You can use the `@lazySingleton` (or other type if preferred) annotation to make sure that only one is created. + +```dart +@module +abstract class ChopperModule { + + @lazySingleton + ChopperClient get chopperClient => + ChopperClient( + baseUrl: 'https://base-url.com', + converter: JsonConverter(), + ); +} +``` + +### Create ChopperService with Injectable + +Define your ChopperService as usual. Annotate the class with `@lazySingleton` (or other type if preferred) and use the `@factoryMethod` annotation to specify the factory method for the service. This would normally be the static create method. + +```dart +@lazySingleton +@ChopperApi(baseUrl: '/todos') +abstract class TodosListService extends ChopperService { + + @factoryMethod + static TodosListService create(ChopperClient client) => _$TodosListService(client); + + @Get() + Future>> getTodos(); +} +``` diff --git a/getting-started.md b/getting-started.md index fe9cbd01..75030c20 100644 --- a/getting-started.md +++ b/getting-started.md @@ -25,8 +25,7 @@ Run `pub get` to start using Chopper in your project. ### ChopperApi -To define a client, use the `@ -ChopperApi` annotation on an abstract class that extends the `ChopperService` class. +To define a client, use the `@ChopperApi` annotation on an abstract class that extends the `ChopperService` class. ```dart // YOUR_FILE.dart @@ -66,7 +65,9 @@ Use one of the following annotations on abstract methods of a service class to d * `@Head` -Request methods must return with values of the type `Future` or `Future>`. +Request methods must return with values of the type `Future`, `Future>` or `Future`. +The `Response` class is a wrapper around the HTTP response that contains the response body, the status code and the error (if any) of the request. +This class can be omitted if only the response body is needed. When omitting the `Response` class, the request will throw an exception if the response status code is not in the range of `< 200` to ` > 300`. To define a `GET` request to the endpoint `/todos` in the service class above, add one of the following method declarations to the class: @@ -82,6 +83,13 @@ or Future>> getTodos(); ``` +or + +```dart +@Get() +Future> getTodos(); +``` + URL manipulation with dynamic path, and query parameters is also supported. To learn more about URL manipulation with Chopper, have a look at the [Requests](requests.md) section of the documentation. ## Defining a ChopperClient diff --git a/requests.md b/requests.md index cee349a1..60d7f2f3 100644 --- a/requests.md +++ b/requests.md @@ -1,63 +1,105 @@ # Requests +## Available Request annotations + +| Annotation | HTTP verb | Description | +|--------------------------------------------|-----------|-----------------------------------------------| +| `@Get()`, `@get` | `GET` | Defines a `GET` request. | +| `@Post()`, `@post` | `POST` | Defines a `POST` request. | +| `@Put()`, `@put` | `PUT` | Defines a `PUT` request. | +| `@Patch()`, `@patch` | `PATCH` | Defines a `PATCH` request. | +| `@Delete()`, `@delete` | `DELETE` | Defines a `DELETE` request. | +| `@Head()`, `@head` | `HEAD` | Defines a `HEAD` request. | +| `@Body()`, `@body` | - | Defines the request's body. | +| `@Multipart()`, `@multipart` | - | Defines a `multipart/form-data` request. | +| `@Query()`, `@query` | - | Defines a query parameter. | +| `@QueryMap()`, `@queryMap` | - | Defines a query parameter map. | +| `@FactoryConverter()`, `@factoryConverter` | - | Defines a request/response converter factory. | +| `@Field()`, `@field` | - | Defines a form field. | +| `@FieldMap()`, `@fieldMap` | - | Defines a form field map. | +| `@Part()`, `@part` | - | Defines a multipart part. | +| `@PartMap()`, `@partMap` | - | Defines a multipart part map. | +| `@PartFile()`, `@partFile` | - | Defines a multipart file part. | +| `@PartFileMap()`, `@partFileMap` | - | Defines a multipart file part map. | + ## Path resolution Chopper handles paths passed to HTTP verb annotations' `path` parameter based on the path's content. -If the `path` value is a relative path, it will be concatenated to the URL composed of the `baseUrl` of the `ChopperClient` and the `baseUrl` of the enclosing service class (provided as a parameter of the `@ChopperApi` annotation). +If the `path` value is a relative path, it will be concatenated to the URL composed of the `baseUrl` of +the `ChopperClient` and the `baseUrl` of the enclosing service class (provided as a parameter of the `@ChopperApi` +annotation). Here are a few examples of the described behavior: -* `ChopperClient` base URL: https://example.com/ - Path: profile - Result: https://example.com/profile - -* `ChopperClient` base URL: https://example.com/ - Service base URL: profile - Path: /image - Result: https://example.com/profile/image - -* `ChopperClient` base URL: https://example.com/ - Service base URL: profile - Path: image - Result: https://example.com/profile/image - -> Chopper detects and handles missing slash (`/`) characters on URL segment borders, but *does not* handle duplicate slashes. - -If the service's `baseUrl` concatenated with the request's `path` results in a full URL, the `ChopperClient`'s `baseUrl` is ignored. - -* `ChopperClient` base URL: https://example.com/ -Service base URL: https://api.github.com/ -Path: user -Result: https://api.github.com/user - -A `path` containing a full URL replaces the base URLs of both the `ChopperClient` and the service class entirely for a request. - -* `ChopperClient` base URL: https://example.com/ - Path: https://api.github.com/user - Result: https://api.github.com/user - -* `ChopperClient` base URL: https://example.com/ - Service base URL: profile - Path: https://api.github.com/user - Result: https://api.github.com/user +| Variable | URI | +|------------|-----------------------------| +| base URL | https://example.com/ | +| Path | profile | +| **Result** | https://example.com/profile | + +| Variable | URI | +|------------------|-----------------------------------| +| base URL | https://example.com/ | +| Service base URL | profile | +| Path | /image | +| **Result** | https://example.com/profile/image | + +| Variable | URI | +|------------------|-----------------------------------| +| base URL | https://example.com/ | +| Service base URL | profile | +| Path | image | +| **Result** | https://example.com/profile/image | + +> Chopper detects and handles missing slash (`/`) characters on URL segment borders, but *does not* handle duplicate +> slashes. + +If the service's `baseUrl` concatenated with the request's `path` results in a full URL, the `ChopperClient`'s `baseUrl` +is ignored. + +| Variable | URI | +|------------------|-----------------------------| +| base URL | https://example.com/ | +| Service base URL | https://api.github.com/ | +| Path | user | +| **Result** | https://api.github.com/user | + +A `path` containing a full URL replaces the base URLs of both the `ChopperClient` and the service class entirely for a +request. + +| Variable | URI | +|------------|-----------------------------| +| base URL | https://example.com/ | +| Path | https://api.github.com/user | +| **Result** | https://api.github.com/user | + +| Variable | URI | +|------------------|-----------------------------| +| base URL | https://example.com/ | +| Service base URL | profile | +| Path | https://api.github.com/user | +| **Result** | https://api.github.com/user | ## Path parameters -Dynamic path parameters can be defined in the URL with replacement blocks. A replacement block is an alphanumeric substring of the path surrounded by `{` and `}`. In the following example `{id}` is a replacement block. +Dynamic path parameters can be defined in the URL with replacement blocks. A replacement block is an alphanumeric +substring of the path surrounded by `{` and `}`. In the following example `{id}` is a replacement block. ```dart @Get(path: "/{id}") ``` -Use the `@Path()` annotation to bind a parameter to a replacement block. This way the parameter's name must match a replacement block's string. +Use the `@Path()` annotation to bind a parameter to a replacement block. This way the parameter's name must match a +replacement block's string. ```dart @Get(path: "/{id}") Future getItemById(@Path() String id); ``` -As an alternative, you can set the `@Path` annotation's `name` parameter to match a replacement block's string while using a different parameter name, like in the following example: +As an alternative, you can set the `@Path` annotation's `name` parameter to match a replacement block's string while +using a different parameter name, like in the following example: ```dart @Get(path: "/{id}") @@ -68,7 +110,8 @@ Future getItemById(@Path("id") int itemId); ## Query parameters -Dynamic query parameters can be added to the URL by adding parameters to a request method annotated with the `@Query` annotation. Default values are supported. +Dynamic query parameters can be added to the URL by adding parameters to a request method annotated with the `@Query` +annotation. Default values are supported. ```dart Future search( @@ -77,7 +120,8 @@ Future search( }); ``` -If the parameter of the `@Query` annotation is not set, Chopper will use the actual name of the annotated parameter as the key for the query parameter in the URL. +If the parameter of the `@Query` annotation is not set, Chopper will use the actual name of the annotated parameter as +the key for the query parameter in the URL. If you prefer to pass a `Map` of query parameters, you can do so with the `@QueryMap` annotation. @@ -97,50 +141,58 @@ Future postData(@Body() String data); {% hint style="warning" %} Chopper does not automatically convert `Object`s to `Map`then `JSON`. -You have to pass a [Converter](converters/converters.md) instance to a `ChopperClient` for JSON conversion to happen. See [built\_value\_converter](converters/built-value-converter.md#built-value) for an example Converter implementation. +You have to pass a [Converter](converters/converters.md) instance to a `ChopperClient` for JSON conversion to happen. +See [built\_value\_converter](converters/built-value-converter.md#built-value) for an example Converter implementation. {% endhint %} ## Headers -Request headers can be set by providing a `Map` object to the `headers` parameter each of the HTTP verb annotations have. +Request headers can be set by providing a `Map` object to the `headers` parameter each of the HTTP verb +annotations have. ```dart @Get(path: "/", headers: {"foo": "bar"}) Future fetch(); ``` -The `@Header` annotation can be used on method parameters to set headers dynamically for each request call. +The `@Header` annotation can be used on method parameters to set headers dynamically for each request call. ```dart @Get(path: "/") Future fetch(@Header("foo") String bar); ``` -> Setting request headers dynamically is also supported by [Interceptors](interceptors.md) and [Converters](converters/converters.md). +> Setting request headers dynamically is also supported by [Interceptors](interceptors.md) +> and [Converters](converters/converters.md). > -> As Chopper invokes Interceptors and Converter(s) *after* creating a Request, Interceptors and Converters *can* override headers set with the `headers` parameter or `@Header` annotations. +> As Chopper invokes Interceptors and Converter(s) *after* creating a Request, Interceptors and Converters *can* +> override headers set with the `headers` parameter or `@Header` annotations. ## Sending `application/x-www-form-urlencoded` data -If no Converter is specified for a request (neither on a `ChopperClient` nor with the `@FactoryConverter` annotation) and the request body is of type `Map`, the body will be sent as form URL encoded data. +If no Converter is specified for a request (neither on a `ChopperClient` nor with the `@FactoryConverter` annotation) +and the request body is of type `Map`, the body will be sent as form URL encoded data. > This is the default behavior of the http package. -You can also use `FormUrlEncodedConverter` that will add the correct `content-type` and convert a `Map` into `Map` for requests. +You can also use `FormUrlEncodedConverter` that will add the correct `content-type` and convert a `Map` +into `Map` for requests. ```dart + final chopper = ChopperClient( - converter: FormUrlEncodedConverter(), + converter: FormUrlEncodedConverter(), ); ``` ### On a single method -To do only a single type of request with form encoding in a service, use the provided `FormUrlEncodedConverter`'s `requestFactory` method with the `@FactoryConverter` annotation. +To do only a single type of request with form encoding in a service, use the provided `FormUrlEncodedConverter`' +s `requestFactory` method with the `@FactoryConverter` annotation. ```dart @Post( - path: "form", + path: "form", headers: {contentTypeKey: formEncodedHeaders}, ) @FactoryConverter( @@ -151,7 +203,8 @@ Future postForm(@Body() Map fields); ### Defining fields individually -To specify fields individually, use the `@Field` annotation on method parameters. If the field's name is not provided, the parameter's name is used as the field's name. +To specify fields individually, use the `@Field` annotation on method parameters. If the field's name is not provided, +the parameter's name is used as the field's name. ```dart @Post(path: "form") @@ -161,5 +214,53 @@ To specify fields individually, use the `@Field` annotation on method parameters Future post(@Field() String foo, @Field("b") int bar); ``` -## Sending files +## Sending files with `@multipart` + +### Sending a file in bytes as `List` using `@PartFile` + +```dart +@Post(path: 'file') +@multipart +Future postFile(@PartFile('file') List bytes,); +``` + +### Sending a file as `MultipartFile` using `@PartFile` with extra parameters via `@Part` + +```dart +@Post(path: 'file') +@multipart +Future postMultipartFile(@PartFile() MultipartFile file, { + @Part() String? id, +}); +``` + +### Sending multiple files as `List` using `@PartFile` + +```dart +@Post(path: 'files') +@multipart +Future postListFiles(@PartFile() List files); +``` + +## Defining Responses + +ChopperService methods need to return a `Future`. Its possible to define return types of `Future` or `Future>` where `T` is the type of the response body. +When `Response` is not needed for a request its also possible to define a return type of `Future` where `T` is the type of the response body. + +Chopper will generate a client which will return the specified return type. When the method doesn't directly returns `Response` and the HTTP call fails a exception is thrown. + +```dart +// Returns a Response +@Get(path: "/") +Future fetch(); + +// Returns a Response +@Get(path: "/") +Future> fetch(); + +// Returns a MyClass +@Get(path: "/") +Future fetch(); +``` +> Note: Chopper doesn't convert response bodies by itself to dart object. You need to use a [Converter](converters/converters.md) for that. \ No newline at end of file From 33615edc89f375bf9f2fcecaac9ce74669e9e82c Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Mon, 8 Jan 2024 16:59:30 +0000 Subject: [PATCH 126/171] :bug: fix chopper_generator regression introduced in #534 (#549) --- chopper/test/test_service.chopper.dart | 26 +++++++++++++++++++ chopper/test/test_service.dart | 9 +++++++ chopper_generator/lib/src/generator.dart | 4 +-- .../test/test_service.chopper.dart | 26 +++++++++++++++++++ chopper_generator/test/test_service.dart | 9 +++++++ 5 files changed, 71 insertions(+), 3 deletions(-) diff --git a/chopper/test/test_service.chopper.dart b/chopper/test/test_service.chopper.dart index ed9a98ed..c6335821 100644 --- a/chopper/test/test_service.chopper.dart +++ b/chopper/test/test_service.chopper.dart @@ -675,4 +675,30 @@ final class _$HttpTestService extends HttpTestService { ); return client.send($request); } + + @override + Future> publish( + String reviewId, + List negatives, + List positives, [ + String? signature, + ]) { + final Uri $url = Uri.parse('/test/publish'); + final $body = { + 'review_id': reviewId, + 'negatives': negatives, + 'positives': positives, + 'signature': signature, + }; + final Request $request = Request( + 'POST', + $url, + client.baseUrl, + body: $body, + ); + return client.send( + $request, + requestConverter: FormUrlEncodedConverter.requestFactory, + ); + } } diff --git a/chopper/test/test_service.dart b/chopper/test/test_service.dart index ff34b785..6ae6cb4e 100644 --- a/chopper/test/test_service.dart +++ b/chopper/test/test_service.dart @@ -204,6 +204,15 @@ abstract class HttpTestService extends ChopperService { @Header('x-double') double? doubleHeader, @Header('x-enum') ExampleEnum? enumHeader, }); + + @Post(path: 'publish') + @FactoryConverter(request: FormUrlEncodedConverter.requestFactory) + Future> publish( + @Field('review_id') final String reviewId, + @Field() final List negatives, + @Field() final List positives, [ + @Field() final String? signature, + ]); } Request customConvertRequest(Request req) { diff --git a/chopper_generator/lib/src/generator.dart b/chopper_generator/lib/src/generator.dart index a39c1181..f35a341a 100644 --- a/chopper_generator/lib/src/generator.dart +++ b/chopper_generator/lib/src/generator.dart @@ -470,9 +470,7 @@ final class ChopperGenerator // ignore: deprecated_member_use function.enclosingElement is ClassElement // ignore: deprecated_member_use - ? refer(function.enclosingElement!.name!) - .property(function.name!) - .toString() + ? '${function.enclosingElement!.name}.${function.name}' : function.name!; static Map _getAnnotation( diff --git a/chopper_generator/test/test_service.chopper.dart b/chopper_generator/test/test_service.chopper.dart index 16004b15..262dfb11 100644 --- a/chopper_generator/test/test_service.chopper.dart +++ b/chopper_generator/test/test_service.chopper.dart @@ -662,4 +662,30 @@ final class _$HttpTestService extends HttpTestService { ); return client.send($request); } + + @override + Future> publish( + String reviewId, + List negatives, + List positives, [ + String? signature, + ]) { + final Uri $url = Uri.parse('/test/publish'); + final $body = { + 'review_id': reviewId, + 'negatives': negatives, + 'positives': positives, + 'signature': signature, + }; + final Request $request = Request( + 'POST', + $url, + client.baseUrl, + body: $body, + ); + return client.send( + $request, + requestConverter: FormUrlEncodedConverter.requestFactory, + ); + } } diff --git a/chopper_generator/test/test_service.dart b/chopper_generator/test/test_service.dart index 58dce35a..1fae94fc 100644 --- a/chopper_generator/test/test_service.dart +++ b/chopper_generator/test/test_service.dart @@ -197,6 +197,15 @@ abstract class HttpTestService extends ChopperService { @Header('x-double') double? doubleHeader, @Header('x-enum') ExampleEnum? enumHeader, }); + + @Post(path: 'publish') + @FactoryConverter(request: FormUrlEncodedConverter.requestFactory) + Future> publish( + @Field('review_id') final String reviewId, + @Field() final List negatives, + @Field() final List positives, [ + @Field() final String? signature, + ]); } Request customConvertRequest(Request req) { From a0d5bc54b4b775a35edc901893fae184ee45f411 Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Mon, 15 Jan 2024 07:31:28 +0000 Subject: [PATCH 127/171] :twisted_rightwards_arrows: post release branch sync (#556) --- chopper/CHANGELOG.md | 10 ++++++++++ chopper/pubspec.yaml | 4 ++-- chopper_generator/CHANGELOG.md | 5 +++++ chopper_generator/pubspec.yaml | 4 ++-- 4 files changed, 19 insertions(+), 4 deletions(-) diff --git a/chopper/CHANGELOG.md b/chopper/CHANGELOG.md index b15c91b9..6c82fa4d 100644 --- a/chopper/CHANGELOG.md +++ b/chopper/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## 7.1.0+1 + +- Bump `chopper_generator` version requirement to 7.1.0 + +## 7.1.0 + +- Add ability to omit `Response` in service ([#545](https://github.com/lejard-h/chopper/pull/545)) +- Add helper function for fetching errors of specific type ([#543](https://github.com/lejard-h/chopper/pull/543)) +- Improve documentation ([#548](https://github.com/lejard-h/chopper/pull/548)) + ## 7.0.10 - Enable the user to specify non-String type header values by calling `.toString()` on any non-String Dart type. ([#538](https://github.com/lejard-h/chopper/pull/538)) diff --git a/chopper/pubspec.yaml b/chopper/pubspec.yaml index 9bd3a192..5f3bb6e4 100644 --- a/chopper/pubspec.yaml +++ b/chopper/pubspec.yaml @@ -1,6 +1,6 @@ name: chopper description: Chopper is an http client generator using source_gen, inspired by Retrofit -version: 7.0.10 +version: 7.1.0+1 documentation: https://hadrien-lejard.gitbook.io/chopper repository: https://github.com/lejard-h/chopper @@ -25,7 +25,7 @@ dev_dependencies: lints: ">=2.1.1 <4.0.0" test: ^1.24.4 transparent_image: ^2.0.1 - chopper_generator: ^7.0.0 + chopper_generator: ^7.1.0 dependency_overrides: chopper_generator: diff --git a/chopper_generator/CHANGELOG.md b/chopper_generator/CHANGELOG.md index 2d30f9e0..6a1cd984 100644 --- a/chopper_generator/CHANGELOG.md +++ b/chopper_generator/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## 7.1.0 + +- Add ability to omit `Response` in service ([#545](https://github.com/lejard-h/chopper/pull/545)) +- Fix `FactoryConverter` regression introduced in v7.0.7 ([#549](https://github.com/lejard-h/chopper/pull/549)) + ## 7.0.7 - Enable the user to specify non-String type header values by calling `.toString()` on any non-String Dart type. ([#538](https://github.com/lejard-h/chopper/pull/538)) diff --git a/chopper_generator/pubspec.yaml b/chopper_generator/pubspec.yaml index 6ee77f1c..56c4f80a 100644 --- a/chopper_generator/pubspec.yaml +++ b/chopper_generator/pubspec.yaml @@ -1,6 +1,6 @@ name: chopper_generator description: Chopper is an http client generator using source_gen, inspired by Retrofit -version: 7.0.7 +version: 7.1.0 documentation: https://hadrien-lejard.gitbook.io/chopper repository: https://github.com/lejard-h/chopper @@ -11,7 +11,7 @@ dependencies: analyzer: ">=5.13.0 <7.0.0" build: ^2.4.1 built_collection: ^5.1.1 - chopper: ^7.0.0 + chopper: ^7.1.0 code_builder: ^4.5.0 dart_style: ^2.3.2 logging: ^1.2.0 From 78fae304fbebed5d1c8110399ed5bdc1e74867d5 Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Mon, 15 Jan 2024 07:42:08 +0000 Subject: [PATCH 128/171] :construction_worker: use Dependabot to check for workflow action updates (#557) --- .github/dependabot.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 68152415..cc598d95 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,5 +1,10 @@ version: 2 updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + target-branch: "develop" - package-ecosystem: "pub" directory: "/chopper" schedule: From d36921729f2dad7f135f19ea47ffd6ef22bd75ab Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Sun, 21 Jan 2024 15:24:09 +0000 Subject: [PATCH 129/171] :sparkles: add option to override build_extension (#562) --- .../lib/src/builder_factory.dart | 64 +++++++++++++++---- chopper_generator/pubspec.yaml | 1 + faq.md | 16 +++++ 3 files changed, 68 insertions(+), 13 deletions(-) diff --git a/chopper_generator/lib/src/builder_factory.dart b/chopper_generator/lib/src/builder_factory.dart index f5ff89d1..34e6108f 100644 --- a/chopper_generator/lib/src/builder_factory.dart +++ b/chopper_generator/lib/src/builder_factory.dart @@ -1,23 +1,61 @@ import 'package:build/build.dart'; import 'package:chopper/chopper.dart' show ChopperApi; import 'package:source_gen/source_gen.dart'; +import 'package:yaml/yaml.dart'; import 'generator.dart'; /// Creates a [PartBuilder] used to generate code for [ChopperApi] annotated /// classes. The [options] are provided by Dart's build system and read from the /// `build.yaml` file. -Builder chopperGeneratorFactory(BuilderOptions options) => PartBuilder( +Builder chopperGeneratorFactory(BuilderOptions options) { + final String buildExtension = _getBuildExtension(options); + + return PartBuilder( + [const ChopperGenerator()], + buildExtension, + header: options.config['header'], + formatOutput: PartBuilder( [const ChopperGenerator()], - '.chopper.dart', - header: options.config['header'], - formatOutput: - PartBuilder([const ChopperGenerator()], '.chopper.dart').formatOutput, - options: !options.config.containsKey('build_extensions') - ? options.overrideWith( - BuilderOptions({ - 'build_extensions': {'.dart': '.chopper.dart'}, - }), - ) - : options, - ); + buildExtension, + ).formatOutput, + options: !options.config.containsKey('build_extensions') + ? options.overrideWith( + BuilderOptions({ + 'build_extensions': { + '.dart': [buildExtension] + }, + }), + ) + : options, + ); +} + +/// Returns the build extension for the generated file. +/// +/// If the `build.yaml` file contains a `build_extensions` key, it will be used +/// to determine the extension. Otherwise, the default extension `.chopper.dart` +/// will be used. +/// +/// Example `build.yaml`: +/// +/// ```yaml +/// targets: +/// $default: +/// builders: +/// chopper_generator: +/// options: +/// build_extensions: {".dart": [".chopper.g.dart"]} +/// ``` +String _getBuildExtension(BuilderOptions options) { + if (options.config.containsKey('build_extensions')) { + final YamlMap buildExtensions = options.config['build_extensions']; + if (buildExtensions.containsKey('.dart')) { + final YamlList dartBuildExtensions = buildExtensions['.dart']; + if (dartBuildExtensions.isNotEmpty) { + return dartBuildExtensions.first; + } + } + } + return '.chopper.dart'; +} diff --git a/chopper_generator/pubspec.yaml b/chopper_generator/pubspec.yaml index 56c4f80a..60412565 100644 --- a/chopper_generator/pubspec.yaml +++ b/chopper_generator/pubspec.yaml @@ -17,6 +17,7 @@ dependencies: logging: ^1.2.0 meta: ^1.9.1 source_gen: ^1.4.0 + yaml: ^3.1.2 dev_dependencies: build_runner: ^2.4.6 diff --git a/faq.md b/faq.md index a5ffeef3..a79ed397 100644 --- a/faq.md +++ b/faq.md @@ -347,6 +347,22 @@ It goes without saying that running the code generation is a pre-requisite at th flutter pub run build_runner build ``` +##### Changing the default extension of the generated files + +If you want to change the default extension of the generated files from `.chopper.dart` to +something else, you can do so by adding the following to your `build.yaml` file: + +```yaml +targets: + $default: + builders: + chopper_generator: + options: + # This assumes you want the files to end with `.chopper.g.dart` + # instead of the default `.chopper.dart`. + build_extensions: { ".dart": [ ".chopper.g.dart" ] } +``` + #### Configure a WorkerPool and run the example ```dart From b8f2fe2747bacf037d5485f40eda5ad7e5dcffdd Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Tue, 23 Jan 2024 04:45:39 +0000 Subject: [PATCH 130/171] :twisted_rightwards_arrows: post release branch sync (#565) --- chopper_generator/CHANGELOG.md | 4 ++++ chopper_generator/pubspec.yaml | 2 +- faq.md | 16 ---------------- 3 files changed, 5 insertions(+), 17 deletions(-) diff --git a/chopper_generator/CHANGELOG.md b/chopper_generator/CHANGELOG.md index 6a1cd984..08e09416 100644 --- a/chopper_generator/CHANGELOG.md +++ b/chopper_generator/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 7.1.1 + +- Add option to override build_extension via build.yaml ([#562](https://github.com/lejard-h/chopper/pull/562)) + ## 7.1.0 - Add ability to omit `Response` in service ([#545](https://github.com/lejard-h/chopper/pull/545)) diff --git a/chopper_generator/pubspec.yaml b/chopper_generator/pubspec.yaml index 60412565..43870f85 100644 --- a/chopper_generator/pubspec.yaml +++ b/chopper_generator/pubspec.yaml @@ -1,6 +1,6 @@ name: chopper_generator description: Chopper is an http client generator using source_gen, inspired by Retrofit -version: 7.1.0 +version: 7.1.1 documentation: https://hadrien-lejard.gitbook.io/chopper repository: https://github.com/lejard-h/chopper diff --git a/faq.md b/faq.md index a79ed397..a5ffeef3 100644 --- a/faq.md +++ b/faq.md @@ -347,22 +347,6 @@ It goes without saying that running the code generation is a pre-requisite at th flutter pub run build_runner build ``` -##### Changing the default extension of the generated files - -If you want to change the default extension of the generated files from `.chopper.dart` to -something else, you can do so by adding the following to your `build.yaml` file: - -```yaml -targets: - $default: - builders: - chopper_generator: - options: - # This assumes you want the files to end with `.chopper.g.dart` - # instead of the default `.chopper.dart`. - build_extensions: { ".dart": [ ".chopper.g.dart" ] } -``` - #### Configure a WorkerPool and run the example ```dart From 168982f30761447317a5a7528821952881a1946a Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Wed, 24 Jan 2024 07:23:46 +0000 Subject: [PATCH 131/171] :rewind: revert faq.md (#566) --- faq.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/faq.md b/faq.md index a5ffeef3..a79ed397 100644 --- a/faq.md +++ b/faq.md @@ -347,6 +347,22 @@ It goes without saying that running the code generation is a pre-requisite at th flutter pub run build_runner build ``` +##### Changing the default extension of the generated files + +If you want to change the default extension of the generated files from `.chopper.dart` to +something else, you can do so by adding the following to your `build.yaml` file: + +```yaml +targets: + $default: + builders: + chopper_generator: + options: + # This assumes you want the files to end with `.chopper.g.dart` + # instead of the default `.chopper.dart`. + build_extensions: { ".dart": [ ".chopper.g.dart" ] } +``` + #### Configure a WorkerPool and run the example ```dart From be1c370e4aed15f4d1e42896aa3184548dd8606c Mon Sep 17 00:00:00 2001 From: Job Guldemeester Date: Thu, 25 Jan 2024 10:50:02 +0100 Subject: [PATCH 132/171] :sparkles: Added Target annotations for the annotations (#567) --- chopper/lib/src/annotations.dart | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/chopper/lib/src/annotations.dart b/chopper/lib/src/annotations.dart index 7ee1552b..ffc690e7 100644 --- a/chopper/lib/src/annotations.dart +++ b/chopper/lib/src/annotations.dart @@ -4,6 +4,7 @@ import 'package:chopper/src/constants.dart'; import 'package:chopper/src/request.dart'; import 'package:chopper/src/response.dart'; import 'package:meta/meta.dart'; +import 'package:meta/meta_meta.dart'; /// {@template ChopperApi} /// Defines a Chopper API. @@ -22,6 +23,7 @@ import 'package:meta/meta.dart'; /// See [Method] to define an HTTP request /// {@endtemplate} @immutable +@Target({TargetKind.classType}) final class ChopperApi { /// A part of a URL that every request defined inside a class annotated with [ChopperApi] will be prefixed with. /// @@ -49,6 +51,7 @@ final class ChopperApi { /// ``` /// {@endtemplate} @immutable +@Target({TargetKind.parameter}) final class Path { /// Name is used to bind a method parameter to /// a URL path parameter. @@ -75,6 +78,7 @@ final class Path { /// See [QueryMap] to pass an [Map] as value /// {@endtemplate} @immutable +@Target({TargetKind.parameter}) final class Query { /// Name is used to bind a method parameter to /// the query parameter. @@ -103,6 +107,7 @@ final class Query { /// ``` /// {@endtemplate} @immutable +@Target({TargetKind.parameter}) final class QueryMap { /// {@macro QueryMap} const QueryMap(); @@ -120,6 +125,7 @@ final class QueryMap { /// See [Converter] to apply conversion to the body. /// {@endtemplate} @immutable +@Target({TargetKind.parameter}) final class Body { /// {@macro Body} const Body(); @@ -136,6 +142,7 @@ final class Body { /// ``` /// {@endtemplate} @immutable +@Target({TargetKind.parameter}) final class Header { /// Name is used to bind a method parameter to /// a header name. @@ -169,6 +176,7 @@ final class Header { /// A [Converter] needs to be specified for conversion. /// {@endtemplate} @immutable +@Target({TargetKind.method}) sealed class Method { /// HTTP method for the request final String method; @@ -232,6 +240,7 @@ sealed class Method { /// Defines a method as an HTTP GET request. /// {@endtemplate} @immutable +@Target({TargetKind.method}) final class Get extends Method { /// {@macro Get} const Get({ @@ -249,6 +258,7 @@ final class Get extends Method { /// Use the [Body] annotation to pass data to send. /// {@endtemplate} @immutable +@Target({TargetKind.method}) final class Post extends Method { /// {@macro Post} const Post({ @@ -264,6 +274,7 @@ final class Post extends Method { /// Defines a method as an HTTP DELETE request. /// {@endtemplate} @immutable +@Target({TargetKind.method}) final class Delete extends Method { /// {@macro Delete} const Delete({ @@ -281,6 +292,7 @@ final class Delete extends Method { /// Use the [Body] annotation to pass data to send. /// {@endtemplate} @immutable +@Target({TargetKind.method}) final class Put extends Method { /// {@macro Put} const Put({ @@ -297,6 +309,7 @@ final class Put extends Method { /// Use the [Body] annotation to pass data to send. /// {@endtemplate} @immutable +@Target({TargetKind.method}) final class Patch extends Method { /// {@macro Patch} const Patch({ @@ -312,6 +325,7 @@ final class Patch extends Method { /// Defines a method as an HTTP HEAD request. /// {@endtemplate} @immutable +@Target({TargetKind.method}) final class Head extends Method { /// {@macro Head} const Head({ @@ -327,6 +341,7 @@ final class Head extends Method { /// Defines a method as an HTTP OPTIONS request. /// {@endtemplate} @immutable +@Target({TargetKind.method}) final class Options extends Method { /// {@macro Options} const Options({ @@ -377,6 +392,7 @@ typedef ConvertResponse = FutureOr Function(Response response); /// ``` /// {@endtemplate} @immutable +@Target({TargetKind.method}) final class FactoryConverter { final ConvertRequest? request; final ConvertResponse? response; @@ -399,6 +415,7 @@ final class FactoryConverter { /// Will be converted to `{ 'name': value }`. /// {@endtemplate} @immutable +@Target({TargetKind.parameter}) final class Field { /// Name can be use to specify the name of the field /// ```dart @@ -420,6 +437,7 @@ final class Field { /// ``` /// {@endtemplate} @immutable +@Target({TargetKind.parameter}) final class FieldMap { /// {@macro FieldMap} const FieldMap(); @@ -438,6 +456,7 @@ final class FieldMap { /// Use [PartFile] annotation to send `File` or `List`. /// {@endtemplate} @immutable +@Target({TargetKind.method}) final class Multipart { /// {@macro Multipart} const Multipart(); @@ -451,6 +470,7 @@ final class Multipart { /// Also accepts `MultipartFile` (from package:http). /// {@endtemplate} @immutable +@Target({TargetKind.parameter}) final class Part { final String? name; @@ -468,6 +488,7 @@ final class Part { /// ``` /// {@endtemplate} @immutable +@Target({TargetKind.parameter}) final class PartMap { /// {@macro PartMap} const PartMap(); @@ -488,6 +509,7 @@ final class PartMap { /// - `MultipartFile` (from package:http) /// {@endtemplate} @immutable +@Target({TargetKind.parameter}) final class PartFile { final String? name; @@ -505,6 +527,7 @@ final class PartFile { /// ``` /// {@endtemplate} @immutable +@Target({TargetKind.parameter}) final class PartFileMap { /// {@macro PartFileMap} const PartFileMap(); From 63e6d002df0a859ca083ca45cd511b34e470e992 Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Sun, 28 Jan 2024 09:59:34 +0000 Subject: [PATCH 133/171] :twisted_rightwards_arrows: post release branch sync (#569) --- chopper/CHANGELOG.md | 4 ++++ chopper/pubspec.yaml | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/chopper/CHANGELOG.md b/chopper/CHANGELOG.md index 6c82fa4d..60a8bc9f 100644 --- a/chopper/CHANGELOG.md +++ b/chopper/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 7.1.1 + +- Add `Target` annotations ([#567](https://github.com/lejard-h/chopper/pull/567)) + ## 7.1.0+1 - Bump `chopper_generator` version requirement to 7.1.0 diff --git a/chopper/pubspec.yaml b/chopper/pubspec.yaml index 5f3bb6e4..b4a729d0 100644 --- a/chopper/pubspec.yaml +++ b/chopper/pubspec.yaml @@ -1,6 +1,6 @@ name: chopper description: Chopper is an http client generator using source_gen, inspired by Retrofit -version: 7.1.0+1 +version: 7.1.1 documentation: https://hadrien-lejard.gitbook.io/chopper repository: https://github.com/lejard-h/chopper From 5585ffb2bea56c366007b19b38f2d53896cd4eae Mon Sep 17 00:00:00 2001 From: Job Guldemeester Date: Wed, 31 Jan 2024 14:40:33 +0100 Subject: [PATCH 134/171] :bug: Export ChopperHttpException so developer can catch it. (#570) --- chopper/lib/chopper.dart | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/chopper/lib/chopper.dart b/chopper/lib/chopper.dart index 3c84987b..bcd68145 100644 --- a/chopper/lib/chopper.dart +++ b/chopper/lib/chopper.dart @@ -6,11 +6,12 @@ library chopper; export 'src/annotations.dart'; export 'src/authenticator.dart'; export 'src/base.dart'; +export 'src/chopper_http_exception.dart'; +export 'src/chopper_log_record.dart'; export 'src/constants.dart'; export 'src/extensions.dart'; -export 'src/interceptor.dart'; export 'src/http_logging_interceptor.dart'; -export 'src/chopper_log_record.dart'; +export 'src/interceptor.dart'; export 'src/request.dart'; export 'src/response.dart'; export 'src/utils.dart' hide mapToQuery; From 3af6e4e06c35aa8768432e9ec3516de8cb9de9ad Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 Feb 2024 19:58:48 +0000 Subject: [PATCH 135/171] :arrow_up: Bump codecov/codecov-action from 3 to 4 (#571) Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 3 to 4. - [Release notes](https://github.com/codecov/codecov-action/releases) - [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/codecov/codecov-action/compare/v3...v4) --- updated-dependencies: - dependency-name: codecov/codecov-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/dart.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/dart.yml b/.github/workflows/dart.yml index 91f883dd..0dbd26b5 100644 --- a/.github/workflows/dart.yml +++ b/.github/workflows/dart.yml @@ -133,7 +133,7 @@ jobs: if: "always() && steps.chopper_pub_upgrade.conclusion == 'success'" working-directory: chopper - name: Upload coverage to codecov.io - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v4 with: files: chopper/coverage/lcov.info fail_ci_if_error: true @@ -148,7 +148,7 @@ jobs: if: "always() && steps.chopper_built_value_pub_upgrade.conclusion == 'success'" working-directory: chopper_built_value - name: Upload coverage to codecov.io - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v4 with: files: chopper_built_value/coverage/lcov.info fail_ci_if_error: true @@ -163,7 +163,7 @@ jobs: if: "always() && steps.chopper_generator_pub_upgrade.conclusion == 'success'" working-directory: chopper_generator - name: Upload coverage to codecov.io - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v4 with: files: chopper_generator/coverage/lcov.info fail_ci_if_error: true From 4a1de4498e30ad1a6b0bfcf7ac7768555698dbe2 Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Thu, 8 Feb 2024 14:21:56 +0000 Subject: [PATCH 136/171] Revert ":arrow_up: Bump codecov/codecov-action from 3 to 4 (#571)" (#572) --- .github/workflows/dart.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/dart.yml b/.github/workflows/dart.yml index 0dbd26b5..91f883dd 100644 --- a/.github/workflows/dart.yml +++ b/.github/workflows/dart.yml @@ -133,7 +133,7 @@ jobs: if: "always() && steps.chopper_pub_upgrade.conclusion == 'success'" working-directory: chopper - name: Upload coverage to codecov.io - uses: codecov/codecov-action@v4 + uses: codecov/codecov-action@v3 with: files: chopper/coverage/lcov.info fail_ci_if_error: true @@ -148,7 +148,7 @@ jobs: if: "always() && steps.chopper_built_value_pub_upgrade.conclusion == 'success'" working-directory: chopper_built_value - name: Upload coverage to codecov.io - uses: codecov/codecov-action@v4 + uses: codecov/codecov-action@v3 with: files: chopper_built_value/coverage/lcov.info fail_ci_if_error: true @@ -163,7 +163,7 @@ jobs: if: "always() && steps.chopper_generator_pub_upgrade.conclusion == 'success'" working-directory: chopper_generator - name: Upload coverage to codecov.io - uses: codecov/codecov-action@v4 + uses: codecov/codecov-action@v3 with: files: chopper_generator/coverage/lcov.info fail_ci_if_error: true From 655aafa5995ee01e251f530094520dfd558538e1 Mon Sep 17 00:00:00 2001 From: wenchieh Date: Fri, 8 Mar 2024 04:53:09 +0800 Subject: [PATCH 137/171] Support @FormUrlEncoded (#579) --- chopper/lib/src/annotations.dart | 29 +++++- chopper_generator/lib/src/generator.dart | 94 +++++++++++++++--- .../test/test_service.chopper.dart | 95 +++++++++++++++++++ chopper_generator/test/test_service.dart | 27 ++++++ requests.md | 63 +++++++----- 5 files changed, 269 insertions(+), 39 deletions(-) diff --git a/chopper/lib/src/annotations.dart b/chopper/lib/src/annotations.dart index ffc690e7..b4d84aca 100644 --- a/chopper/lib/src/annotations.dart +++ b/chopper/lib/src/annotations.dart @@ -433,7 +433,7 @@ final class Field { /// /// ```dart /// @Post(path: '/something') -/// Future fetch(@FieldMap List> query); +/// Future fetch(@FieldMap Map query); /// ``` /// {@endtemplate} @immutable @@ -533,6 +533,30 @@ final class PartFileMap { const PartFileMap(); } +/// {@template FormUrlEncoded} +/// +/// +/// Denotes that the request body will use form URL encoding. Fields should be declared as parameters +/// and annotated with [Field]/[FieldMap]. +/// +/// Requests made with this annotation will have application/x-www-form-urlencoded MIME +/// type. Field names and values will be UTF-8 encoded before being URI-encoded in accordance to RFC-3986. +/// +/// +/// ```dart +/// @Post(path: '/something') +/// @FormUrlEncoded +/// Future fetch(@Field("param") String? param); +/// ``` +/// {@endtemplate} +@immutable +@Target({TargetKind.method}) +final class FormUrlEncoded { + /// {@macro FormUrlEncoded} + const FormUrlEncoded(); +} + /// {@macro ChopperApi} const chopperApi = ChopperApi(); @@ -595,3 +619,6 @@ const partFile = PartFile(); /// {@macro PartFileMap} const partFileMap = PartFileMap(); + +/// {@macro FormUrlEncoded} +const formUrlEncoded = FormUrlEncoded(); diff --git a/chopper_generator/lib/src/generator.dart b/chopper_generator/lib/src/generator.dart index f35a341a..b7946c75 100644 --- a/chopper_generator/lib/src/generator.dart +++ b/chopper_generator/lib/src/generator.dart @@ -150,6 +150,7 @@ final class ChopperGenerator ) { final ConstantReader? method = _getMethodAnnotation(m); final bool multipart = _hasAnnotation(m, chopper.Multipart); + final bool formUrlEncoded = _hasAnnotation(m, chopper.FormUrlEncoded); final ConstantReader? factoryConverter = _getFactoryConverterAnnotation(m); final Map body = _getAnnotation(m, chopper.Body); @@ -172,7 +173,7 @@ final class ChopperGenerator final Map fileFieldMap = _getAnnotation(m, chopper.PartFileMap); - final Code? headers = _generateHeaders(m, method!); + final Code? headers = _generateHeaders(m, method!, formUrlEncoded); final Expression url = _generateUrl( method, paths, @@ -298,15 +299,21 @@ final class ChopperGenerator bool hasBody = body.isNotEmpty || fields.isNotEmpty; if (hasBody) { if (body.isNotEmpty) { + final DartType bodyType = m.parameters + .firstWhere((p) => _typeChecker(chopper.Body).hasAnnotationOf(p)) + .type; + final Expression map = (formUrlEncoded && + _isMap(bodyType) && + !_isMapStringString(bodyType)) + ? _generateMapToStringExpression(refer(body.keys.first)) + : refer(body.keys.first); blocks.add( - declareFinal(Vars.body.toString()) - .assign(refer(body.keys.first)) - .statement, + declareFinal(Vars.body.toString()).assign(map).statement, ); } else { blocks.add( declareFinal(Vars.body.toString()) - .assign(_generateMap(fields)) + .assign(_generateMap(fields, enableToString: formUrlEncoded)) .statement, ); } @@ -314,17 +321,23 @@ final class ChopperGenerator final bool hasFieldMap = fieldMap.isNotEmpty; if (hasFieldMap) { + final DartType fieldMapType = m.parameters + .firstWhere( + (p) => _typeChecker(chopper.FieldMap).hasAnnotationOf(p)) + .type; + final Expression map = + (formUrlEncoded && !_isMapStringString(fieldMapType)) + ? _generateMapToStringExpression(refer(fieldMap.keys.first)) + : refer(fieldMap.keys.first); if (hasBody) { blocks.add( refer(Vars.body.toString()).property('addAll').call( - [refer(fieldMap.keys.first)], + [map], ).statement, ); } else { blocks.add( - declareFinal(Vars.body.toString()) - .assign(refer(fieldMap.keys.first)) - .statement, + declareFinal(Vars.body.toString()).assign(map).statement, ); } } @@ -466,6 +479,26 @@ final class ChopperGenerator }); } + static Expression _generateMapToStringExpression(Reference map) { + return map.property('map').call([ + Method((b) => b + ..requiredParameters.add( + Parameter((b) => b..name = 'key'), + ) + ..requiredParameters.add( + Parameter((b) => b..name = 'value'), + ) + ..returns = refer('MapEntry', 'dart.core') + ..body = refer('MapEntry', 'dart.core') + .newInstance([ + refer('key').property('toString').call([]), + refer('value').property('toString').call([]), + ]) + .returned + .statement).closure + ]); + } + static String _factoryForFunction(FunctionTypedElement function) => // ignore: deprecated_member_use function.enclosingElement is ClassElement @@ -546,6 +579,32 @@ final class ChopperGenerator ? type.typeArguments.first : null; + static bool _isMap(DartType type) { + return _typeChecker(Map).isExactlyType(type) || + _typeChecker(Map).isAssignableFromType(type); + } + + static bool _isMapStringString(DartType type) { + if (!_isMap(type)) { + return false; + } + final firsType = type is InterfaceType && type.typeArguments.isNotEmpty + ? type.typeArguments.first + : null; + final secondType = type is InterfaceType && type.typeArguments.length > 1 + ? type.typeArguments[1] + : null; + if (firsType == null || secondType == null) { + return false; + } + return _isString(firsType) && _isString(secondType); + } + + static bool _isString(DartType type) { + return _typeChecker(String).isExactlyType(type) || + _typeChecker(String).isAssignableFromType(type); + } + static bool _isResponse(DartType type) { final DartType? responseType = _genericOf(type); if (responseType == null) return false; @@ -664,17 +723,20 @@ final class ChopperGenerator ); static Expression _generateMap( - Map queries, - ) => + Map queries, { + bool enableToString = false, + }) => literalMap( { for (final MapEntry query in queries.entries) query.value.peek('name')?.stringValue ?? query.key.displayName: - refer(query.key.displayName), + enableToString + ? refer(query.key.displayName).property('toString').call([]) + : refer(query.key.displayName), }, refer('String'), - refer('dynamic'), + refer(enableToString ? 'String' : 'dynamic'), ); static Expression _generateList( @@ -718,6 +780,7 @@ final class ChopperGenerator static Code? _generateHeaders( MethodElement methodElement, ConstantReader method, + bool formUrlEncoded, ) { final StringBuffer codeBuffer = StringBuffer('')..writeln('{'); @@ -756,6 +819,11 @@ final class ChopperGenerator } }); + if (formUrlEncoded) { + codeBuffer + .writeln("'content-type': 'application/x-www-form-urlencoded',"); + } + codeBuffer.writeln('}'); final String code = codeBuffer.toString(); diff --git a/chopper_generator/test/test_service.chopper.dart b/chopper_generator/test/test_service.chopper.dart index 262dfb11..5019d39c 100644 --- a/chopper_generator/test/test_service.chopper.dart +++ b/chopper_generator/test/test_service.chopper.dart @@ -393,6 +393,101 @@ final class _$HttpTestService extends HttpTestService { return client.send($request); } + @override + Future> postFormUrlEncodeBody( + HashMap hashMapBody, + Map map, + ) { + final Uri $url = Uri.parse('/test/formUrlEncoded'); + final Map $headers = { + 'content-type': 'application/x-www-form-urlencoded', + }; + final $body = hashMapBody.map(( + key, + value, + ) { + return MapEntry( + key.toString(), + value.toString(), + ); + }); + $body.addAll(map); + final Request $request = Request( + 'POST', + $url, + client.baseUrl, + body: $body, + headers: $headers, + ); + return client.send($request); + } + + @override + Future> postFormUrlEncodeField( + String a, + String a2, + ) { + final Uri $url = Uri.parse('/test/formUrlEncoded'); + final Map $headers = { + 'content-type': 'application/x-www-form-urlencoded', + }; + final $body = { + 'a': a.toString(), + 'a1': a2.toString(), + }; + final Request $request = Request( + 'POST', + $url, + client.baseUrl, + body: $body, + headers: $headers, + ); + return client.send($request); + } + + @override + Future> postFormUrlEncodeFieldMap(Map c) { + final Uri $url = Uri.parse('/test/formUrlEncoded'); + final Map $headers = { + 'content-type': 'application/x-www-form-urlencoded', + }; + final $body = c; + final Request $request = Request( + 'POST', + $url, + client.baseUrl, + body: $body, + headers: $headers, + ); + return client.send($request); + } + + @override + Future> postFormUrlEncodeFieldDynamicMap( + Map c) { + final Uri $url = Uri.parse('/test/formUrlEncoded'); + final Map $headers = { + 'content-type': 'application/x-www-form-urlencoded', + }; + final $body = c.map(( + key, + value, + ) { + return MapEntry( + key.toString(), + value.toString(), + ); + }); + final Request $request = Request( + 'POST', + $url, + client.baseUrl, + body: $body, + headers: $headers, + ); + return client.send($request); + } + @override Future> postFile(List bytes) { final Uri $url = Uri.parse('/test/file'); diff --git a/chopper_generator/test/test_service.dart b/chopper_generator/test/test_service.dart index 1fae94fc..273e9d73 100644 --- a/chopper_generator/test/test_service.dart +++ b/chopper_generator/test/test_service.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:collection'; import 'dart:convert'; import 'package:chopper/chopper.dart'; @@ -113,6 +114,32 @@ abstract class HttpTestService extends ChopperService { @Part('2') Map b, ); + @Post(path: 'formUrlEncoded') + @FormUrlEncoded() + Future postFormUrlEncodeBody( + @Body() HashMap hashMapBody, + @FieldMap() Map map, + ); + + @Post(path: 'formUrlEncoded') + @FormUrlEncoded() + Future postFormUrlEncodeField( + @Field('a') String a, + @Field('a1') String a2, + ); + + @Post(path: 'formUrlEncoded') + @FormUrlEncoded() + Future postFormUrlEncodeFieldMap( + @FieldMap() Map c, + ); + + @Post(path: 'formUrlEncoded') + @FormUrlEncoded() + Future postFormUrlEncodeFieldDynamicMap( + @FieldMap() Map c, + ); + @Post(path: 'file') @multipart Future postFile( diff --git a/requests.md b/requests.md index 60d7f2f3..4a66090d 100644 --- a/requests.md +++ b/requests.md @@ -2,25 +2,26 @@ ## Available Request annotations -| Annotation | HTTP verb | Description | -|--------------------------------------------|-----------|-----------------------------------------------| -| `@Get()`, `@get` | `GET` | Defines a `GET` request. | -| `@Post()`, `@post` | `POST` | Defines a `POST` request. | -| `@Put()`, `@put` | `PUT` | Defines a `PUT` request. | -| `@Patch()`, `@patch` | `PATCH` | Defines a `PATCH` request. | -| `@Delete()`, `@delete` | `DELETE` | Defines a `DELETE` request. | -| `@Head()`, `@head` | `HEAD` | Defines a `HEAD` request. | -| `@Body()`, `@body` | - | Defines the request's body. | -| `@Multipart()`, `@multipart` | - | Defines a `multipart/form-data` request. | -| `@Query()`, `@query` | - | Defines a query parameter. | -| `@QueryMap()`, `@queryMap` | - | Defines a query parameter map. | -| `@FactoryConverter()`, `@factoryConverter` | - | Defines a request/response converter factory. | -| `@Field()`, `@field` | - | Defines a form field. | -| `@FieldMap()`, `@fieldMap` | - | Defines a form field map. | -| `@Part()`, `@part` | - | Defines a multipart part. | -| `@PartMap()`, `@partMap` | - | Defines a multipart part map. | -| `@PartFile()`, `@partFile` | - | Defines a multipart file part. | -| `@PartFileMap()`, `@partFileMap` | - | Defines a multipart file part map. | +| Annotation | HTTP verb | Description | +|--------------------------------------------|-----------|--------------------------------------------------------| +| `@Get()`, `@get` | `GET` | Defines a `GET` request. | +| `@Post()`, `@post` | `POST` | Defines a `POST` request. | +| `@Put()`, `@put` | `PUT` | Defines a `PUT` request. | +| `@Patch()`, `@patch` | `PATCH` | Defines a `PATCH` request. | +| `@Delete()`, `@delete` | `DELETE` | Defines a `DELETE` request. | +| `@Head()`, `@head` | `HEAD` | Defines a `HEAD` request. | +| `@Body()`, `@body` | - | Defines the request's body. | +| `@FormUrlEncoded`, `@formUrlEncoded` | - | Defines a `application/x-www-form-urlencoded` request. | +| `@Multipart()`, `@multipart` | - | Defines a `multipart/form-data` request. | +| `@Query()`, `@query` | - | Defines a query parameter. | +| `@QueryMap()`, `@queryMap` | - | Defines a query parameter map. | +| `@FactoryConverter()`, `@factoryConverter` | - | Defines a request/response converter factory. | +| `@Field()`, `@field` | - | Defines a form field. | +| `@FieldMap()`, `@fieldMap` | - | Defines a form field map. | +| `@Part()`, `@part` | - | Defines a multipart part. | +| `@PartMap()`, `@partMap` | - | Defines a multipart part map. | +| `@PartFile()`, `@partFile` | - | Defines a multipart file part. | +| `@PartFileMap()`, `@partFileMap` | - | Defines a multipart file part map. | ## Path resolution @@ -170,12 +171,27 @@ Future fetch(@Header("foo") String bar); ## Sending `application/x-www-form-urlencoded` data -If no Converter is specified for a request (neither on a `ChopperClient` nor with the `@FactoryConverter` annotation) +If no Converter (neither on a `ChopperClient` nor with the `@FactoryConverter` annotation) or formUrlEncoded (`@FormUrlEncoded` annotation) is specified for a request and the request body is of type `Map`, the body will be sent as form URL encoded data. > This is the default behavior of the http package. -You can also use `FormUrlEncodedConverter` that will add the correct `content-type` and convert a `Map` +### FormUrlEncoded annotation + +We recommend annotation `@formUrlEncoded` on method that will add the correct `content-type` and convert a `Map` +into `Map` for requests. + +```dart +@Post( + path: "form", +) +@formUrlEncoded +Future postForm(@Body() Map fields); +``` + +### FormUrlEncodedConverter + +you can also use `FormUrlEncodedConverter` that also will add the correct `content-type` and convert a `Map` into `Map` for requests. ```dart @@ -185,7 +201,6 @@ final chopper = ChopperClient( ); ``` -### On a single method To do only a single type of request with form encoding in a service, use the provided `FormUrlEncodedConverter`' s `requestFactory` method with the `@FactoryConverter` annotation. @@ -208,9 +223,7 @@ the parameter's name is used as the field's name. ```dart @Post(path: "form") -@FactoryConverter( - request: FormUrlEncodedConverter.requestFactory, -) +@formUrlEncoded Future post(@Field() String foo, @Field("b") int bar); ``` From 49943bf12f2575b76c89cdcb4c063ecc9e14ce7e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 7 Mar 2024 20:58:49 +0000 Subject: [PATCH 138/171] :arrow_up: Bump actions/cache from 3.3.1 to 4.0.1 (#580) Bumps [actions/cache](https://github.com/actions/cache) from 3.3.1 to 4.0.1. - [Release notes](https://github.com/actions/cache/releases) - [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md) - [Commits](https://github.com/actions/cache/compare/88522ab9f39a2ea568f7027eddc7d8d8bc9d59c8...ab5e6d0c87105b4c9c2047343972218f562e4319) --- updated-dependencies: - dependency-name: actions/cache dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/dart.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/dart.yml b/.github/workflows/dart.yml index 91f883dd..67485f82 100644 --- a/.github/workflows/dart.yml +++ b/.github/workflows/dart.yml @@ -22,7 +22,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Cache Pub hosted dependencies - uses: actions/cache@88522ab9f39a2ea568f7027eddc7d8d8bc9d59c8 + uses: actions/cache@ab5e6d0c87105b4c9c2047343972218f562e4319 with: path: "~/.pub-cache/hosted" key: "os:ubuntu-latest;pub-cache-hosted;sdk:stable" @@ -45,7 +45,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Cache Pub hosted dependencies - uses: actions/cache@88522ab9f39a2ea568f7027eddc7d8d8bc9d59c8 + uses: actions/cache@ab5e6d0c87105b4c9c2047343972218f562e4319 with: path: "~/.pub-cache/hosted" key: "os:ubuntu-latest;pub-cache-hosted;sdk:stable;packages:chopper-chopper_built_value-chopper_generator;commands:format-analyze" @@ -105,7 +105,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Cache Pub hosted dependencies - uses: actions/cache@88522ab9f39a2ea568f7027eddc7d8d8bc9d59c8 + uses: actions/cache@ab5e6d0c87105b4c9c2047343972218f562e4319 with: path: "~/.pub-cache/hosted" key: "os:ubuntu-latest;pub-cache-hosted;sdk:stable;packages:chopper-chopper_built_value-chopper_generator;commands:test_with_coverage" @@ -176,7 +176,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Cache Pub hosted dependencies - uses: actions/cache@88522ab9f39a2ea568f7027eddc7d8d8bc9d59c8 + uses: actions/cache@ab5e6d0c87105b4c9c2047343972218f562e4319 with: path: "~/.pub-cache/hosted" key: "os:ubuntu-latest;pub-cache-hosted;sdk:stable;packages:chopper-chopper_built_value;commands:test" From 89002560b0d58acb69341d0294a561d61c9b30a9 Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Fri, 8 Mar 2024 14:59:13 +0000 Subject: [PATCH 139/171] :twisted_rightwards_arrows: post release branch sync (#582) --- chopper/CHANGELOG.md | 8 ++++++++ chopper/pubspec.yaml | 2 +- chopper_generator/CHANGELOG.md | 4 ++++ chopper_generator/pubspec.yaml | 4 ++-- 4 files changed, 15 insertions(+), 3 deletions(-) diff --git a/chopper/CHANGELOG.md b/chopper/CHANGELOG.md index 60a8bc9f..dea66c34 100644 --- a/chopper/CHANGELOG.md +++ b/chopper/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## 7.2.0 + +- Add support for `@FormUrlEncoded` annotation ([#579](https://github.com/lejard-h/chopper/pull/579)) + +## 7.1.1+1 + +- Export `ChopperHttpException` in library exports ([#570](https://github.com/lejard-h/chopper/pull/570)) + ## 7.1.1 - Add `Target` annotations ([#567](https://github.com/lejard-h/chopper/pull/567)) diff --git a/chopper/pubspec.yaml b/chopper/pubspec.yaml index b4a729d0..4eaeaffe 100644 --- a/chopper/pubspec.yaml +++ b/chopper/pubspec.yaml @@ -1,6 +1,6 @@ name: chopper description: Chopper is an http client generator using source_gen, inspired by Retrofit -version: 7.1.1 +version: 7.2.0 documentation: https://hadrien-lejard.gitbook.io/chopper repository: https://github.com/lejard-h/chopper diff --git a/chopper_generator/CHANGELOG.md b/chopper_generator/CHANGELOG.md index 08e09416..202a3cce 100644 --- a/chopper_generator/CHANGELOG.md +++ b/chopper_generator/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 7.2.0 + +- Add support for `@FormUrlEncoded` annotation ([#579](https://github.com/lejard-h/chopper/pull/579)) + ## 7.1.1 - Add option to override build_extension via build.yaml ([#562](https://github.com/lejard-h/chopper/pull/562)) diff --git a/chopper_generator/pubspec.yaml b/chopper_generator/pubspec.yaml index 43870f85..773f94c8 100644 --- a/chopper_generator/pubspec.yaml +++ b/chopper_generator/pubspec.yaml @@ -1,6 +1,6 @@ name: chopper_generator description: Chopper is an http client generator using source_gen, inspired by Retrofit -version: 7.1.1 +version: 7.2.0 documentation: https://hadrien-lejard.gitbook.io/chopper repository: https://github.com/lejard-h/chopper @@ -11,7 +11,7 @@ dependencies: analyzer: ">=5.13.0 <7.0.0" build: ^2.4.1 built_collection: ^5.1.1 - chopper: ^7.1.0 + chopper: ^7.2.0 code_builder: ^4.5.0 dart_style: ^2.3.2 logging: ^1.2.0 From 8ff4ecdd17d7b77cc5e9427d5bea35de97a72596 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Mar 2024 10:01:00 +0000 Subject: [PATCH 140/171] :arrow_up: Bump softprops/action-gh-release from 1 to 2 (#583) Bumps [softprops/action-gh-release](https://github.com/softprops/action-gh-release) from 1 to 2. - [Release notes](https://github.com/softprops/action-gh-release/releases) - [Changelog](https://github.com/softprops/action-gh-release/blob/master/CHANGELOG.md) - [Commits](https://github.com/softprops/action-gh-release/compare/v1...v2) --- updated-dependencies: - dependency-name: softprops/action-gh-release dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index f7835d30..478734f5 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -111,7 +111,7 @@ jobs: - name: Github release id: github_release if: ${{ env.IS_VERSION_GREATER == 1 }} - uses: softprops/action-gh-release@v1 + uses: softprops/action-gh-release@v2 with: name: ${{ format('{0}-v{1}', matrix.package, env.THIS_VERSION) }} tag_name: ${{ format('{0}-v{1}', matrix.package, env.THIS_VERSION) }} From 7c65a250e7f01cb92c41588d0bfd4ea70902268d Mon Sep 17 00:00:00 2001 From: wenchieh Date: Wed, 20 Mar 2024 22:46:50 +0800 Subject: [PATCH 141/171] Support @Tag (#586) --- chopper/example/tag.chopper.dart | 44 ++++++++++ chopper/example/tag.dart | 87 +++++++++++++++++++ chopper/lib/src/annotations.dart | 22 +++++ chopper/lib/src/request.dart | 2 + chopper_generator/lib/src/generator.dart | 6 ++ .../test/test_service.chopper.dart | 17 ++++ chopper_generator/test/test_service.dart | 6 ++ requests.md | 46 +++++++++- 8 files changed, 229 insertions(+), 1 deletion(-) create mode 100644 chopper/example/tag.chopper.dart create mode 100644 chopper/example/tag.dart diff --git a/chopper/example/tag.chopper.dart b/chopper/example/tag.chopper.dart new file mode 100644 index 00000000..77f07fa9 --- /dev/null +++ b/chopper/example/tag.chopper.dart @@ -0,0 +1,44 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'tag.dart'; + +// ************************************************************************** +// ChopperGenerator +// ************************************************************************** + +// coverage:ignore-file +// ignore_for_file: type=lint +final class _$TagService extends TagService { + _$TagService([ChopperClient? client]) { + if (client == null) return; + this.client = client; + } + + @override + final Type definitionType = TagService; + + @override + Future> requestWithTag({BizTag tag = const BizTag()}) { + final Uri $url = Uri.parse('/tag'); + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + tag: tag, + ); + return client.send($request); + } + + @override + Future> includeBodyNullOrEmptyTag( + {IncludeBodyNullOrEmptyTag tag = const IncludeBodyNullOrEmptyTag()}) { + final Uri $url = Uri.parse('/tag'); + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + tag: tag, + ); + return client.send($request); + } +} diff --git a/chopper/example/tag.dart b/chopper/example/tag.dart new file mode 100644 index 00000000..eb168a09 --- /dev/null +++ b/chopper/example/tag.dart @@ -0,0 +1,87 @@ +/// @author luwenjie on 2024/3/20 11:38:11 +/// +/// +/// +import "package:chopper/chopper.dart"; + +import 'definition.dart'; + +part 'tag.chopper.dart'; + +Future main() async { + final chopper = ChopperClient( + baseUrl: Uri.parse('http://localhost:8000'), + services: [ + // the generated service + TagService.create(ChopperClient()), + ], + interceptors: [ + TagInterceptor(), + ], + converter: JsonConverter(), + ); + + final myService = chopper.getService(); + + final response = await myService.getMapResource('1'); + print(response.body); + + final list = await myService.getListResources(); + print(list.body); + chopper.dispose(); +} + +// add a uniform appId header for some path +class BizTag { + final int appId; + + BizTag({this.appId = 0}); +} + +class IncludeBodyNullOrEmptyTag { + bool includeNull = false; + bool includeEmpty = false; + + IncludeBodyNullOrEmptyTag(this.includeNull, this.includeEmpty); +} + +class TagConverter extends JsonConverter { + FutureOr convertRequest(Request request) { + final tag = request.tag; + if (tag is IncludeBodyNullOrEmptyTag) { + if (request.body is Map) { + final Map body = request.body as Map; + final Map bodyCopy = {}; + for (final MapEntry entry in body.entries) { + if (!tag.includeNull && entry.value == null) continue; + if (!tag.includeEmpty && entry.value == "") continue; + bodyCopy[entry.key] = entry.value; + } + request = request.copyWith(body: bodyCopy); + } + } + } +} + +class TagInterceptor implements RequestInterceptor { + FutureOr onRequest(Request request) { + final tag = request.tag; + if (tag is BizTag) { + request.headers["x-appId"] = tag.appId; + } + return request; + } +} + +@ChopperApi(baseUrl: '/tag') +abstract class TagService extends ChopperService { + static TagService create(ChopperClient client) => _$TagService(client); + + @get(path: '/bizRequest') + Future requestWithTag({@Tag() BizTag tag = const BizTag()}); + + @get(path: '/include') + Future includeBodyNullOrEmptyTag( + {@Tag() + IncludeBodyNullOrEmptyTag tag = const IncludeBodyNullOrEmptyTag()}); +} diff --git a/chopper/lib/src/annotations.dart b/chopper/lib/src/annotations.dart index b4d84aca..0a0b8058 100644 --- a/chopper/lib/src/annotations.dart +++ b/chopper/lib/src/annotations.dart @@ -557,6 +557,25 @@ final class FormUrlEncoded { const FormUrlEncoded(); } +/// +/// {@template Tag} +/// Adds the argument instance as a request tag. +/// +/// ```dart +/// Future requestWithTag( +/// @Tag() String t1, +/// ); +/// ``` +/// get tag via `request.tags` +/// +/// {@endtemplate} +@immutable +@Target({TargetKind.parameter}) +final class Tag { + /// {@macro Tag} + const Tag(); +} + /// {@macro ChopperApi} const chopperApi = ChopperApi(); @@ -622,3 +641,6 @@ const partFileMap = PartFileMap(); /// {@macro FormUrlEncoded} const formUrlEncoded = FormUrlEncoded(); + +/// {@macro Tag} +const tag = Tag(); diff --git a/chopper/lib/src/request.dart b/chopper/lib/src/request.dart index 68d27331..f503d0a7 100644 --- a/chopper/lib/src/request.dart +++ b/chopper/lib/src/request.dart @@ -14,6 +14,7 @@ base class Request extends http.BaseRequest with EquatableMixin { final Uri baseUri; final dynamic body; final Map parameters; + final Object? tag; final bool multipart; final List parts; final bool useBrackets; @@ -29,6 +30,7 @@ base class Request extends http.BaseRequest with EquatableMixin { Map headers = const {}, this.multipart = false, this.parts = const [], + this.tag, this.useBrackets = false, this.includeNullQueryVars = false, }) : assert( diff --git a/chopper_generator/lib/src/generator.dart b/chopper_generator/lib/src/generator.dart index b7946c75..c3a99766 100644 --- a/chopper_generator/lib/src/generator.dart +++ b/chopper_generator/lib/src/generator.dart @@ -172,6 +172,7 @@ final class ChopperGenerator _getAnnotations(m, chopper.PartFile); final Map fileFieldMap = _getAnnotation(m, chopper.PartFileMap); + final Map tag = _getAnnotation(m, chopper.Tag); final Code? headers = _generateHeaders(m, method!, formUrlEncoded); final Expression url = _generateUrl( @@ -400,6 +401,8 @@ final class ChopperGenerator ); } + final bool hasTag = tag.isNotEmpty; + final bool useBrackets = Utils.getUseBrackets(method); final bool includeNullQueryVars = Utils.getIncludeNullQueryVars(method); @@ -413,6 +416,7 @@ final class ChopperGenerator useQueries: hasQuery, useHeaders: headers != null, hasParts: hasParts, + tagRefer: hasTag ? refer(tag.keys.first) : null, useBrackets: useBrackets, includeNullQueryVars: includeNullQueryVars, ), @@ -701,6 +705,7 @@ final class ChopperGenerator bool useHeaders = false, bool useBrackets = false, bool includeNullQueryVars = false, + Reference? tagRefer, }) => refer('Request').newInstance( [ @@ -716,6 +721,7 @@ final class ChopperGenerator }, if (useQueries) 'parameters': refer(Vars.parameters.toString()), if (useHeaders) 'headers': refer(Vars.headers.toString()), + if (tagRefer != null) 'tag': tagRefer, if (useBrackets) 'useBrackets': literalBool(useBrackets), if (includeNullQueryVars) 'includeNullQueryVars': literalBool(includeNullQueryVars), diff --git a/chopper_generator/test/test_service.chopper.dart b/chopper_generator/test/test_service.chopper.dart index 5019d39c..773bddf3 100644 --- a/chopper_generator/test/test_service.chopper.dart +++ b/chopper_generator/test/test_service.chopper.dart @@ -783,4 +783,21 @@ final class _$HttpTestService extends HttpTestService { requestConverter: FormUrlEncodedConverter.requestFactory, ); } + + @override + Future> tag( + String foo, + Object? t1, + ) { + final Uri $url = Uri.parse('/test/tag'); + final $body = {'fool': foo}; + final Request $request = Request( + 'POST', + $url, + client.baseUrl, + body: $body, + tag: t1, + ); + return client.send($request); + } } diff --git a/chopper_generator/test/test_service.dart b/chopper_generator/test/test_service.dart index 273e9d73..7f818dae 100644 --- a/chopper_generator/test/test_service.dart +++ b/chopper_generator/test/test_service.dart @@ -233,6 +233,12 @@ abstract class HttpTestService extends ChopperService { @Field() final List positives, [ @Field() final String? signature, ]); + + @Post(path: 'tag') + Future> tag( + @Field('fool') final String foo, + @Tag() Object? t1, + ); } Request customConvertRequest(Request req) { diff --git a/requests.md b/requests.md index 4a66090d..5a1a5703 100644 --- a/requests.md +++ b/requests.md @@ -22,6 +22,8 @@ | `@PartMap()`, `@partMap` | - | Defines a multipart part map. | | `@PartFile()`, `@partFile` | - | Defines a multipart file part. | | `@PartFileMap()`, `@partFileMap` | - | Defines a multipart file part map. | +| `@Tag`, `@tag` | - | Defines a tag parameter. | + ## Path resolution @@ -276,4 +278,46 @@ Future> fetch(); Future fetch(); ``` -> Note: Chopper doesn't convert response bodies by itself to dart object. You need to use a [Converter](converters/converters.md) for that. \ No newline at end of file +> Note: Chopper doesn't convert response bodies by itself to dart object. You need to use a [Converter](converters/converters.md) for that. + +## Add tag +`@Tag` parameter annotation for setting tag on the underlying Chopper `Request` object. These can be read +in `Converter`s or `Interceptor`s for tracing, analytics, varying behavior, and more. + + +if want to filter null value or empty String for some url. we can make an `IncludeBodyNullOrEmptyTag` Object as Tag. +```dart +class IncludeBodyNullOrEmptyTag { + bool includeNull = false; + bool includeEmpty = false; + + IncludeBodyNullOrEmptyTag(this.includeNull, this.includeEmpty); +} + +@get(path: '/include') +Future includeBodyNullOrEmptyTag( + {@Tag() + IncludeBodyNullOrEmptyTag tag = const IncludeBodyNullOrEmptyTag()}); +``` + +get tag via `request.tag` in `Converter` or `Interceptor`: + +```dart +class TagConverter extends JsonConverter { + FutureOr convertRequest(Request request) { + final tag = request.tag; + if (tag is IncludeBodyNullOrEmptyTag) { + if (request.body is Map) { + final Map body = request.body as Map; + final Map bodyCopy = {}; + for (final MapEntry entry in body.entries) { + if (!tag.includeNull && entry.value == null) continue; + if (!tag.includeEmpty && entry.value == "") continue; + bodyCopy[entry.key] = entry.value; + } + request = request.copyWith(body: bodyCopy); + } + } + } +} +``` \ No newline at end of file From fd9747a5d0d1d467e40aae79c3796db8b524f098 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 20 Mar 2024 14:47:58 +0000 Subject: [PATCH 142/171] :arrow_up: Bump actions/checkout from 3.5.2 to 4.1.2 (#587) Bumps [actions/checkout](https://github.com/actions/checkout) from 3.5.2 to 4.1.2. - [Release notes](https://github.com/actions/checkout/releases) - [Commits](https://github.com/actions/checkout/compare/v3.5.2...v4.1.2) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/dart.yml | 8 ++++---- .github/workflows/publish.yml | 4 ++-- .github/workflows/publish_dry_run.yml | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/dart.yml b/.github/workflows/dart.yml index 67485f82..d10566f1 100644 --- a/.github/workflows/dart.yml +++ b/.github/workflows/dart.yml @@ -35,7 +35,7 @@ jobs: sdk: stable - id: checkout name: Checkout repository - uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab + uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 - name: mono_repo self validate run: dart pub global activate mono_repo 6.6.1 - name: mono_repo self validate @@ -60,7 +60,7 @@ jobs: sdk: stable - id: checkout name: Checkout repository - uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab + uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 - id: chopper_pub_upgrade name: chopper; dart pub upgrade run: dart pub upgrade @@ -122,7 +122,7 @@ jobs: run: "dart pub global activate coverage '>=1.5.0'" - id: checkout name: Checkout repository - uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab + uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 - id: chopper_pub_upgrade name: chopper; dart pub upgrade run: dart pub upgrade @@ -191,7 +191,7 @@ jobs: sdk: stable - id: checkout name: Checkout repository - uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab + uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 - id: chopper_pub_upgrade name: chopper; dart pub upgrade run: dart pub upgrade diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 478734f5..58ab9265 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -27,7 +27,7 @@ jobs: with: sdk: stable - id: checkout - uses: actions/checkout@v4 + uses: actions/checkout@v4.1.2 with: fetch-depth: 2 - run: git checkout HEAD^ @@ -53,7 +53,7 @@ jobs: with: sdk: stable - id: checkout - uses: actions/checkout@v4 + uses: actions/checkout@v4.1.2 - name: Load this version id: load_this_version working-directory: ${{ matrix.package }} diff --git a/.github/workflows/publish_dry_run.yml b/.github/workflows/publish_dry_run.yml index 8a23a866..6afcb3a2 100644 --- a/.github/workflows/publish_dry_run.yml +++ b/.github/workflows/publish_dry_run.yml @@ -27,7 +27,7 @@ jobs: with: sdk: stable - id: checkout - uses: actions/checkout@v4 + uses: actions/checkout@v4.1.2 with: ref: ${{ github.event.pull_request.base.ref }} - name: Load base version @@ -50,7 +50,7 @@ jobs: with: sdk: stable - id: checkout - uses: actions/checkout@v4 + uses: actions/checkout@v4.1.2 - name: Load this version id: load_this_version working-directory: ${{ matrix.package }} From 153f13e3fc35082abccf3d0e8550cc2cf47e4809 Mon Sep 17 00:00:00 2001 From: wenchieh Date: Thu, 21 Mar 2024 16:59:39 +0800 Subject: [PATCH 143/171] :bug: fix Request.copyWith lose tag param (#588) --- chopper/lib/src/request.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/chopper/lib/src/request.dart b/chopper/lib/src/request.dart index f503d0a7..a9e7e9b7 100644 --- a/chopper/lib/src/request.dart +++ b/chopper/lib/src/request.dart @@ -64,6 +64,7 @@ base class Request extends http.BaseRequest with EquatableMixin { List? parts, bool? useBrackets, bool? includeNullQueryVars, + Object? tag, }) => Request( method ?? this.method, @@ -76,6 +77,7 @@ base class Request extends http.BaseRequest with EquatableMixin { parts: parts ?? this.parts, useBrackets: useBrackets ?? this.useBrackets, includeNullQueryVars: includeNullQueryVars ?? this.includeNullQueryVars, + tag: tag ?? this.tag, ); /// Builds a valid URI from [baseUrl], [url] and [parameters]. From 5039e4939f18ca849bfcd3553e71a62643c3384c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 25 Mar 2024 09:48:23 +0000 Subject: [PATCH 144/171] :arrow_up: Bump actions/cache from 4.0.1 to 4.0.2 (#590) Bumps [actions/cache](https://github.com/actions/cache) from 4.0.1 to 4.0.2. - [Release notes](https://github.com/actions/cache/releases) - [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md) - [Commits](https://github.com/actions/cache/compare/ab5e6d0c87105b4c9c2047343972218f562e4319...0c45773b623bea8c8e75f6c82b208c3cf94ea4f9) --- updated-dependencies: - dependency-name: actions/cache dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/dart.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/dart.yml b/.github/workflows/dart.yml index d10566f1..d26c7482 100644 --- a/.github/workflows/dart.yml +++ b/.github/workflows/dart.yml @@ -22,7 +22,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Cache Pub hosted dependencies - uses: actions/cache@ab5e6d0c87105b4c9c2047343972218f562e4319 + uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 with: path: "~/.pub-cache/hosted" key: "os:ubuntu-latest;pub-cache-hosted;sdk:stable" @@ -45,7 +45,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Cache Pub hosted dependencies - uses: actions/cache@ab5e6d0c87105b4c9c2047343972218f562e4319 + uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 with: path: "~/.pub-cache/hosted" key: "os:ubuntu-latest;pub-cache-hosted;sdk:stable;packages:chopper-chopper_built_value-chopper_generator;commands:format-analyze" @@ -105,7 +105,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Cache Pub hosted dependencies - uses: actions/cache@ab5e6d0c87105b4c9c2047343972218f562e4319 + uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 with: path: "~/.pub-cache/hosted" key: "os:ubuntu-latest;pub-cache-hosted;sdk:stable;packages:chopper-chopper_built_value-chopper_generator;commands:test_with_coverage" @@ -176,7 +176,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Cache Pub hosted dependencies - uses: actions/cache@ab5e6d0c87105b4c9c2047343972218f562e4319 + uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 with: path: "~/.pub-cache/hosted" key: "os:ubuntu-latest;pub-cache-hosted;sdk:stable;packages:chopper-chopper_built_value;commands:test" From dd61ec1e5846756ace13381763b76fb5bf1dfaa7 Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Thu, 4 Apr 2024 09:26:02 +0200 Subject: [PATCH 145/171] :twisted_rightwards_arrows: post release branch sync (#593) --- .github/workflows/dart.yml | 8 ++++---- chopper/CHANGELOG.md | 4 ++++ chopper/pubspec.yaml | 6 +++--- chopper_generator/CHANGELOG.md | 4 ++++ chopper_generator/pubspec.yaml | 4 ++-- 5 files changed, 17 insertions(+), 9 deletions(-) diff --git a/.github/workflows/dart.yml b/.github/workflows/dart.yml index d26c7482..d10566f1 100644 --- a/.github/workflows/dart.yml +++ b/.github/workflows/dart.yml @@ -22,7 +22,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Cache Pub hosted dependencies - uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 + uses: actions/cache@ab5e6d0c87105b4c9c2047343972218f562e4319 with: path: "~/.pub-cache/hosted" key: "os:ubuntu-latest;pub-cache-hosted;sdk:stable" @@ -45,7 +45,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Cache Pub hosted dependencies - uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 + uses: actions/cache@ab5e6d0c87105b4c9c2047343972218f562e4319 with: path: "~/.pub-cache/hosted" key: "os:ubuntu-latest;pub-cache-hosted;sdk:stable;packages:chopper-chopper_built_value-chopper_generator;commands:format-analyze" @@ -105,7 +105,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Cache Pub hosted dependencies - uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 + uses: actions/cache@ab5e6d0c87105b4c9c2047343972218f562e4319 with: path: "~/.pub-cache/hosted" key: "os:ubuntu-latest;pub-cache-hosted;sdk:stable;packages:chopper-chopper_built_value-chopper_generator;commands:test_with_coverage" @@ -176,7 +176,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Cache Pub hosted dependencies - uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 + uses: actions/cache@ab5e6d0c87105b4c9c2047343972218f562e4319 with: path: "~/.pub-cache/hosted" key: "os:ubuntu-latest;pub-cache-hosted;sdk:stable;packages:chopper-chopper_built_value;commands:test" diff --git a/chopper/CHANGELOG.md b/chopper/CHANGELOG.md index dea66c34..c5b30513 100644 --- a/chopper/CHANGELOG.md +++ b/chopper/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 7.3.0 + +- Add support for `@Tag` annotation ([#586](https://github.com/lejard-h/chopper/pull/586)) + ## 7.2.0 - Add support for `@FormUrlEncoded` annotation ([#579](https://github.com/lejard-h/chopper/pull/579)) diff --git a/chopper/pubspec.yaml b/chopper/pubspec.yaml index 4eaeaffe..dc5a2d0b 100644 --- a/chopper/pubspec.yaml +++ b/chopper/pubspec.yaml @@ -1,6 +1,6 @@ name: chopper description: Chopper is an http client generator using source_gen, inspired by Retrofit -version: 7.2.0 +version: 7.3.0 documentation: https://hadrien-lejard.gitbook.io/chopper repository: https://github.com/lejard-h/chopper @@ -25,7 +25,7 @@ dev_dependencies: lints: ">=2.1.1 <4.0.0" test: ^1.24.4 transparent_image: ^2.0.1 - chopper_generator: ^7.1.0 + chopper_generator: ^7.2.0 dependency_overrides: chopper_generator: @@ -35,4 +35,4 @@ topics: - api - client - http - - rest \ No newline at end of file + - rest diff --git a/chopper_generator/CHANGELOG.md b/chopper_generator/CHANGELOG.md index 202a3cce..cac791d0 100644 --- a/chopper_generator/CHANGELOG.md +++ b/chopper_generator/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 7.3.0 + +- Add support for `@Tag` annotation ([#586](https://github.com/lejard-h/chopper/pull/586)) + ## 7.2.0 - Add support for `@FormUrlEncoded` annotation ([#579](https://github.com/lejard-h/chopper/pull/579)) diff --git a/chopper_generator/pubspec.yaml b/chopper_generator/pubspec.yaml index 773f94c8..7eb33501 100644 --- a/chopper_generator/pubspec.yaml +++ b/chopper_generator/pubspec.yaml @@ -1,6 +1,6 @@ name: chopper_generator description: Chopper is an http client generator using source_gen, inspired by Retrofit -version: 7.2.0 +version: 7.3.0 documentation: https://hadrien-lejard.gitbook.io/chopper repository: https://github.com/lejard-h/chopper @@ -11,7 +11,7 @@ dependencies: analyzer: ">=5.13.0 <7.0.0" build: ^2.4.1 built_collection: ^5.1.1 - chopper: ^7.2.0 + chopper: ^7.3.0 code_builder: ^4.5.0 dart_style: ^2.3.2 logging: ^1.2.0 From 84471e41bdb311a2734ab8399c7b498ceb5b54fc Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Thu, 4 Apr 2024 21:32:15 +0200 Subject: [PATCH 146/171] :zap: use qs_dart for query string encoding (#592) --- chopper/analysis_options.yaml | 1 + chopper/lib/chopper.dart | 1 + chopper/lib/src/annotations.dart | 32 +- chopper/lib/src/list_format.dart | 29 + chopper/lib/src/request.dart | 30 +- chopper/lib/src/utils.dart | 111 +- chopper/pubspec.yaml | 1 + chopper/test/base_test.dart | 310 +++- chopper/test/test_service.chopper.dart | 123 +- chopper/test/test_service.dart | 44 +- .../test/test_service_base_url.chopper.dart | 131 +- chopper/test/test_service_base_url.dart | 44 +- .../test/test_service_variable.chopper.dart | 125 +- chopper/test/test_service_variable.dart | 44 +- ...test_without_response_service.chopper.dart | 129 +- .../test/test_without_response_service.dart | 44 +- chopper/test/utils_test.dart | 1379 ++++++++++++++++- chopper_generator/analysis_options.yaml | 1 + chopper_generator/lib/src/generator.dart | 20 +- chopper_generator/lib/src/utils.dart | 22 +- chopper_generator/pubspec.yaml | 2 + .../test/test_service.chopper.dart | 123 +- chopper_generator/test/test_service.dart | 44 +- .../test/test_service_variable.chopper.dart | 125 +- .../test/test_service_variable.dart | 44 +- ...test_without_response_service.chopper.dart | 129 +- .../test/test_without_response_service.dart | 44 +- example/analysis_options.yaml | 1 + 28 files changed, 2920 insertions(+), 213 deletions(-) create mode 100644 chopper/lib/src/list_format.dart diff --git a/chopper/analysis_options.yaml b/chopper/analysis_options.yaml index 4fd79467..6f56a451 100644 --- a/chopper/analysis_options.yaml +++ b/chopper/analysis_options.yaml @@ -3,6 +3,7 @@ include: package:lints/recommended.yaml analyzer: exclude: - "**.g.dart" + - "**.chopper.dart" - "**.mocks.dart" - "example/**" diff --git a/chopper/lib/chopper.dart b/chopper/lib/chopper.dart index bcd68145..38fd56aa 100644 --- a/chopper/lib/chopper.dart +++ b/chopper/lib/chopper.dart @@ -12,6 +12,7 @@ export 'src/constants.dart'; export 'src/extensions.dart'; export 'src/http_logging_interceptor.dart'; export 'src/interceptor.dart'; +export 'src/list_format.dart'; export 'src/request.dart'; export 'src/response.dart'; export 'src/utils.dart' hide mapToQuery; diff --git a/chopper/lib/src/annotations.dart b/chopper/lib/src/annotations.dart index 0a0b8058..899b5ecb 100644 --- a/chopper/lib/src/annotations.dart +++ b/chopper/lib/src/annotations.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:chopper/src/constants.dart'; +import 'package:chopper/src/list_format.dart'; import 'package:chopper/src/request.dart'; import 'package:chopper/src/response.dart'; import 'package:meta/meta.dart'; @@ -190,14 +191,23 @@ sealed class Method { /// Mark the body as optional to suppress warnings during code generation final bool optionalBody; - /// Use brackets [ ] to when encoding + /// List format to use when encoding lists + /// + /// - [ListFormat.repeat] `hxxp://path/to/script?foo=123&foo=456&foo=789` (default) + /// - [ListFormat.brackets] `hxxp://path/to/script?foo[]=123&foo[]=456&foo[]=789` + /// - [ListFormat.indices] `hxxp://path/to/script?foo[0]=123&foo[1]=456&foo[2]=789` + /// - [ListFormat.comma] `hxxp://path/to/script?foo=123,456,789` + final ListFormat? listFormat; + + /// Use brackets `[ ]` to when encoding /// /// - lists - /// hxxp://path/to/script?foo[]=123&foo[]=456&foo[]=789 + /// `hxxp://path/to/script?foo[]=123&foo[]=456&foo[]=789` /// /// - maps - /// hxxp://path/to/script?user[name]=john&user[surname]=doe&user[age]=21 - final bool useBrackets; + /// `hxxp://path/to/script?user[name]=john&user[surname]=doe&user[age]=21` + @Deprecated('Use listFormat instead') + final bool? useBrackets; /// Set to [true] to include query variables with null values. This includes nested maps. /// The default is to exclude them. @@ -223,7 +233,7 @@ sealed class Method { /// ``` /// /// The above code produces hxxp://path/to/script&foo=foo_var&bar=&baz=baz_var - final bool includeNullQueryVars; + final bool? includeNullQueryVars; /// {@macro Method} const Method( @@ -231,8 +241,9 @@ sealed class Method { this.optionalBody = false, this.path = '', this.headers = const {}, - this.useBrackets = false, - this.includeNullQueryVars = false, + this.listFormat, + @Deprecated('Use listFormat instead') this.useBrackets, + this.includeNullQueryVars, }); } @@ -247,6 +258,7 @@ final class Get extends Method { super.optionalBody = true, super.path, super.headers, + super.listFormat, super.useBrackets, super.includeNullQueryVars, }) : super(HttpMethod.Get); @@ -265,6 +277,7 @@ final class Post extends Method { super.optionalBody, super.path, super.headers, + super.listFormat, super.useBrackets, super.includeNullQueryVars, }) : super(HttpMethod.Post); @@ -281,6 +294,7 @@ final class Delete extends Method { super.optionalBody = true, super.path, super.headers, + super.listFormat, super.useBrackets, super.includeNullQueryVars, }) : super(HttpMethod.Delete); @@ -299,6 +313,7 @@ final class Put extends Method { super.optionalBody, super.path, super.headers, + super.listFormat, super.useBrackets, super.includeNullQueryVars, }) : super(HttpMethod.Put); @@ -316,6 +331,7 @@ final class Patch extends Method { super.optionalBody, super.path, super.headers, + super.listFormat, super.useBrackets, super.includeNullQueryVars, }) : super(HttpMethod.Patch); @@ -332,6 +348,7 @@ final class Head extends Method { super.optionalBody = true, super.path, super.headers, + super.listFormat, super.useBrackets, super.includeNullQueryVars, }) : super(HttpMethod.Head); @@ -348,6 +365,7 @@ final class Options extends Method { super.optionalBody = true, super.path, super.headers, + super.listFormat, super.useBrackets, super.includeNullQueryVars, }) : super(HttpMethod.Options); diff --git a/chopper/lib/src/list_format.dart b/chopper/lib/src/list_format.dart new file mode 100644 index 00000000..dedb3d72 --- /dev/null +++ b/chopper/lib/src/list_format.dart @@ -0,0 +1,29 @@ +import 'package:qs_dart/qs_dart.dart' as qs show ListFormat; + +/// An enum of all available list format options. +/// +/// This is a wrapper around the [qs.ListFormat] enum. +enum ListFormat { + /// Use brackets to represent list items, for example + /// `foo[]=123&foo[]=456&foo[]=789` + brackets(qs.ListFormat.brackets), + + /// Use commas to represent list items, for example + /// `foo=123,456,789` + comma(qs.ListFormat.comma), + + /// Repeat the same key to represent list items, for example + /// `foo=123&foo=456&foo=789` + repeat(qs.ListFormat.repeat), + + /// Use indices in brackets to represent list items, for example + /// `foo[0]=123&foo[1]=456&foo[2]=789` + indices(qs.ListFormat.indices); + + const ListFormat(this.qsListFormat); + + final qs.ListFormat qsListFormat; + + @override + String toString() => name; +} diff --git a/chopper/lib/src/request.dart b/chopper/lib/src/request.dart index a9e7e9b7..fbf1891b 100644 --- a/chopper/lib/src/request.dart +++ b/chopper/lib/src/request.dart @@ -1,6 +1,7 @@ -import 'dart:async'; +import 'dart:async' show Stream; import 'package:chopper/src/extensions.dart'; +import 'package:chopper/src/list_format.dart'; import 'package:chopper/src/utils.dart'; import 'package:equatable/equatable.dart' show EquatableMixin; import 'package:http/http.dart' as http; @@ -17,8 +18,10 @@ base class Request extends http.BaseRequest with EquatableMixin { final Object? tag; final bool multipart; final List parts; - final bool useBrackets; - final bool includeNullQueryVars; + final ListFormat? listFormat; + @Deprecated('Use listFormat instead') + final bool? useBrackets; + final bool? includeNullQueryVars; /// {@macro request} Request( @@ -31,8 +34,9 @@ base class Request extends http.BaseRequest with EquatableMixin { this.multipart = false, this.parts = const [], this.tag, - this.useBrackets = false, - this.includeNullQueryVars = false, + this.listFormat, + @Deprecated('Use listFormat instead') this.useBrackets, + this.includeNullQueryVars, }) : assert( !baseUri.hasQuery, 'baseUri should not contain query parameters.' @@ -45,6 +49,8 @@ base class Request extends http.BaseRequest with EquatableMixin { baseUri, uri, {...uri.queryParametersAll, ...?parameters}, + listFormat: listFormat, + // ignore: deprecated_member_use_from_same_package useBrackets: useBrackets, includeNullQueryVars: includeNullQueryVars, ), @@ -62,7 +68,8 @@ base class Request extends http.BaseRequest with EquatableMixin { Map? headers, bool? multipart, List? parts, - bool? useBrackets, + ListFormat? listFormat, + @Deprecated('Use listFormat instead') bool? useBrackets, bool? includeNullQueryVars, Object? tag, }) => @@ -75,6 +82,8 @@ base class Request extends http.BaseRequest with EquatableMixin { headers: headers ?? this.headers, multipart: multipart ?? this.multipart, parts: parts ?? this.parts, + listFormat: listFormat ?? this.listFormat, + // ignore: deprecated_member_use_from_same_package useBrackets: useBrackets ?? this.useBrackets, includeNullQueryVars: includeNullQueryVars ?? this.includeNullQueryVars, tag: tag ?? this.tag, @@ -88,8 +97,9 @@ base class Request extends http.BaseRequest with EquatableMixin { Uri baseUrl, Uri url, Map parameters, { - bool useBrackets = false, - bool includeNullQueryVars = false, + ListFormat? listFormat, + @Deprecated('Use listFormat instead') bool? useBrackets, + bool? includeNullQueryVars, }) { // If the request's url is already a fully qualified URL, we can use it // as-is and ignore the baseUrl. @@ -106,6 +116,8 @@ base class Request extends http.BaseRequest with EquatableMixin { final String query = mapToQuery( allParameters, + listFormat: listFormat, + // ignore: deprecated_member_use_from_same_package useBrackets: useBrackets, includeNullQueryVars: includeNullQueryVars, ); @@ -239,6 +251,8 @@ base class Request extends http.BaseRequest with EquatableMixin { headers, multipart, parts, + listFormat, + // ignore: deprecated_member_use_from_same_package useBrackets, includeNullQueryVars, ]; diff --git a/chopper/lib/src/utils.dart b/chopper/lib/src/utils.dart index 63eeba6e..4906df9b 100644 --- a/chopper/lib/src/utils.dart +++ b/chopper/lib/src/utils.dart @@ -1,8 +1,8 @@ import 'dart:collection'; import 'package:chopper/chopper.dart'; -import 'package:equatable/equatable.dart' show EquatableMixin; import 'package:logging/logging.dart'; +import 'package:qs_dart/qs_dart.dart' as qs; /// Creates a new [Request] by copying [request] and adding a header with the /// provided key [name] and value [value] to the result. @@ -63,99 +63,24 @@ final chopperLogger = Logger('Chopper'); /// E.g., `{'foo': 'bar', 'ints': [ 1337, 42 ] }` will become 'foo=bar&ints=1337&ints=42'. String mapToQuery( Map map, { - bool useBrackets = false, - bool includeNullQueryVars = false, -}) => - _mapToQuery( - map, - useBrackets: useBrackets, - includeNullQueryVars: includeNullQueryVars, - ).join('&'); - -Iterable<_Pair> _mapToQuery( - Map map, { - String? prefix, - bool useBrackets = false, - bool includeNullQueryVars = false, + ListFormat? listFormat, + @Deprecated('Use listFormat instead') bool? useBrackets, + bool? includeNullQueryVars, }) { - final Set<_Pair> pairs = {}; - - map.forEach((key, value) { - String name = Uri.encodeQueryComponent(key); - - if (prefix != null) { - name = useBrackets - ? '$prefix${Uri.encodeQueryComponent('[')}$name${Uri.encodeQueryComponent(']')}' - : '$prefix.$name'; - } - - if (value != null) { - if (value is Iterable) { - pairs.addAll(_iterableToQuery(name, value, useBrackets: useBrackets)); - } else if (value is Map) { - pairs.addAll( - _mapToQuery( - value, - prefix: name, - useBrackets: useBrackets, - includeNullQueryVars: includeNullQueryVars, - ), - ); - } else { - pairs.add( - _Pair(name, _normalizeValue(value)), - ); - } - } else { - if (includeNullQueryVars) { - pairs.add(_Pair(name, '')); - } - } - }); - - return pairs; -} - -Iterable<_Pair> _iterableToQuery( - String name, - Iterable values, { - bool useBrackets = false, -}) => - values.where((value) => value?.toString().isNotEmpty ?? false).map( - (value) => _Pair( - name, - _normalizeValue(value), - useBrackets: useBrackets, - ), - ); - -String _normalizeValue(value) => Uri.encodeComponent( - value is DateTime - ? value.toUtc().toIso8601String() - : value?.toString() ?? '', - ); - -final class _Pair with EquatableMixin { - final A first; - final B second; - final bool useBrackets; - - const _Pair( - this.first, - this.second, { - this.useBrackets = false, - }); - - @override - String toString() => useBrackets - ? '$first${Uri.encodeQueryComponent('[]')}=$second' - : '$first=$second'; - - @override - List get props => [ - first, - second, - ]; + listFormat ??= useBrackets == true ? ListFormat.brackets : ListFormat.repeat; + + return qs.encode( + map, + qs.EncodeOptions( + listFormat: listFormat.qsListFormat, + allowDots: listFormat == ListFormat.repeat, + encodeDotInKeys: listFormat == ListFormat.repeat, + encodeValuesOnly: listFormat == ListFormat.repeat, + skipNulls: includeNullQueryVars != true, + strictNullHandling: false, + serializeDate: (DateTime date) => date.toUtc().toIso8601String(), + ), + ); } bool isTypeOf() => _Instance() is _Instance; diff --git a/chopper/pubspec.yaml b/chopper/pubspec.yaml index dc5a2d0b..dd8f83d2 100644 --- a/chopper/pubspec.yaml +++ b/chopper/pubspec.yaml @@ -12,6 +12,7 @@ dependencies: http: ^1.1.0 logging: ^1.2.0 meta: ^1.9.1 + qs_dart: ^1.0.3 dev_dependencies: build_runner: ^2.4.6 diff --git a/chopper/test/base_test.dart b/chopper/test/base_test.dart index 8de582a8..d3e0fee2 100644 --- a/chopper/test/base_test.dart +++ b/chopper/test/base_test.dart @@ -154,7 +154,7 @@ void main() { final httpClient = MockClient((request) async { expect( request.url.toString(), - equals('$baseUrl/test/query?name='), + equals('$baseUrl/test/query'), ); expect(request.method, equals('GET')); @@ -176,7 +176,7 @@ void main() { final httpClient = MockClient((request) async { expect( request.url.toString(), - equals('$baseUrl/test/query?name=&default_value=42'), + equals('$baseUrl/test/query?default_value=42'), ); expect(request.method, equals('GET')); @@ -1328,6 +1328,122 @@ void main() { httpClient.close(); }); + test('List query param with brackets (legacy)', () async { + final httpClient = MockClient((request) async { + expect( + request.url.toString(), + equals('$baseUrl/test/list_query_param_with_brackets_legacy' + '?value%5B%5D=foo' + '&value%5B%5D=bar' + '&value%5B%5D=baz'), + ); + expect(request.method, equals('GET')); + + return http.Response('get response', 200); + }); + + final chopper = buildClient(httpClient); + final service = chopper.getService(); + + final response = await service.getUsingListQueryParamWithBracketsLegacy([ + 'foo', + 'bar', + 'baz', + ]); + + expect(response.body, equals('get response')); + expect(response.statusCode, equals(200)); + + httpClient.close(); + }); + + test('List query param with indices', () async { + final httpClient = MockClient((request) async { + expect( + request.url.toString(), + equals('$baseUrl/test/list_query_param_with_indices' + '?value%5B0%5D=foo' + '&value%5B1%5D=bar' + '&value%5B2%5D=baz'), + ); + expect(request.method, equals('GET')); + + return http.Response('get response', 200); + }); + + final chopper = buildClient(httpClient); + final service = chopper.getService(); + + final response = await service.getUsingListQueryParamWithIndices([ + 'foo', + 'bar', + 'baz', + ]); + + expect(response.body, equals('get response')); + expect(response.statusCode, equals(200)); + + httpClient.close(); + }); + + test('List query param with repeat', () async { + final httpClient = MockClient((request) async { + expect( + request.url.toString(), + equals('$baseUrl/test/list_query_param_with_repeat' + '?value=foo' + '&value=bar' + '&value=baz'), + ); + expect(request.method, equals('GET')); + + return http.Response('get response', 200); + }); + + final chopper = buildClient(httpClient); + final service = chopper.getService(); + + final response = await service.getUsingListQueryParamWithRepeat([ + 'foo', + 'bar', + 'baz', + ]); + + expect(response.body, equals('get response')); + expect(response.statusCode, equals(200)); + + httpClient.close(); + }); + + test('List query param with comma', () async { + final httpClient = MockClient((request) async { + expect( + request.url.toString(), + equals('$baseUrl/test/list_query_param_with_comma' + '?value=foo' + '%2Cbar' + '%2Cbaz'), + ); + expect(request.method, equals('GET')); + + return http.Response('get response', 200); + }); + + final chopper = buildClient(httpClient); + final service = chopper.getService(); + + final response = await service.getUsingListQueryParamWithComma([ + 'foo', + 'bar', + 'baz', + ]); + + expect(response.body, equals('get response')); + expect(response.statusCode, equals(200)); + + httpClient.close(); + }); + test('Map query param using default dot QueryMapSeparator', () async { final DateTime now = DateTime.now(); @@ -1423,6 +1539,196 @@ void main() { httpClient.close(); }); + test('Map query param with brackets (legacy) QueryMapSeparator', () async { + final DateTime now = DateTime.now(); + + final httpClient = MockClient((request) async { + expect( + request.url.toString(), + equals('$baseUrl/test/map_query_param_with_brackets_legacy' + '?value%5Bbar%5D=baz' + '&value%5Bzap%5D=abc' + '&value%5Betc%5D%5Babc%5D=def' + '&value%5Betc%5D%5Bghi%5D=jkl' + '&value%5Betc%5D%5Bmno%5D%5Bopq%5D=rst' + '&value%5Betc%5D%5Bmno%5D%5Buvw%5D=xyz' + '&value%5Betc%5D%5Bmno%5D%5Blist%5D%5B%5D=a' + '&value%5Betc%5D%5Bmno%5D%5Blist%5D%5B%5D=123' + '&value%5Betc%5D%5Bmno%5D%5Blist%5D%5B%5D=false' + '&value%5Betc%5D%5Bdt%5D=${Uri.encodeComponent(now.toUtc().toIso8601String())}'), + ); + expect(request.method, equals('GET')); + + return http.Response('get response', 200); + }); + + final chopper = buildClient(httpClient); + final service = chopper.getService(); + + final response = + await service.getUsingMapQueryParamWithBracketsLegacy({ + 'bar': 'baz', + 'zap': 'abc', + 'etc': { + 'abc': 'def', + 'ghi': 'jkl', + 'mno': { + 'opq': 'rst', + 'uvw': 'xyz', + 'list': ['a', 123, false], + }, + 'dt': now, + }, + }); + + expect(response.body, equals('get response')); + expect(response.statusCode, equals(200)); + + httpClient.close(); + }); + + test('Map query param with indices QueryMapSeparator', () async { + final DateTime now = DateTime.now(); + + final httpClient = MockClient((request) async { + expect( + request.url.toString(), + equals('$baseUrl/test/map_query_param_with_indices' + '?value%5Bbar%5D=baz' + '&value%5Bzap%5D=abc' + '&value%5Betc%5D%5Babc%5D=def' + '&value%5Betc%5D%5Bghi%5D=jkl' + '&value%5Betc%5D%5Bmno%5D%5Bopq%5D=rst' + '&value%5Betc%5D%5Bmno%5D%5Buvw%5D=xyz' + '&value%5Betc%5D%5Bmno%5D%5Blist%5D%5B0%5D=a' + '&value%5Betc%5D%5Bmno%5D%5Blist%5D%5B1%5D=123' + '&value%5Betc%5D%5Bmno%5D%5Blist%5D%5B2%5D=false' + '&value%5Betc%5D%5Bdt%5D=${Uri.encodeComponent(now.toUtc().toIso8601String())}'), + ); + expect(request.method, equals('GET')); + + return http.Response('get response', 200); + }); + + final chopper = buildClient(httpClient); + final service = chopper.getService(); + + final response = + await service.getUsingMapQueryParamWithIndices({ + 'bar': 'baz', + 'zap': 'abc', + 'etc': { + 'abc': 'def', + 'ghi': 'jkl', + 'mno': { + 'opq': 'rst', + 'uvw': 'xyz', + 'list': ['a', 123, false], + }, + 'dt': now, + }, + }); + + expect(response.body, equals('get response')); + expect(response.statusCode, equals(200)); + + httpClient.close(); + }); + + test('Map query param with repeat QueryMapSeparator', () async { + final DateTime now = DateTime.now(); + + final httpClient = MockClient((request) async { + expect( + request.url.toString(), + equals('$baseUrl/test/map_query_param_with_repeat' + '?value.bar=baz' + '&value.zap=abc' + '&value.etc.abc=def' + '&value.etc.ghi=jkl' + '&value.etc.mno.opq=rst' + '&value.etc.mno.uvw=xyz' + '&value.etc.mno.list=a' + '&value.etc.mno.list=123' + '&value.etc.mno.list=false' + '&value.etc.dt=${Uri.encodeComponent(now.toUtc().toIso8601String())}'), + ); + expect(request.method, equals('GET')); + + return http.Response('get response', 200); + }); + + final chopper = buildClient(httpClient); + final service = chopper.getService(); + + final response = + await service.getUsingMapQueryParamWithRepeat({ + 'bar': 'baz', + 'zap': 'abc', + 'etc': { + 'abc': 'def', + 'ghi': 'jkl', + 'mno': { + 'opq': 'rst', + 'uvw': 'xyz', + 'list': ['a', 123, false], + }, + 'dt': now, + }, + }); + + expect(response.body, equals('get response')); + expect(response.statusCode, equals(200)); + + httpClient.close(); + }); + + test('Map query param with comma QueryMapSeparator', () async { + final DateTime now = DateTime.now(); + + final httpClient = MockClient((request) async { + expect( + request.url.toString(), + equals('$baseUrl/test/map_query_param_with_comma' + '?value%5Bbar%5D=baz' + '&value%5Bzap%5D=abc' + '&value%5Betc%5D%5Babc%5D=def' + '&value%5Betc%5D%5Bghi%5D=jkl' + '&value%5Betc%5D%5Bmno%5D%5Bopq%5D=rst' + '&value%5Betc%5D%5Bmno%5D%5Buvw%5D=xyz' + '&value%5Betc%5D%5Bmno%5D%5Blist%5D=a%2C123%2Cfalse' + '&value%5Betc%5D%5Bdt%5D=${Uri.encodeComponent(now.toUtc().toIso8601String())}'), + ); + expect(request.method, equals('GET')); + + return http.Response('get response', 200); + }); + + final chopper = buildClient(httpClient); + final service = chopper.getService(); + + final response = + await service.getUsingMapQueryParamWithComma({ + 'bar': 'baz', + 'zap': 'abc', + 'etc': { + 'abc': 'def', + 'ghi': 'jkl', + 'mno': { + 'opq': 'rst', + 'uvw': 'xyz', + 'list': ['a', 123, false], + }, + 'dt': now, + }, + }); + + expect(response.body, equals('get response')); + expect(response.statusCode, equals(200)); + + httpClient.close(); + }); + test('Map query param without including null query vars', () async { final DateTime now = DateTime.now(); diff --git a/chopper/test/test_service.chopper.dart b/chopper/test/test_service.chopper.dart index c6335821..28dbbb81 100644 --- a/chopper/test/test_service.chopper.dart +++ b/chopper/test/test_service.chopper.dart @@ -580,6 +580,21 @@ final class _$HttpTestService extends HttpTestService { return client.send($request); } + @override + Future> getUsingListQueryParamWithBracketsLegacy( + List value) { + final Uri $url = Uri.parse('/test/list_query_param_with_brackets_legacy'); + final Map $params = {'value': value}; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + useBrackets: true, + ); + return client.send($request); + } + @override Future> getUsingListQueryParamWithBrackets( List value) { @@ -590,7 +605,51 @@ final class _$HttpTestService extends HttpTestService { $url, client.baseUrl, parameters: $params, - useBrackets: true, + listFormat: ListFormat.brackets, + ); + return client.send($request); + } + + @override + Future> getUsingListQueryParamWithIndices( + List value) { + final Uri $url = Uri.parse('/test/list_query_param_with_indices'); + final Map $params = {'value': value}; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + listFormat: ListFormat.indices, + ); + return client.send($request); + } + + @override + Future> getUsingListQueryParamWithRepeat( + List value) { + final Uri $url = Uri.parse('/test/list_query_param_with_repeat'); + final Map $params = {'value': value}; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + listFormat: ListFormat.repeat, + ); + return client.send($request); + } + + @override + Future> getUsingListQueryParamWithComma(List value) { + final Uri $url = Uri.parse('/test/list_query_param_with_comma'); + final Map $params = {'value': value}; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + listFormat: ListFormat.comma, ); return client.send($request); } @@ -623,6 +682,21 @@ final class _$HttpTestService extends HttpTestService { return client.send($request); } + @override + Future> getUsingMapQueryParamWithBracketsLegacy( + Map value) { + final Uri $url = Uri.parse('/test/map_query_param_with_brackets_legacy'); + final Map $params = {'value': value}; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + useBrackets: true, + ); + return client.send($request); + } + @override Future> getUsingMapQueryParamWithBrackets( Map value) { @@ -633,7 +707,52 @@ final class _$HttpTestService extends HttpTestService { $url, client.baseUrl, parameters: $params, - useBrackets: true, + listFormat: ListFormat.brackets, + ); + return client.send($request); + } + + @override + Future> getUsingMapQueryParamWithIndices( + Map value) { + final Uri $url = Uri.parse('/test/map_query_param_with_indices'); + final Map $params = {'value': value}; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + listFormat: ListFormat.indices, + ); + return client.send($request); + } + + @override + Future> getUsingMapQueryParamWithRepeat( + Map value) { + final Uri $url = Uri.parse('/test/map_query_param_with_repeat'); + final Map $params = {'value': value}; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + listFormat: ListFormat.repeat, + ); + return client.send($request); + } + + @override + Future> getUsingMapQueryParamWithComma( + Map value) { + final Uri $url = Uri.parse('/test/map_query_param_with_comma'); + final Map $params = {'value': value}; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + listFormat: ListFormat.comma, ); return client.send($request); } diff --git a/chopper/test/test_service.dart b/chopper/test/test_service.dart index 6ae6cb4e..cca87ca6 100644 --- a/chopper/test/test_service.dart +++ b/chopper/test/test_service.dart @@ -168,11 +168,31 @@ abstract class HttpTestService extends ChopperService { @Query('value') List value, ); - @Get(path: '/list_query_param_with_brackets', useBrackets: true) + @Get(path: '/list_query_param_with_brackets_legacy', useBrackets: true) + Future> getUsingListQueryParamWithBracketsLegacy( + @Query('value') List value, + ); + + @Get(path: '/list_query_param_with_brackets', listFormat: ListFormat.brackets) Future> getUsingListQueryParamWithBrackets( @Query('value') List value, ); + @Get(path: '/list_query_param_with_indices', listFormat: ListFormat.indices) + Future> getUsingListQueryParamWithIndices( + @Query('value') List value, + ); + + @Get(path: '/list_query_param_with_repeat', listFormat: ListFormat.repeat) + Future> getUsingListQueryParamWithRepeat( + @Query('value') List value, + ); + + @Get(path: '/list_query_param_with_comma', listFormat: ListFormat.comma) + Future> getUsingListQueryParamWithComma( + @Query('value') List value, + ); + @Get(path: '/map_query_param') Future> getUsingMapQueryParam( @Query('value') Map value, @@ -186,11 +206,31 @@ abstract class HttpTestService extends ChopperService { @Query('value') Map value, ); - @Get(path: '/map_query_param_with_brackets', useBrackets: true) + @Get(path: '/map_query_param_with_brackets_legacy', useBrackets: true) + Future> getUsingMapQueryParamWithBracketsLegacy( + @Query('value') Map value, + ); + + @Get(path: '/map_query_param_with_brackets', listFormat: ListFormat.brackets) Future> getUsingMapQueryParamWithBrackets( @Query('value') Map value, ); + @Get(path: '/map_query_param_with_indices', listFormat: ListFormat.indices) + Future> getUsingMapQueryParamWithIndices( + @Query('value') Map value, + ); + + @Get(path: '/map_query_param_with_repeat', listFormat: ListFormat.repeat) + Future> getUsingMapQueryParamWithRepeat( + @Query('value') Map value, + ); + + @Get(path: '/map_query_param_with_comma', listFormat: ListFormat.comma) + Future> getUsingMapQueryParamWithComma( + @Query('value') Map value, + ); + @Get(path: '/date_time') Future> getDateTime( @Query('value') DateTime value, diff --git a/chopper/test/test_service_base_url.chopper.dart b/chopper/test/test_service_base_url.chopper.dart index 6b859b03..262d72c7 100644 --- a/chopper/test/test_service_base_url.chopper.dart +++ b/chopper/test/test_service_base_url.chopper.dart @@ -86,6 +86,22 @@ final class _$HttpTestServiceBaseUrl extends HttpTestServiceBaseUrl { return client.send($request); } + @override + Future> getUsingListQueryParamWithBracketsLegacy( + List value) { + final Uri $url = Uri.parse( + 'https://localhost:4000/test/list_query_param_with_brackets_legacy'); + final Map $params = {'value': value}; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + useBrackets: true, + ); + return client.send($request); + } + @override Future> getUsingListQueryParamWithBrackets( List value) { @@ -97,7 +113,54 @@ final class _$HttpTestServiceBaseUrl extends HttpTestServiceBaseUrl { $url, client.baseUrl, parameters: $params, - useBrackets: true, + listFormat: ListFormat.brackets, + ); + return client.send($request); + } + + @override + Future> getUsingListQueryParamWithIndices( + List value) { + final Uri $url = + Uri.parse('https://localhost:4000/test/list_query_param_with_indices'); + final Map $params = {'value': value}; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + listFormat: ListFormat.indices, + ); + return client.send($request); + } + + @override + Future> getUsingListQueryParamWithRepeat( + List value) { + final Uri $url = + Uri.parse('https://localhost:4000/test/list_query_param_with_repeat'); + final Map $params = {'value': value}; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + listFormat: ListFormat.repeat, + ); + return client.send($request); + } + + @override + Future> getUsingListQueryParamWithComma(List value) { + final Uri $url = + Uri.parse('https://localhost:4000/test/list_query_param_with_comma'); + final Map $params = {'value': value}; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + listFormat: ListFormat.comma, ); return client.send($request); } @@ -131,6 +194,22 @@ final class _$HttpTestServiceBaseUrl extends HttpTestServiceBaseUrl { return client.send($request); } + @override + Future> getUsingMapQueryParamWithBracketsLegacy( + Map value) { + final Uri $url = Uri.parse( + 'https://localhost:4000/test/map_query_param_with_brackets_legacy'); + final Map $params = {'value': value}; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + useBrackets: true, + ); + return client.send($request); + } + @override Future> getUsingMapQueryParamWithBrackets( Map value) { @@ -142,7 +221,55 @@ final class _$HttpTestServiceBaseUrl extends HttpTestServiceBaseUrl { $url, client.baseUrl, parameters: $params, - useBrackets: true, + listFormat: ListFormat.brackets, + ); + return client.send($request); + } + + @override + Future> getUsingMapQueryParamWithIndices( + Map value) { + final Uri $url = + Uri.parse('https://localhost:4000/test/map_query_param_with_indices'); + final Map $params = {'value': value}; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + listFormat: ListFormat.indices, + ); + return client.send($request); + } + + @override + Future> getUsingMapQueryParamWithRepeat( + Map value) { + final Uri $url = + Uri.parse('https://localhost:4000/test/map_query_param_with_repeat'); + final Map $params = {'value': value}; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + listFormat: ListFormat.repeat, + ); + return client.send($request); + } + + @override + Future> getUsingMapQueryParamWithComa( + Map value) { + final Uri $url = + Uri.parse('https://localhost:4000/test/map_query_param_with_comma'); + final Map $params = {'value': value}; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + listFormat: ListFormat.comma, ); return client.send($request); } diff --git a/chopper/test/test_service_base_url.dart b/chopper/test/test_service_base_url.dart index 02de8bcf..905707f1 100644 --- a/chopper/test/test_service_base_url.dart +++ b/chopper/test/test_service_base_url.dart @@ -31,11 +31,31 @@ abstract class HttpTestServiceBaseUrl extends ChopperService { @Query('value') List value, ); - @Get(path: '/list_query_param_with_brackets', useBrackets: true) + @Get(path: '/list_query_param_with_brackets_legacy', useBrackets: true) + Future> getUsingListQueryParamWithBracketsLegacy( + @Query('value') List value, + ); + + @Get(path: '/list_query_param_with_brackets', listFormat: ListFormat.brackets) Future> getUsingListQueryParamWithBrackets( @Query('value') List value, ); + @Get(path: '/list_query_param_with_indices', listFormat: ListFormat.indices) + Future> getUsingListQueryParamWithIndices( + @Query('value') List value, + ); + + @Get(path: '/list_query_param_with_repeat', listFormat: ListFormat.repeat) + Future> getUsingListQueryParamWithRepeat( + @Query('value') List value, + ); + + @Get(path: '/list_query_param_with_comma', listFormat: ListFormat.comma) + Future> getUsingListQueryParamWithComma( + @Query('value') List value, + ); + @Get(path: '/map_query_param') Future> getUsingMapQueryParam( @Query('value') Map value, @@ -49,10 +69,30 @@ abstract class HttpTestServiceBaseUrl extends ChopperService { @Query('value') Map value, ); - @Get(path: '/map_query_param_with_brackets', useBrackets: true) + @Get(path: '/map_query_param_with_brackets_legacy', useBrackets: true) + Future> getUsingMapQueryParamWithBracketsLegacy( + @Query('value') Map value, + ); + + @Get(path: '/map_query_param_with_brackets', listFormat: ListFormat.brackets) Future> getUsingMapQueryParamWithBrackets( @Query('value') Map value, ); + + @Get(path: '/map_query_param_with_indices', listFormat: ListFormat.indices) + Future> getUsingMapQueryParamWithIndices( + @Query('value') Map value, + ); + + @Get(path: '/map_query_param_with_repeat', listFormat: ListFormat.repeat) + Future> getUsingMapQueryParamWithRepeat( + @Query('value') Map value, + ); + + @Get(path: '/map_query_param_with_comma', listFormat: ListFormat.comma) + Future> getUsingMapQueryParamWithComa( + @Query('value') Map value, + ); } Request customConvertRequest(Request req) { diff --git a/chopper/test/test_service_variable.chopper.dart b/chopper/test/test_service_variable.chopper.dart index 9c69ffa0..c60e766e 100644 --- a/chopper/test/test_service_variable.chopper.dart +++ b/chopper/test/test_service_variable.chopper.dart @@ -581,6 +581,22 @@ final class _$HttpTestServiceVariable extends HttpTestServiceVariable { return client.send($request); } + @override + Future> getUsingListQueryParamWithBracketsLegacy( + List value) { + final Uri $url = + Uri.parse('${service}/list_query_param_with_brackets_legacy'); + final Map $params = {'value': value}; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + useBrackets: true, + ); + return client.send($request); + } + @override Future> getUsingListQueryParamWithBrackets( List value) { @@ -591,7 +607,51 @@ final class _$HttpTestServiceVariable extends HttpTestServiceVariable { $url, client.baseUrl, parameters: $params, - useBrackets: true, + listFormat: ListFormat.brackets, + ); + return client.send($request); + } + + @override + Future> getUsingListQueryParamWithIndices( + List value) { + final Uri $url = Uri.parse('${service}/list_query_param_with_indices'); + final Map $params = {'value': value}; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + listFormat: ListFormat.indices, + ); + return client.send($request); + } + + @override + Future> getUsingListQueryParamWithRepeat( + List value) { + final Uri $url = Uri.parse('${service}/list_query_param_with_repeat'); + final Map $params = {'value': value}; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + listFormat: ListFormat.repeat, + ); + return client.send($request); + } + + @override + Future> getUsingListQueryParamWithComma(List value) { + final Uri $url = Uri.parse('${service}/list_query_param_with_comma'); + final Map $params = {'value': value}; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + listFormat: ListFormat.comma, ); return client.send($request); } @@ -625,6 +685,22 @@ final class _$HttpTestServiceVariable extends HttpTestServiceVariable { return client.send($request); } + @override + Future> getUsingMapQueryParamWithBracketsLegacy( + Map value) { + final Uri $url = + Uri.parse('${service}/map_query_param_with_brackets_legacy'); + final Map $params = {'value': value}; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + useBrackets: true, + ); + return client.send($request); + } + @override Future> getUsingMapQueryParamWithBrackets( Map value) { @@ -635,7 +711,52 @@ final class _$HttpTestServiceVariable extends HttpTestServiceVariable { $url, client.baseUrl, parameters: $params, - useBrackets: true, + listFormat: ListFormat.brackets, + ); + return client.send($request); + } + + @override + Future> getUsingMapQueryParamWithIndices( + Map value) { + final Uri $url = Uri.parse('${service}/map_query_param_with_indices'); + final Map $params = {'value': value}; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + listFormat: ListFormat.indices, + ); + return client.send($request); + } + + @override + Future> getUsingMapQueryParamWithRepeat( + Map value) { + final Uri $url = Uri.parse('${service}/map_query_param_with_repeat'); + final Map $params = {'value': value}; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + listFormat: ListFormat.repeat, + ); + return client.send($request); + } + + @override + Future> getUsingMapQueryParamWithComma( + Map value) { + final Uri $url = Uri.parse('${service}/map_query_param_with_comma'); + final Map $params = {'value': value}; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + listFormat: ListFormat.comma, ); return client.send($request); } diff --git a/chopper/test/test_service_variable.dart b/chopper/test/test_service_variable.dart index 251b48cb..c878ada8 100644 --- a/chopper/test/test_service_variable.dart +++ b/chopper/test/test_service_variable.dart @@ -168,11 +168,31 @@ abstract class HttpTestServiceVariable extends ChopperService { @Query('value') List value, ); - @Get(path: '/list_query_param_with_brackets', useBrackets: true) + @Get(path: '/list_query_param_with_brackets_legacy', useBrackets: true) + Future> getUsingListQueryParamWithBracketsLegacy( + @Query('value') List value, + ); + + @Get(path: '/list_query_param_with_brackets', listFormat: ListFormat.brackets) Future> getUsingListQueryParamWithBrackets( @Query('value') List value, ); + @Get(path: '/list_query_param_with_indices', listFormat: ListFormat.indices) + Future> getUsingListQueryParamWithIndices( + @Query('value') List value, + ); + + @Get(path: '/list_query_param_with_repeat', listFormat: ListFormat.repeat) + Future> getUsingListQueryParamWithRepeat( + @Query('value') List value, + ); + + @Get(path: '/list_query_param_with_comma', listFormat: ListFormat.comma) + Future> getUsingListQueryParamWithComma( + @Query('value') List value, + ); + @Get(path: '/map_query_param') Future> getUsingMapQueryParam( @Query('value') Map value, @@ -186,10 +206,30 @@ abstract class HttpTestServiceVariable extends ChopperService { @Query('value') Map value, ); - @Get(path: '/map_query_param_with_brackets', useBrackets: true) + @Get(path: '/map_query_param_with_brackets_legacy', useBrackets: true) + Future> getUsingMapQueryParamWithBracketsLegacy( + @Query('value') Map value, + ); + + @Get(path: '/map_query_param_with_brackets', listFormat: ListFormat.brackets) Future> getUsingMapQueryParamWithBrackets( @Query('value') Map value, ); + + @Get(path: '/map_query_param_with_indices', listFormat: ListFormat.indices) + Future> getUsingMapQueryParamWithIndices( + @Query('value') Map value, + ); + + @Get(path: '/map_query_param_with_repeat', listFormat: ListFormat.repeat) + Future> getUsingMapQueryParamWithRepeat( + @Query('value') Map value, + ); + + @Get(path: '/map_query_param_with_comma', listFormat: ListFormat.comma) + Future> getUsingMapQueryParamWithComma( + @Query('value') Map value, + ); } Request customConvertRequest(Request req) { diff --git a/chopper/test/test_without_response_service.chopper.dart b/chopper/test/test_without_response_service.chopper.dart index c7b8462a..2bc8fcbd 100644 --- a/chopper/test/test_without_response_service.chopper.dart +++ b/chopper/test/test_without_response_service.chopper.dart @@ -616,6 +616,22 @@ final class _$HttpTestService extends HttpTestService { return $response.bodyOrThrow; } + @override + Future getUsingListQueryParamWithBracketsLegacy( + List value) async { + final Uri $url = Uri.parse('/test/list_query_param_with_brackets_legacy'); + final Map $params = {'value': value}; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + useBrackets: true, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + @override Future getUsingListQueryParamWithBrackets(List value) async { final Uri $url = Uri.parse('/test/list_query_param_with_brackets'); @@ -625,7 +641,52 @@ final class _$HttpTestService extends HttpTestService { $url, client.baseUrl, parameters: $params, - useBrackets: true, + listFormat: ListFormat.brackets, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + + @override + Future getUsingListQueryParamWithIndices(List value) async { + final Uri $url = Uri.parse('/test/list_query_param_with_indices'); + final Map $params = {'value': value}; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + listFormat: ListFormat.indices, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + + @override + Future getUsingListQueryParamWithRepeat(List value) async { + final Uri $url = Uri.parse('/test/list_query_param_with_repeat'); + final Map $params = {'value': value}; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + listFormat: ListFormat.repeat, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + + @override + Future getUsingListQueryParamWithComma(List value) async { + final Uri $url = Uri.parse('/test/list_query_param_with_comma'); + final Map $params = {'value': value}; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + listFormat: ListFormat.comma, ); final Response $response = await client.send($request); return $response.bodyOrThrow; @@ -661,6 +722,22 @@ final class _$HttpTestService extends HttpTestService { return $response.bodyOrThrow; } + @override + Future getUsingMapQueryParamWithBracketsLegacy( + Map value) async { + final Uri $url = Uri.parse('/test/map_query_param_with_brackets_legacy'); + final Map $params = {'value': value}; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + useBrackets: true, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + @override Future getUsingMapQueryParamWithBrackets( Map value) async { @@ -671,7 +748,55 @@ final class _$HttpTestService extends HttpTestService { $url, client.baseUrl, parameters: $params, - useBrackets: true, + listFormat: ListFormat.brackets, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + + @override + Future getUsingMapQueryParamWithIndices( + Map value) async { + final Uri $url = Uri.parse('/test/map_query_param_with_indices'); + final Map $params = {'value': value}; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + listFormat: ListFormat.indices, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + + @override + Future getUsingMapQueryParamWithRepeat( + Map value) async { + final Uri $url = Uri.parse('/test/map_query_param_with_repeat'); + final Map $params = {'value': value}; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + listFormat: ListFormat.repeat, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + + @override + Future getUsingMapQueryParamWithComma( + Map value) async { + final Uri $url = Uri.parse('/test/map_query_param_with_comma'); + final Map $params = {'value': value}; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + listFormat: ListFormat.comma, ); final Response $response = await client.send($request); return $response.bodyOrThrow; diff --git a/chopper/test/test_without_response_service.dart b/chopper/test/test_without_response_service.dart index d8a95417..d29c0eb9 100644 --- a/chopper/test/test_without_response_service.dart +++ b/chopper/test/test_without_response_service.dart @@ -166,11 +166,31 @@ abstract class HttpTestService extends ChopperService { @Query('value') List value, ); - @Get(path: '/list_query_param_with_brackets', useBrackets: true) + @Get(path: '/list_query_param_with_brackets_legacy', useBrackets: true) + Future getUsingListQueryParamWithBracketsLegacy( + @Query('value') List value, + ); + + @Get(path: '/list_query_param_with_brackets', listFormat: ListFormat.brackets) Future getUsingListQueryParamWithBrackets( @Query('value') List value, ); + @Get(path: '/list_query_param_with_indices', listFormat: ListFormat.indices) + Future getUsingListQueryParamWithIndices( + @Query('value') List value, + ); + + @Get(path: '/list_query_param_with_repeat', listFormat: ListFormat.repeat) + Future getUsingListQueryParamWithRepeat( + @Query('value') List value, + ); + + @Get(path: '/list_query_param_with_comma', listFormat: ListFormat.comma) + Future getUsingListQueryParamWithComma( + @Query('value') List value, + ); + @Get(path: '/map_query_param') Future getUsingMapQueryParam( @Query('value') Map value, @@ -184,11 +204,31 @@ abstract class HttpTestService extends ChopperService { @Query('value') Map value, ); - @Get(path: '/map_query_param_with_brackets', useBrackets: true) + @Get(path: '/map_query_param_with_brackets_legacy', useBrackets: true) + Future getUsingMapQueryParamWithBracketsLegacy( + @Query('value') Map value, + ); + + @Get(path: '/map_query_param_with_brackets', listFormat: ListFormat.brackets) Future getUsingMapQueryParamWithBrackets( @Query('value') Map value, ); + @Get(path: '/map_query_param_with_indices', listFormat: ListFormat.indices) + Future getUsingMapQueryParamWithIndices( + @Query('value') Map value, + ); + + @Get(path: '/map_query_param_with_repeat', listFormat: ListFormat.repeat) + Future getUsingMapQueryParamWithRepeat( + @Query('value') Map value, + ); + + @Get(path: '/map_query_param_with_comma', listFormat: ListFormat.comma) + Future getUsingMapQueryParamWithComma( + @Query('value') Map value, + ); + @Get(path: 'headers') Future getHeaders({ @Header('x-string') required String stringHeader, diff --git a/chopper/test/utils_test.dart b/chopper/test/utils_test.dart index 63a35084..fc4e6810 100644 --- a/chopper/test/utils_test.dart +++ b/chopper/test/utils_test.dart @@ -1,12 +1,17 @@ +// ignore_for_file: deprecated_member_use_from_same_package + +import 'package:chopper/src/list_format.dart'; import 'package:chopper/src/request.dart'; import 'package:chopper/src/utils.dart'; import 'package:test/test.dart'; +import 'fixtures/example_enum.dart'; + void main() { group('mapToQuery single', () { , String>{ {'foo': null}: '', - {'foo': ''}: 'foo=', + {'foo': ''}: '', {'foo': ' '}: 'foo=%20', {'foo': ' '}: 'foo=%20%20', {'foo': '\t'}: 'foo=%09', @@ -62,12 +67,12 @@ void main() { group('mapToQuery multiple', () { , String>{ {'foo': null, 'baz': null}: '', - {'foo': '', 'baz': ''}: 'foo=&baz=', - {'foo': null, 'baz': ''}: 'baz=', - {'foo': '', 'baz': null}: 'foo=', - {'foo': 'bar', 'baz': ''}: 'foo=bar&baz=', + {'foo': '', 'baz': ''}: '', + {'foo': null, 'baz': ''}: '', + {'foo': '', 'baz': null}: '', + {'foo': 'bar', 'baz': ''}: 'foo=bar', {'foo': null, 'baz': 'etc'}: 'baz=etc', - {'foo': '', 'baz': 'etc'}: 'foo=&baz=etc', + {'foo': '', 'baz': 'etc'}: 'baz=etc', {'foo': 'bar', 'baz': 'etc'}: 'foo=bar&baz=etc', {'foo': 'null', 'baz': 'null'}: 'foo=null&baz=null', {'foo': ' ', 'baz': ' '}: 'foo=%20&baz=%20', @@ -113,7 +118,7 @@ void main() { ); }); - group('mapToQuery lists', () { + group('mapToQuery lists with repeat (default)', () { , String>{ { 'foo': ['bar', 'baz', 'etc'], @@ -150,12 +155,28 @@ void main() { 'bar': 'baz', 'etc': '', 'xyz': null, - }: 'foo=bar&foo=baz&foo=etc&bar=baz&etc=', - }.forEach((map, query) => - test('$map -> $query', () => expect(mapToQuery(map), query))); + }: 'foo=bar&foo=baz&foo=etc&bar=baz', + }.forEach((map, query) { + test( + '$map -> $query', + () => expect( + mapToQuery(map), + query, + reason: 'legacy default', + ), + ); + + test( + '$map -> $query', + () => expect( + mapToQuery(map, listFormat: ListFormat.repeat), + query, + ), + ); + }); }); - group('mapToQuery lists with includeNullQueryVars', () { + group('mapToQuery lists with repeat (default) with includeNullQueryVars', () { , String>{ { 'foo': ['bar', 'baz', 'etc'], @@ -165,22 +186,22 @@ void main() { }: 'foo=bar&foo=123&foo=456.789&foo=0&foo=-123&foo=-456.789', { 'foo': ['', 'baz', 'etc'], - }: 'foo=baz&foo=etc', + }: 'foo=&foo=baz&foo=etc', { 'foo': ['bar', '', 'etc'], - }: 'foo=bar&foo=etc', + }: 'foo=bar&foo=&foo=etc', { 'foo': ['bar', 'baz', ''], - }: 'foo=bar&foo=baz', + }: 'foo=bar&foo=baz&foo=', { 'foo': [null, 'baz', 'etc'], - }: 'foo=baz&foo=etc', + }: 'foo=&foo=baz&foo=etc', { 'foo': ['bar', null, 'etc'], - }: 'foo=bar&foo=etc', + }: 'foo=bar&foo=&foo=etc', { 'foo': ['bar', 'baz', null], - }: 'foo=bar&foo=baz', + }: 'foo=bar&foo=baz&foo=', { 'foo': ['bar', 'baz', ' '], }: 'foo=bar&foo=baz&foo=%20', @@ -194,10 +215,28 @@ void main() { 'xyz': null, }: 'foo=bar&foo=baz&foo=etc&bar=baz&etc=&xyz=', }.forEach( - (map, query) => test( - '$map -> $query', - () => expect(mapToQuery(map, includeNullQueryVars: true), query), - ), + (map, query) { + test( + '$map -> $query', + () => expect( + mapToQuery(map, includeNullQueryVars: true), + query, + reason: 'legacy default', + ), + ); + + test( + '$map -> $query', + () => expect( + mapToQuery( + map, + listFormat: ListFormat.repeat, + includeNullQueryVars: true, + ), + query, + ), + ); + }, ); }); @@ -238,15 +277,26 @@ void main() { 'bar': 'baz', 'etc': '', 'xyz': null, - }: 'foo%5B%5D=bar&foo%5B%5D=baz&foo%5B%5D=etc&bar=baz&etc=', + }: 'foo%5B%5D=bar&foo%5B%5D=baz&foo%5B%5D=etc&bar=baz', }.forEach( - (map, query) => test( - '$map -> $query', - () => expect( - mapToQuery(map, useBrackets: true), - query, - ), - ), + (map, query) { + test( + '$map -> $query', + () => expect( + mapToQuery(map, useBrackets: true), + query, + reason: 'legacy brackets', + ), + ); + + test( + '$map -> $query', + () => expect( + mapToQuery(map, listFormat: ListFormat.brackets), + query, + ), + ); + }, ); }); @@ -260,22 +310,22 @@ void main() { }: 'foo%5B%5D=bar&foo%5B%5D=123&foo%5B%5D=456.789&foo%5B%5D=0&foo%5B%5D=-123&foo%5B%5D=-456.789', { 'foo': ['', 'baz', 'etc'], - }: 'foo%5B%5D=baz&foo%5B%5D=etc', + }: 'foo%5B%5D=&foo%5B%5D=baz&foo%5B%5D=etc', { 'foo': ['bar', '', 'etc'], - }: 'foo%5B%5D=bar&foo%5B%5D=etc', + }: 'foo%5B%5D=bar&foo%5B%5D=&foo%5B%5D=etc', { 'foo': ['bar', 'baz', ''], - }: 'foo%5B%5D=bar&foo%5B%5D=baz', + }: 'foo%5B%5D=bar&foo%5B%5D=baz&foo%5B%5D=', { 'foo': [null, 'baz', 'etc'], - }: 'foo%5B%5D=baz&foo%5B%5D=etc', + }: 'foo%5B%5D=&foo%5B%5D=baz&foo%5B%5D=etc', { 'foo': ['bar', null, 'etc'], - }: 'foo%5B%5D=bar&foo%5B%5D=etc', + }: 'foo%5B%5D=bar&foo%5B%5D=&foo%5B%5D=etc', { 'foo': ['bar', 'baz', null], - }: 'foo%5B%5D=bar&foo%5B%5D=baz', + }: 'foo%5B%5D=bar&foo%5B%5D=baz&foo%5B%5D=', { 'foo': ['bar', 'baz', ' '], }: 'foo%5B%5D=bar&foo%5B%5D=baz&foo%5B%5D=%20', @@ -288,25 +338,248 @@ void main() { 'etc': '', 'xyz': null, }: 'foo%5B%5D=bar&foo%5B%5D=baz&foo%5B%5D=etc&bar=baz&etc=&xyz=', + }.forEach( + (map, query) { + test( + '$map -> $query', + () => expect( + mapToQuery( + map, + useBrackets: true, + includeNullQueryVars: true, + ), + query, + reason: 'legacy brackets', + ), + ); + + test( + '$map -> $query', + () => expect( + mapToQuery( + map, + listFormat: ListFormat.brackets, + includeNullQueryVars: true, + ), + query, + ), + ); + }, + ); + }); + + group('mapToQuery lists with indices', () { + , String>{ + { + 'foo': ['bar', 'baz', 'etc'], + }: 'foo%5B0%5D=bar&foo%5B1%5D=baz&foo%5B2%5D=etc', + { + 'foo': ['bar', 123, 456.789, 0, -123, -456.789], + }: 'foo%5B0%5D=bar&foo%5B1%5D=123&foo%5B2%5D=456.789&foo%5B3%5D=0&foo%5B4%5D=-123&foo%5B5%5D=-456.789', + { + 'foo': ['', 'baz', 'etc'], + }: 'foo%5B1%5D=baz&foo%5B2%5D=etc', + { + 'foo': ['bar', '', 'etc'], + }: 'foo%5B0%5D=bar&foo%5B2%5D=etc', + { + 'foo': ['bar', 'baz', ''], + }: 'foo%5B0%5D=bar&foo%5B1%5D=baz', + { + 'foo': [null, 'baz', 'etc'], + }: 'foo%5B1%5D=baz&foo%5B2%5D=etc', + { + 'foo': ['bar', null, 'etc'], + }: 'foo%5B0%5D=bar&foo%5B2%5D=etc', + { + 'foo': ['bar', 'baz', null], + }: 'foo%5B0%5D=bar&foo%5B1%5D=baz', + { + 'foo': ['bar', 'baz', ' '], + }: 'foo%5B0%5D=bar&foo%5B1%5D=baz&foo%5B2%5D=%20', + { + 'foo': ['bar', 'baz', '\t'], + }: 'foo%5B0%5D=bar&foo%5B1%5D=baz&foo%5B2%5D=%09', + { + 'foo': ['bar', 'baz', 'etc'], + 'bar': 'baz', + 'etc': '', + 'xyz': null, + }: 'foo%5B0%5D=bar&foo%5B1%5D=baz&foo%5B2%5D=etc&bar=baz', + }.forEach( + (map, query) => test( + '$map -> $query', + () => expect( + mapToQuery(map, listFormat: ListFormat.indices), + query, + ), + ), + ); + }); + + group('mapToQuery lists with indices with includeNullQueryVars', () { + , String>{ + { + 'foo': ['bar', 'baz', 'etc'], + }: 'foo%5B0%5D=bar&foo%5B1%5D=baz&foo%5B2%5D=etc', + { + 'foo': ['bar', 123, 456.789, 0, -123, -456.789], + }: 'foo%5B0%5D=bar&foo%5B1%5D=123&foo%5B2%5D=456.789&foo%5B3%5D=0&foo%5B4%5D=-123&foo%5B5%5D=-456.789', + { + 'foo': ['', 'baz', 'etc'], + }: 'foo%5B0%5D=&foo%5B1%5D=baz&foo%5B2%5D=etc', + { + 'foo': ['bar', '', 'etc'], + }: 'foo%5B0%5D=bar&foo%5B1%5D=&foo%5B2%5D=etc', + { + 'foo': ['bar', 'baz', ''], + }: 'foo%5B0%5D=bar&foo%5B1%5D=baz&foo%5B2%5D=', + { + 'foo': [null, 'baz', 'etc'], + }: 'foo%5B0%5D=&foo%5B1%5D=baz&foo%5B2%5D=etc', + { + 'foo': ['bar', null, 'etc'], + }: 'foo%5B0%5D=bar&foo%5B1%5D=&foo%5B2%5D=etc', + { + 'foo': ['bar', 'baz', null], + }: 'foo%5B0%5D=bar&foo%5B1%5D=baz&foo%5B2%5D=', + { + 'foo': ['bar', 'baz', ' '], + }: 'foo%5B0%5D=bar&foo%5B1%5D=baz&foo%5B2%5D=%20', + { + 'foo': ['bar', 'baz', '\t'], + }: 'foo%5B0%5D=bar&foo%5B1%5D=baz&foo%5B2%5D=%09', + { + 'foo': ['bar', 'baz', 'etc'], + 'bar': 'baz', + 'etc': '', + 'xyz': null, + }: 'foo%5B0%5D=bar&foo%5B1%5D=baz&foo%5B2%5D=etc&bar=baz&etc=&xyz=', + }.forEach( + (map, query) => test( + '$map -> $query', + () => expect( + mapToQuery( + map, + listFormat: ListFormat.indices, + includeNullQueryVars: true, + ), + query, + ), + ), + ); + }); + + group('mapToQuery lists with comma', () { + , String>{ + { + 'foo': ['bar', 'baz', 'etc'], + }: 'foo=bar%2Cbaz%2Cetc', + { + 'foo': ['bar', 123, 456.789, 0, -123, -456.789], + }: 'foo=bar%2C123%2C456.789%2C0%2C-123%2C-456.789', + { + 'foo': ['', 'baz', 'etc'], + }: 'foo=%2Cbaz%2Cetc', + { + 'foo': ['bar', '', 'etc'], + }: 'foo=bar%2C%2Cetc', + { + 'foo': ['bar', 'baz', ''], + }: 'foo=bar%2Cbaz%2C', + { + 'foo': [null, 'baz', 'etc'], + }: 'foo=%2Cbaz%2Cetc', + { + 'foo': ['bar', null, 'etc'], + }: 'foo=bar%2C%2Cetc', + { + 'foo': ['bar', 'baz', null], + }: 'foo=bar%2Cbaz%2C', + { + 'foo': ['bar', 'baz', ' '], + }: 'foo=bar%2Cbaz%2C%20', + { + 'foo': ['bar', 'baz', '\t'], + }: 'foo=bar%2Cbaz%2C%09', + { + 'foo': ['bar', 'baz', 'etc'], + 'bar': 'baz', + 'etc': '', + 'xyz': null, + }: 'foo=bar%2Cbaz%2Cetc&bar=baz', + }.forEach( + (map, query) => test( + '$map -> $query', + () => expect( + mapToQuery(map, listFormat: ListFormat.comma), + query, + ), + ), + ); + }); + + group('mapToQuery lists with comma with includeNullQueryVars', () { + , String>{ + { + 'foo': ['bar', 'baz', 'etc'], + }: 'foo=bar%2Cbaz%2Cetc', + { + 'foo': ['bar', 123, 456.789, 0, -123, -456.789], + }: 'foo=bar%2C123%2C456.789%2C0%2C-123%2C-456.789', + { + 'foo': ['', 'baz', 'etc'], + }: 'foo=%2Cbaz%2Cetc', + { + 'foo': ['bar', '', 'etc'], + }: 'foo=bar%2C%2Cetc', + { + 'foo': ['bar', 'baz', ''], + }: 'foo=bar%2Cbaz%2C', + { + 'foo': [null, 'baz', 'etc'], + }: 'foo=%2Cbaz%2Cetc', + { + 'foo': ['bar', null, 'etc'], + }: 'foo=bar%2C%2Cetc', + { + 'foo': ['bar', 'baz', null], + }: 'foo=bar%2Cbaz%2C', + { + 'foo': ['bar', 'baz', ' '], + }: 'foo=bar%2Cbaz%2C%20', + { + 'foo': ['bar', 'baz', '\t'], + }: 'foo=bar%2Cbaz%2C%09', + { + 'foo': ['bar', 'baz', 'etc'], + 'bar': 'baz', + 'etc': '', + 'xyz': null, + }: 'foo=bar%2Cbaz%2Cetc&bar=baz&etc=&xyz=', }.forEach( (map, query) => test( '$map -> $query', () => expect( - mapToQuery(map, useBrackets: true, includeNullQueryVars: true), + mapToQuery( + map, + listFormat: ListFormat.comma, + includeNullQueryVars: true, + ), query, ), ), ); }); - group('mapToQuery maps', () { + group('mapToQuery maps with repeat (default)', () { , String>{ { 'foo': {'bar': 'baz'}, }: 'foo.bar=baz', { 'foo': {'bar': ''}, - }: 'foo.bar=', + }: '', { 'foo': {'bar': null}, }: '', @@ -333,7 +606,7 @@ void main() { 'tab': '\t', 'list': ['a', 123, false], }, - }: 'foo.bar=baz&foo.int=123&foo.double=456.789&foo.zero=0&foo.negInt=-123&foo.negDouble=-456.789&foo.emptyString=&foo.space=%20&foo.tab=%09&foo.list=a&foo.list=123&foo.list=false', + }: 'foo.bar=baz&foo.int=123&foo.double=456.789&foo.zero=0&foo.negInt=-123&foo.negDouble=-456.789&foo.space=%20&foo.tab=%09&foo%2Elist=a&foo%2Elist=123&foo%2Elist=false', { 'foo': {'bar': 'baz'}, 'etc': 'xyz', @@ -356,12 +629,49 @@ void main() { }, }, }, - }: 'foo.bar=baz&foo.zap=abc&foo.etc.abc=def&foo.etc.ghi=jkl&foo.etc.mno.opq=rst&foo.etc.mno.uvw=xyz&foo.etc.mno.aab=bbc&foo.etc.mno.aab=ccd&foo.etc.mno.aab=eef', - }.forEach((map, query) => - test('$map -> $query', () => expect(mapToQuery(map), query))); + }: 'foo.bar=baz&foo.zap=abc&foo%2Eetc.abc=def&foo%2Eetc.ghi=jkl&foo%2Eetc%2Emno.opq=rst&foo%2Eetc%2Emno.uvw=xyz&foo%2Eetc%2Emno%2Eaab=bbc&foo%2Eetc%2Emno%2Eaab=ccd&foo%2Eetc%2Emno%2Eaab=eef', + { + 'filters': { + r'$or': [ + { + 'date': { + r'$eq': '2020-01-01', + } + }, + { + 'date': { + r'$eq': '2020-01-02', + } + } + ], + 'author': { + 'name': { + r'$eq': 'John doe', + }, + } + } + }: r'filters%2E$or%2Edate.$eq=2020-01-01&filters%2E$or%2Edate.$eq=2020-01-02&filters%2Eauthor%2Ename.$eq=John%20doe', + }.forEach((map, query) { + test( + '$map -> $query', + () => expect( + mapToQuery(map), + query, + reason: 'legacy default', + ), + ); + + test( + '$map -> $query', + () => expect( + mapToQuery(map, listFormat: ListFormat.repeat), + query, + ), + ); + }); }); - group('mapToQuery maps with includeNullQueryVars', () { + group('mapToQuery maps with repeat (default) with includeNullQueryVars', () { , String>{ { 'foo': {'bar': 'baz'}, @@ -395,7 +705,7 @@ void main() { 'tab': '\t', 'list': ['a', 123, false], }, - }: 'foo.bar=baz&foo.int=123&foo.double=456.789&foo.zero=0&foo.negInt=-123&foo.negDouble=-456.789&foo.emptyString=&foo.nullValue=&foo.space=%20&foo.tab=%09&foo.list=a&foo.list=123&foo.list=false', + }: 'foo.bar=baz&foo.int=123&foo.double=456.789&foo.zero=0&foo.negInt=-123&foo.negDouble=-456.789&foo.emptyString=&foo.nullValue=&foo.space=%20&foo.tab=%09&foo%2Elist=a&foo%2Elist=123&foo%2Elist=false', { 'foo': {'bar': 'baz'}, 'etc': 'xyz', @@ -418,12 +728,51 @@ void main() { }, }, }, - }: 'foo.bar=baz&foo.zap=abc&foo.etc.abc=def&foo.etc.ghi=jkl&foo.etc.mno.opq=rst&foo.etc.mno.uvw=xyz&foo.etc.mno.aab=bbc&foo.etc.mno.aab=ccd&foo.etc.mno.aab=eef', + }: 'foo.bar=baz&foo.zap=abc&foo%2Eetc.abc=def&foo%2Eetc.ghi=jkl&foo%2Eetc%2Emno.opq=rst&foo%2Eetc%2Emno.uvw=xyz&foo%2Eetc%2Emno%2Eaab=bbc&foo%2Eetc%2Emno%2Eaab=ccd&foo%2Eetc%2Emno%2Eaab=eef', + { + 'filters': { + r'$or': [ + { + 'date': { + r'$eq': '2020-01-01', + } + }, + { + 'date': { + r'$eq': '2020-01-02', + } + } + ], + 'author': { + 'name': { + r'$eq': 'John doe', + }, + } + } + }: r'filters%2E$or%2Edate.$eq=2020-01-01&filters%2E$or%2Edate.$eq=2020-01-02&filters%2Eauthor%2Ename.$eq=John%20doe', }.forEach( - (map, query) => test( - '$map -> $query', - () => expect(mapToQuery(map, includeNullQueryVars: true), query), - ), + (map, query) { + test( + '$map -> $query', + () => expect( + mapToQuery(map, includeNullQueryVars: true), + query, + reason: 'legacy default', + ), + ); + + test( + '$map -> $query', + () => expect( + mapToQuery( + map, + listFormat: ListFormat.repeat, + includeNullQueryVars: true, + ), + query, + ), + ); + }, ); }); @@ -434,7 +783,7 @@ void main() { }: 'foo%5Bbar%5D=baz', { 'foo': {'bar': ''}, - }: 'foo%5Bbar%5D=', + }: '', { 'foo': {'bar': null}, }: '', @@ -461,7 +810,7 @@ void main() { 'tab': '\t', 'list': ['a', 123, false], }, - }: 'foo%5Bbar%5D=baz&foo%5Bint%5D=123&foo%5Bdouble%5D=456.789&foo%5Bzero%5D=0&foo%5BnegInt%5D=-123&foo%5BnegDouble%5D=-456.789&foo%5BemptyString%5D=&foo%5Bspace%5D=%20&foo%5Btab%5D=%09&foo%5Blist%5D%5B%5D=a&foo%5Blist%5D%5B%5D=123&foo%5Blist%5D%5B%5D=false', + }: 'foo%5Bbar%5D=baz&foo%5Bint%5D=123&foo%5Bdouble%5D=456.789&foo%5Bzero%5D=0&foo%5BnegInt%5D=-123&foo%5BnegDouble%5D=-456.789&foo%5Bspace%5D=%20&foo%5Btab%5D=%09&foo%5Blist%5D%5B%5D=a&foo%5Blist%5D%5B%5D=123&foo%5Blist%5D%5B%5D=false', { 'foo': {'bar': 'baz'}, 'etc': 'xyz', @@ -485,14 +834,46 @@ void main() { }, }, }: 'foo%5Bbar%5D=baz&foo%5Bzap%5D=abc&foo%5Betc%5D%5Babc%5D=def&foo%5Betc%5D%5Bghi%5D=jkl&foo%5Betc%5D%5Bmno%5D%5Bopq%5D=rst&foo%5Betc%5D%5Bmno%5D%5Buvw%5D=xyz&foo%5Betc%5D%5Bmno%5D%5Baab%5D%5B%5D=bbc&foo%5Betc%5D%5Bmno%5D%5Baab%5D%5B%5D=ccd&foo%5Betc%5D%5Bmno%5D%5Baab%5D%5B%5D=eef', + { + 'filters': { + r'$or': [ + { + 'date': { + r'$eq': '2020-01-01', + } + }, + { + 'date': { + r'$eq': '2020-01-02', + } + } + ], + 'author': { + 'name': { + r'$eq': 'John doe', + }, + } + } + }: 'filters%5B%24or%5D%5B%5D%5Bdate%5D%5B%24eq%5D=2020-01-01&filters%5B%24or%5D%5B%5D%5Bdate%5D%5B%24eq%5D=2020-01-02&filters%5Bauthor%5D%5Bname%5D%5B%24eq%5D=John%20doe', }.forEach( - (map, query) => test( - '$map -> $query', - () => expect( - mapToQuery(map, useBrackets: true), - query, - ), - ), + (map, query) { + test( + '$map -> $query', + () => expect( + mapToQuery(map, useBrackets: true), + query, + reason: 'legacy brackets', + ), + ); + + test( + '$map -> $query', + () => expect( + mapToQuery(map, listFormat: ListFormat.brackets), + query, + ), + ); + }, ); }); @@ -554,17 +935,885 @@ void main() { }, }, }: 'foo%5Bbar%5D=baz&foo%5Bzap%5D=abc&foo%5Betc%5D%5Babc%5D=def&foo%5Betc%5D%5Bghi%5D=jkl&foo%5Betc%5D%5Bmno%5D%5Bopq%5D=rst&foo%5Betc%5D%5Bmno%5D%5Buvw%5D=xyz&foo%5Betc%5D%5Bmno%5D%5Baab%5D%5B%5D=bbc&foo%5Betc%5D%5Bmno%5D%5Baab%5D%5B%5D=ccd&foo%5Betc%5D%5Bmno%5D%5Baab%5D%5B%5D=eef', + { + 'filters': { + r'$or': [ + { + 'date': { + r'$eq': '2020-01-01', + } + }, + { + 'date': { + r'$eq': '2020-01-02', + } + } + ], + 'author': { + 'name': { + r'$eq': 'John doe', + }, + } + } + }: 'filters%5B%24or%5D%5B%5D%5Bdate%5D%5B%24eq%5D=2020-01-01&filters%5B%24or%5D%5B%5D%5Bdate%5D%5B%24eq%5D=2020-01-02&filters%5Bauthor%5D%5Bname%5D%5B%24eq%5D=John%20doe', }.forEach( - (map, query) => test( - '$map -> $query', - () => expect( - mapToQuery(map, useBrackets: true, includeNullQueryVars: true), - query, - ), - ), + (map, query) { + test( + '$map -> $query', + () => expect( + mapToQuery( + map, + useBrackets: true, + includeNullQueryVars: true, + ), + query, + reason: 'legacy brackets', + ), + ); + + test( + '$map -> $query', + () => expect( + mapToQuery( + map, + listFormat: ListFormat.brackets, + includeNullQueryVars: true, + ), + query, + ), + ); + }, ); }); + group('mapToQuery maps with indices', () { + , String>{ + { + 'foo': {'bar': 'baz'}, + }: 'foo%5Bbar%5D=baz', + { + 'foo': {'bar': ''}, + }: '', + { + 'foo': {'bar': null}, + }: '', + { + 'foo': {'bar': ' '}, + }: 'foo%5Bbar%5D=%20', + { + 'foo': {'bar': '\t'}, + }: 'foo%5Bbar%5D=%09', + { + 'foo': {'bar': 'baz', 'etc': 'xyz', 'space': ' ', 'tab': '\t'}, + }: 'foo%5Bbar%5D=baz&foo%5Betc%5D=xyz&foo%5Bspace%5D=%20&foo%5Btab%5D=%09', + { + 'foo': { + 'bar': 'baz', + 'int': 123, + 'double': 456.789, + 'zero': 0, + 'negInt': -123, + 'negDouble': -456.789, + 'emptyString': '', + 'nullValue': null, + 'space': ' ', + 'tab': '\t', + 'list': ['a', 123, false], + }, + }: 'foo%5Bbar%5D=baz&foo%5Bint%5D=123&foo%5Bdouble%5D=456.789&foo%5Bzero%5D=0&foo%5BnegInt%5D=-123&foo%5BnegDouble%5D=-456.789&foo%5Bspace%5D=%20&foo%5Btab%5D=%09&foo%5Blist%5D%5B0%5D=a&foo%5Blist%5D%5B1%5D=123&foo%5Blist%5D%5B2%5D=false', + { + 'foo': {'bar': 'baz'}, + 'etc': 'xyz', + }: 'foo%5Bbar%5D=baz&etc=xyz', + { + 'foo': { + 'bar': 'baz', + 'zap': 'abc', + 'etc': { + 'abc': 'def', + 'ghi': 'jkl', + 'mno': { + 'opq': 'rst', + 'uvw': 'xyz', + 'aab': [ + 'bbc', + 'ccd', + 'eef', + ], + }, + }, + }, + }: 'foo%5Bbar%5D=baz&foo%5Bzap%5D=abc&foo%5Betc%5D%5Babc%5D=def&foo%5Betc%5D%5Bghi%5D=jkl&foo%5Betc%5D%5Bmno%5D%5Bopq%5D=rst&foo%5Betc%5D%5Bmno%5D%5Buvw%5D=xyz&foo%5Betc%5D%5Bmno%5D%5Baab%5D%5B0%5D=bbc&foo%5Betc%5D%5Bmno%5D%5Baab%5D%5B1%5D=ccd&foo%5Betc%5D%5Bmno%5D%5Baab%5D%5B2%5D=eef', + { + 'filters': { + r'$or': [ + { + 'date': { + r'$eq': '2020-01-01', + } + }, + { + 'date': { + r'$eq': '2020-01-02', + } + } + ], + 'author': { + 'name': { + r'$eq': 'John doe', + }, + } + } + }: 'filters%5B%24or%5D%5B0%5D%5Bdate%5D%5B%24eq%5D=2020-01-01&filters%5B%24or%5D%5B1%5D%5Bdate%5D%5B%24eq%5D=2020-01-02&filters%5Bauthor%5D%5Bname%5D%5B%24eq%5D=John%20doe', + }.forEach( + (map, query) => test( + '$map -> $query', + () => expect( + mapToQuery(map, listFormat: ListFormat.indices), + query, + ), + ), + ); + }); + + group('mapToQuery maps with indices with includeNullQueryVars', () { + , String>{ + { + 'foo': {'bar': 'baz'}, + }: 'foo%5Bbar%5D=baz', + { + 'foo': {'bar': ''}, + }: 'foo%5Bbar%5D=', + { + 'foo': {'bar': null}, + }: 'foo%5Bbar%5D=', + { + 'foo': {'bar': ' '}, + }: 'foo%5Bbar%5D=%20', + { + 'foo': {'bar': '\t'}, + }: 'foo%5Bbar%5D=%09', + { + 'foo': {'bar': 'baz', 'etc': 'xyz', 'space': ' ', 'tab': '\t'}, + }: 'foo%5Bbar%5D=baz&foo%5Betc%5D=xyz&foo%5Bspace%5D=%20&foo%5Btab%5D=%09', + { + 'foo': { + 'bar': 'baz', + 'int': 123, + 'double': 456.789, + 'zero': 0, + 'negInt': -123, + 'negDouble': -456.789, + 'emptyString': '', + 'nullValue': null, + 'space': ' ', + 'tab': '\t', + 'list': ['a', 123, false], + }, + }: 'foo%5Bbar%5D=baz&foo%5Bint%5D=123&foo%5Bdouble%5D=456.789&foo%5Bzero%5D=0&foo%5BnegInt%5D=-123&foo%5BnegDouble%5D=-456.789&foo%5BemptyString%5D=&foo%5BnullValue%5D=&foo%5Bspace%5D=%20&foo%5Btab%5D=%09&foo%5Blist%5D%5B0%5D=a&foo%5Blist%5D%5B1%5D=123&foo%5Blist%5D%5B2%5D=false', + { + 'foo': {'bar': 'baz'}, + 'etc': 'xyz', + }: 'foo%5Bbar%5D=baz&etc=xyz', + { + 'foo': { + 'bar': 'baz', + 'zap': 'abc', + 'etc': { + 'abc': 'def', + 'ghi': 'jkl', + 'mno': { + 'opq': 'rst', + 'uvw': 'xyz', + 'aab': [ + 'bbc', + 'ccd', + 'eef', + ], + }, + }, + }, + }: 'foo%5Bbar%5D=baz&foo%5Bzap%5D=abc&foo%5Betc%5D%5Babc%5D=def&foo%5Betc%5D%5Bghi%5D=jkl&foo%5Betc%5D%5Bmno%5D%5Bopq%5D=rst&foo%5Betc%5D%5Bmno%5D%5Buvw%5D=xyz&foo%5Betc%5D%5Bmno%5D%5Baab%5D%5B0%5D=bbc&foo%5Betc%5D%5Bmno%5D%5Baab%5D%5B1%5D=ccd&foo%5Betc%5D%5Bmno%5D%5Baab%5D%5B2%5D=eef', + { + 'filters': { + r'$or': [ + { + 'date': { + r'$eq': '2020-01-01', + } + }, + { + 'date': { + r'$eq': '2020-01-02', + } + } + ], + 'author': { + 'name': { + r'$eq': 'John doe', + }, + } + } + }: 'filters%5B%24or%5D%5B0%5D%5Bdate%5D%5B%24eq%5D=2020-01-01&filters%5B%24or%5D%5B1%5D%5Bdate%5D%5B%24eq%5D=2020-01-02&filters%5Bauthor%5D%5Bname%5D%5B%24eq%5D=John%20doe', + }.forEach( + (map, query) => test( + '$map -> $query', + () => expect( + mapToQuery( + map, + listFormat: ListFormat.indices, + includeNullQueryVars: true, + ), + query, + ), + ), + ); + }); + + group('mapToQuery maps with comma', () { + , String>{ + { + 'foo': {'bar': 'baz'}, + }: 'foo%5Bbar%5D=baz', + { + 'foo': {'bar': ''}, + }: '', + { + 'foo': {'bar': null}, + }: '', + { + 'foo': {'bar': ' '}, + }: 'foo%5Bbar%5D=%20', + { + 'foo': {'bar': '\t'}, + }: 'foo%5Bbar%5D=%09', + { + 'foo': {'bar': 'baz', 'etc': 'xyz', 'space': ' ', 'tab': '\t'}, + }: 'foo%5Bbar%5D=baz&foo%5Betc%5D=xyz&foo%5Bspace%5D=%20&foo%5Btab%5D=%09', + { + 'foo': { + 'bar': 'baz', + 'int': 123, + 'double': 456.789, + 'zero': 0, + 'negInt': -123, + 'negDouble': -456.789, + 'emptyString': '', + 'nullValue': null, + 'space': ' ', + 'tab': '\t', + 'list': ['a', 123, false], + }, + }: 'foo%5Bbar%5D=baz&foo%5Bint%5D=123&foo%5Bdouble%5D=456.789&foo%5Bzero%5D=0&foo%5BnegInt%5D=-123&foo%5BnegDouble%5D=-456.789&foo%5Bspace%5D=%20&foo%5Btab%5D=%09&foo%5Blist%5D=a%2C123%2Cfalse', + { + 'foo': {'bar': 'baz'}, + 'etc': 'xyz', + }: 'foo%5Bbar%5D=baz&etc=xyz', + { + 'foo': { + 'bar': 'baz', + 'zap': 'abc', + 'etc': { + 'abc': 'def', + 'ghi': 'jkl', + 'mno': { + 'opq': 'rst', + 'uvw': 'xyz', + 'aab': [ + 'bbc', + 'ccd', + 'eef', + ], + }, + }, + }, + }: 'foo%5Bbar%5D=baz&foo%5Bzap%5D=abc&foo%5Betc%5D%5Babc%5D=def&foo%5Betc%5D%5Bghi%5D=jkl&foo%5Betc%5D%5Bmno%5D%5Bopq%5D=rst&foo%5Betc%5D%5Bmno%5D%5Buvw%5D=xyz&foo%5Betc%5D%5Bmno%5D%5Baab%5D=bbc%2Cccd%2Ceef', + { + 'filters': { + r'$or': [ + { + 'date': { + r'$eq': '2020-01-01', + } + }, + { + 'date': { + r'$eq': '2020-01-02', + } + } + ], + 'author': { + 'name': { + r'$eq': 'John doe', + }, + } + } + }: 'filters%5B%24or%5D=%7Bdate%3A%20%7B%24eq%3A%202020-01-01%7D%7D%2C%7Bdate%3A%20%7B%24eq%3A%202020-01-02%7D%7D&filters%5Bauthor%5D%5Bname%5D%5B%24eq%5D=John%20doe', + }.forEach( + (map, query) => test( + '$map -> $query', + () => expect( + mapToQuery(map, listFormat: ListFormat.comma), + query, + ), + ), + ); + }); + + group('mapToQuery maps with comma with includeNullQueryVars', () { + , String>{ + { + 'foo': {'bar': 'baz'}, + }: 'foo%5Bbar%5D=baz', + { + 'foo': {'bar': ''}, + }: 'foo%5Bbar%5D=', + { + 'foo': {'bar': null}, + }: 'foo%5Bbar%5D=', + { + 'foo': {'bar': ' '}, + }: 'foo%5Bbar%5D=%20', + { + 'foo': {'bar': '\t'}, + }: 'foo%5Bbar%5D=%09', + { + 'foo': {'bar': 'baz', 'etc': 'xyz', 'space': ' ', 'tab': '\t'}, + }: 'foo%5Bbar%5D=baz&foo%5Betc%5D=xyz&foo%5Bspace%5D=%20&foo%5Btab%5D=%09', + { + 'foo': { + 'bar': 'baz', + 'int': 123, + 'double': 456.789, + 'zero': 0, + 'negInt': -123, + 'negDouble': -456.789, + 'emptyString': '', + 'nullValue': null, + 'space': ' ', + 'tab': '\t', + 'list': ['a', 123, false], + }, + }: 'foo%5Bbar%5D=baz&foo%5Bint%5D=123&foo%5Bdouble%5D=456.789&foo%5Bzero%5D=0&foo%5BnegInt%5D=-123&foo%5BnegDouble%5D=-456.789&foo%5BemptyString%5D=&foo%5BnullValue%5D=&foo%5Bspace%5D=%20&foo%5Btab%5D=%09&foo%5Blist%5D=a%2C123%2Cfalse', + { + 'foo': {'bar': 'baz'}, + 'etc': 'xyz', + }: 'foo%5Bbar%5D=baz&etc=xyz', + { + 'foo': { + 'bar': 'baz', + 'zap': 'abc', + 'etc': { + 'abc': 'def', + 'ghi': 'jkl', + 'mno': { + 'opq': 'rst', + 'uvw': 'xyz', + 'aab': [ + 'bbc', + 'ccd', + 'eef', + ], + }, + }, + }, + }: 'foo%5Bbar%5D=baz&foo%5Bzap%5D=abc&foo%5Betc%5D%5Babc%5D=def&foo%5Betc%5D%5Bghi%5D=jkl&foo%5Betc%5D%5Bmno%5D%5Bopq%5D=rst&foo%5Betc%5D%5Bmno%5D%5Buvw%5D=xyz&foo%5Betc%5D%5Bmno%5D%5Baab%5D=bbc%2Cccd%2Ceef', + { + 'filters': { + r'$or': [ + { + 'date': { + r'$eq': '2020-01-01', + } + }, + { + 'date': { + r'$eq': '2020-01-02', + } + } + ], + 'author': { + 'name': { + r'$eq': 'John doe', + }, + } + } + }: 'filters%5B%24or%5D=%7Bdate%3A%20%7B%24eq%3A%202020-01-01%7D%7D%2C%7Bdate%3A%20%7B%24eq%3A%202020-01-02%7D%7D&filters%5Bauthor%5D%5Bname%5D%5B%24eq%5D=John%20doe', + }.forEach( + (map, query) => test( + '$map -> $query', + () => expect( + mapToQuery( + map, + listFormat: ListFormat.comma, + includeNullQueryVars: true, + ), + query, + ), + ), + ); + }); + + group('mapToQuery maps with indices and nested lists', () { + group( + 'mapToQuery maps with repeat (default) and nested lists', + () { + , String>{ + { + 'filters': { + r'$or': [ + { + 'date': { + r'$eq': '2020-01-01', + } + }, + null, + { + 'date': { + r'$eq': '2020-01-02', + } + } + ], + 'author': { + 'name': { + r'$eq': 'Kai doe', + }, + } + } + }: r'filters%2E$or%2Edate.$eq=2020-01-01&filters%2E$or%2Edate.$eq=2020-01-02&filters%2Eauthor%2Ename.$eq=Kai%20doe', + { + 'filters': { + 'id': { + r'$in': [3, 6, 8], + }, + } + }: r'filters%2Eid%2E$in=3&filters%2Eid%2E$in=6&filters%2Eid%2E$in=8' + }.forEach( + (map, query) { + test( + '$map -> $query', + () => expect( + mapToQuery( + map, + ), + query, + reason: 'legacy default', + ), + ); + + test( + '$map -> $query', + () => expect( + mapToQuery( + map, + listFormat: ListFormat.repeat, + ), + query, + ), + ); + }, + ); + }, + ); + + group( + 'mapToQuery maps with brackets and nested lists', + () { + , String>{ + { + 'filters': { + r'$or': [ + { + 'date': { + r'$eq': '2020-01-01', + } + }, + { + 'date': { + r'$eq': '2020-01-02', + } + } + ], + 'author': { + 'name': { + r'$eq': 'Kai doe', + }, + } + } + }: 'filters%5B%24or%5D%5B%5D%5Bdate%5D%5B%24eq%5D=2020-01-01&filters%5B%24or%5D%5B%5D%5Bdate%5D%5B%24eq%5D=2020-01-02&filters%5Bauthor%5D%5Bname%5D%5B%24eq%5D=Kai%20doe', + { + 'filters': { + 'id': { + r'$in': [3, 6, 8], + }, + } + }: 'filters%5Bid%5D%5B%24in%5D%5B%5D=3&filters%5Bid%5D%5B%24in%5D%5B%5D=6&filters%5Bid%5D%5B%24in%5D%5B%5D=8' + }.forEach( + (map, query) { + test( + '$map -> $query', + () => expect( + mapToQuery( + map, + useBrackets: true, + ), + query, + reason: 'legacy brackets', + ), + ); + + test( + '$map -> $query', + () => expect( + mapToQuery( + map, + listFormat: ListFormat.brackets, + ), + query, + ), + ); + }, + ); + }, + ); + + group( + 'mapToQuery maps with comma and nested lists', + () { + , String>{ + { + 'filters': { + r'$or': [ + { + 'date': { + r'$eq': '2020-01-01', + } + }, + { + 'date': { + r'$eq': '2020-01-02', + } + } + ], + 'author': { + 'name': { + r'$eq': 'Kai doe', + }, + } + } + }: 'filters%5B%24or%5D=%7Bdate%3A%20%7B%24eq%3A%202020-01-01%7D%7D%2C%7Bdate%3A%20%7B%24eq%3A%202020-01-02%7D%7D&filters%5Bauthor%5D%5Bname%5D%5B%24eq%5D=Kai%20doe', + { + 'filters': { + 'id': { + r'$in': [3, 6, 8], + }, + } + }: 'filters%5Bid%5D%5B%24in%5D=3%2C6%2C8' + }.forEach( + (map, query) => test( + '$map -> $query', + () => expect( + mapToQuery( + map, + listFormat: ListFormat.comma, + includeNullQueryVars: true, + ), + query, + ), + ), + ); + }, + ); + , String>{ + { + 'filters': { + r'$or': [ + { + 'date': { + r'$eq': '2020-01-01', + } + }, + { + 'date': { + r'$eq': '2020-01-02', + } + } + ], + 'author': { + 'name': { + r'$eq': 'Kai doe', + }, + } + } + }: 'filters%5B%24or%5D%5B%5D%5Bdate%5D%5B%24eq%5D=2020-01-01&filters%5B%24or%5D%5B%5D%5Bdate%5D%5B%24eq%5D=2020-01-02&filters%5Bauthor%5D%5Bname%5D%5B%24eq%5D=Kai%20doe', + { + 'filters': { + 'id': { + r'$in': [3, 6, 8], + }, + } + }: 'filters%5Bid%5D%5B%24in%5D%5B%5D=3&filters%5Bid%5D%5B%24in%5D%5B%5D=6&filters%5Bid%5D%5B%24in%5D%5B%5D=8' + }.forEach( + (map, query) { + test( + '$map -> $query', + () => expect( + mapToQuery(map, useBrackets: true), + query, + reason: 'legacy brackets', + ), + ); + + test( + '$map -> $query', + () => expect( + mapToQuery(map, listFormat: ListFormat.brackets), + query, + ), + ); + }, + ); + }); + + group( + 'mapToQuery maps with repeat (default) with includeNullQueryVars and nested lists', + () { + , String>{ + { + 'filters': { + r'$or': [ + { + 'date': { + r'$eq': '2020-01-01', + } + }, + null, + { + 'date': { + r'$eq': '2020-01-02', + } + } + ], + 'author': { + 'name': { + r'$eq': 'Kai doe', + }, + } + } + }: r'filters%2E$or%2Edate.$eq=2020-01-01&filters%2E$or=&filters%2E$or%2Edate.$eq=2020-01-02&filters%2Eauthor%2Ename.$eq=Kai%20doe', + { + 'filters': { + 'id': { + r'$in': [3, null, 8], + }, + } + }: r'filters%2Eid%2E$in=3&filters%2Eid%2E$in=&filters%2Eid%2E$in=8' + }.forEach( + (map, query) { + test( + '$map -> $query', + () => expect( + mapToQuery( + map, + includeNullQueryVars: true, + ), + query, + reason: 'legacy default', + ), + ); + + test( + '$map -> $query', + () => expect( + mapToQuery( + map, + listFormat: ListFormat.repeat, + includeNullQueryVars: true, + ), + query, + ), + ); + }, + ); + }, + ); + + group( + 'mapToQuery maps with brackets with includeNullQueryVars and nested lists', + () { + , String>{ + { + 'filters': { + r'$or': [ + { + 'date': { + r'$eq': '2020-01-01', + } + }, + null, + { + 'date': { + r'$eq': '2020-01-02', + } + } + ], + 'author': { + 'name': { + r'$eq': 'Kai doe', + }, + } + } + }: 'filters%5B%24or%5D%5B%5D%5Bdate%5D%5B%24eq%5D=2020-01-01&filters%5B%24or%5D%5B%5D=&filters%5B%24or%5D%5B%5D%5Bdate%5D%5B%24eq%5D=2020-01-02&filters%5Bauthor%5D%5Bname%5D%5B%24eq%5D=Kai%20doe', + { + 'filters': { + 'id': { + r'$in': [3, null, 8], + }, + } + }: 'filters%5Bid%5D%5B%24in%5D%5B%5D=3&filters%5Bid%5D%5B%24in%5D%5B%5D=&filters%5Bid%5D%5B%24in%5D%5B%5D=8' + }.forEach( + (map, query) { + test( + '$map -> $query', + () => expect( + mapToQuery( + map, + useBrackets: true, + includeNullQueryVars: true, + ), + query, + reason: 'legacy brackets', + ), + ); + + test( + '$map -> $query', + () => expect( + mapToQuery( + map, + listFormat: ListFormat.brackets, + includeNullQueryVars: true, + ), + query, + ), + ); + }, + ); + }, + ); + + group( + 'mapToQuery maps with indices with includeNullQueryVars and nested lists', + () { + , String>{ + { + 'filters': { + r'$or': [ + { + 'date': { + r'$eq': '2020-01-01', + } + }, + null, + { + 'date': { + r'$eq': '2020-01-02', + } + } + ], + 'author': { + 'name': { + r'$eq': 'Kai doe', + }, + } + } + }: 'filters%5B%24or%5D%5B0%5D%5Bdate%5D%5B%24eq%5D=2020-01-01&filters%5B%24or%5D%5B1%5D=&filters%5B%24or%5D%5B2%5D%5Bdate%5D%5B%24eq%5D=2020-01-02&filters%5Bauthor%5D%5Bname%5D%5B%24eq%5D=Kai%20doe', + { + 'filters': { + 'id': { + r'$in': [3, null, 8], + }, + } + }: 'filters%5Bid%5D%5B%24in%5D%5B0%5D=3&filters%5Bid%5D%5B%24in%5D%5B1%5D=&filters%5Bid%5D%5B%24in%5D%5B2%5D=8' + }.forEach( + (map, query) => test( + '$map -> $query', + () => expect( + mapToQuery( + map, + listFormat: ListFormat.indices, + includeNullQueryVars: true, + ), + query, + ), + ), + ); + }, + ); + + group( + 'mapToQuery maps with comma with includeNullQueryVars and nested lists', + () { + , String>{ + { + 'filters': { + r'$or': [ + { + 'date': { + r'$eq': '2020-01-01', + } + }, + null, + { + 'date': { + r'$eq': '2020-01-02', + } + } + ], + 'author': { + 'name': { + r'$eq': 'Kai doe', + }, + } + } + }: 'filters%5B%24or%5D=%7Bdate%3A%20%7B%24eq%3A%202020-01-01%7D%7D%2C%2C%7Bdate%3A%20%7B%24eq%3A%202020-01-02%7D%7D&filters%5Bauthor%5D%5Bname%5D%5B%24eq%5D=Kai%20doe', + { + 'filters': { + 'id': { + r'$in': [3, null, 8], + }, + } + }: 'filters%5Bid%5D%5B%24in%5D=3%2C%2C8' + }.forEach( + (map, query) => test( + '$map -> $query', + () => expect( + mapToQuery( + map, + listFormat: ListFormat.comma, + includeNullQueryVars: true, + ), + query, + ), + ), + ); + + test('mapToQuery maps with enums', () { + final map = { + 'filters': { + 'name': 'foo', + 'example': ExampleEnum.bar, + } + }; + + expect( + mapToQuery(map), + equals('filters.name=foo&filters.example=bar'), + ); + }); + }, + ); + Request createRequest(Map headers) => Request( 'POST', Uri.parse('foo'), diff --git a/chopper_generator/analysis_options.yaml b/chopper_generator/analysis_options.yaml index 4fd79467..6f56a451 100644 --- a/chopper_generator/analysis_options.yaml +++ b/chopper_generator/analysis_options.yaml @@ -3,6 +3,7 @@ include: package:lints/recommended.yaml analyzer: exclude: - "**.g.dart" + - "**.chopper.dart" - "**.mocks.dart" - "example/**" diff --git a/chopper_generator/lib/src/generator.dart b/chopper_generator/lib/src/generator.dart index c3a99766..7c1b4d80 100644 --- a/chopper_generator/lib/src/generator.dart +++ b/chopper_generator/lib/src/generator.dart @@ -12,6 +12,7 @@ import 'package:chopper_generator/src/vars.dart'; import 'package:code_builder/code_builder.dart'; import 'package:dart_style/dart_style.dart'; import 'package:logging/logging.dart'; +import 'package:qs_dart/qs_dart.dart' show ListFormat; import 'package:source_gen/source_gen.dart'; /// Code generator for [chopper.ChopperApi] annotated classes. @@ -403,9 +404,11 @@ final class ChopperGenerator final bool hasTag = tag.isNotEmpty; - final bool useBrackets = Utils.getUseBrackets(method); + final ListFormat? listFormat = Utils.getListFormat(method); - final bool includeNullQueryVars = Utils.getIncludeNullQueryVars(method); + final bool? useBrackets = Utils.getUseBrackets(method); + + final bool? includeNullQueryVars = Utils.getIncludeNullQueryVars(method); blocks.add( declareFinal(Vars.request.toString(), type: refer('Request')) @@ -417,6 +420,8 @@ final class ChopperGenerator useHeaders: headers != null, hasParts: hasParts, tagRefer: hasTag ? refer(tag.keys.first) : null, + listFormat: listFormat, + // ignore: deprecated_member_use_from_same_package useBrackets: useBrackets, includeNullQueryVars: includeNullQueryVars, ), @@ -703,8 +708,9 @@ final class ChopperGenerator bool hasParts = false, bool useQueries = false, bool useHeaders = false, - bool useBrackets = false, - bool includeNullQueryVars = false, + ListFormat? listFormat, + @Deprecated('Use listFormat instead') bool? useBrackets, + bool? includeNullQueryVars, Reference? tagRefer, }) => refer('Request').newInstance( @@ -722,8 +728,10 @@ final class ChopperGenerator if (useQueries) 'parameters': refer(Vars.parameters.toString()), if (useHeaders) 'headers': refer(Vars.headers.toString()), if (tagRefer != null) 'tag': tagRefer, - if (useBrackets) 'useBrackets': literalBool(useBrackets), - if (includeNullQueryVars) + if (listFormat != null) + 'listFormat': refer('ListFormat').type.property(listFormat.name), + if (useBrackets != null) 'useBrackets': literalBool(useBrackets), + if (includeNullQueryVars != null) 'includeNullQueryVars': literalBool(includeNullQueryVars), }, ); diff --git a/chopper_generator/lib/src/utils.dart b/chopper_generator/lib/src/utils.dart index 315238ed..1e5f9915 100644 --- a/chopper_generator/lib/src/utils.dart +++ b/chopper_generator/lib/src/utils.dart @@ -1,6 +1,8 @@ import 'package:analyzer/dart/element/element.dart'; import 'package:chopper_generator/src/extensions.dart'; import 'package:code_builder/code_builder.dart'; +import 'package:collection/collection.dart'; +import 'package:qs_dart/qs_dart.dart' show ListFormat; import 'package:source_gen/source_gen.dart'; final class Utils { @@ -13,11 +15,23 @@ final class Utils { static String getMethodName(ConstantReader method) => method.read('method').stringValue; - static bool getUseBrackets(ConstantReader method) => - method.peek('useBrackets')?.boolValue ?? false; + static ListFormat? getListFormat(ConstantReader method) { + return ListFormat.values.firstWhereOrNull( + (listFormat) => + listFormat.name == + method + .peek('listFormat') + ?.objectValue + .getField('_name') + ?.toStringValue(), + ); + } - static bool getIncludeNullQueryVars(ConstantReader method) => - method.peek('includeNullQueryVars')?.boolValue ?? false; + static bool? getUseBrackets(ConstantReader method) => + method.peek('useBrackets')?.boolValue; + + static bool? getIncludeNullQueryVars(ConstantReader method) => + method.peek('includeNullQueryVars')?.boolValue; /// All positional required params must support nullability static Parameter buildRequiredPositionalParam(ParameterElement p) => diff --git a/chopper_generator/pubspec.yaml b/chopper_generator/pubspec.yaml index 7eb33501..0df81516 100644 --- a/chopper_generator/pubspec.yaml +++ b/chopper_generator/pubspec.yaml @@ -18,6 +18,8 @@ dependencies: meta: ^1.9.1 source_gen: ^1.4.0 yaml: ^3.1.2 + qs_dart: ^1.0.3 + collection: ^1.18.0 dev_dependencies: build_runner: ^2.4.6 diff --git a/chopper_generator/test/test_service.chopper.dart b/chopper_generator/test/test_service.chopper.dart index 773bddf3..ae0052ea 100644 --- a/chopper_generator/test/test_service.chopper.dart +++ b/chopper_generator/test/test_service.chopper.dart @@ -675,6 +675,21 @@ final class _$HttpTestService extends HttpTestService { return client.send($request); } + @override + Future> getUsingListQueryParamWithBracketsLegacy( + List value) { + final Uri $url = Uri.parse('/test/list_query_param_with_brackets_legacy'); + final Map $params = {'value': value}; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + useBrackets: true, + ); + return client.send($request); + } + @override Future> getUsingListQueryParamWithBrackets( List value) { @@ -685,7 +700,51 @@ final class _$HttpTestService extends HttpTestService { $url, client.baseUrl, parameters: $params, - useBrackets: true, + listFormat: ListFormat.brackets, + ); + return client.send($request); + } + + @override + Future> getUsingListQueryParamWithIndices( + List value) { + final Uri $url = Uri.parse('/test/list_query_param_with_indices'); + final Map $params = {'value': value}; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + listFormat: ListFormat.indices, + ); + return client.send($request); + } + + @override + Future> getUsingListQueryParamWithRepeat( + List value) { + final Uri $url = Uri.parse('/test/list_query_param_with_repeat'); + final Map $params = {'value': value}; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + listFormat: ListFormat.repeat, + ); + return client.send($request); + } + + @override + Future> getUsingListQueryParamWithComma(List value) { + final Uri $url = Uri.parse('/test/list_query_param_with_comma'); + final Map $params = {'value': value}; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + listFormat: ListFormat.comma, ); return client.send($request); } @@ -718,6 +777,21 @@ final class _$HttpTestService extends HttpTestService { return client.send($request); } + @override + Future> getUsingMapQueryParamWithBracketsLegacy( + Map value) { + final Uri $url = Uri.parse('/test/map_query_param_with_brackets_legacy'); + final Map $params = {'value': value}; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + useBrackets: true, + ); + return client.send($request); + } + @override Future> getUsingMapQueryParamWithBrackets( Map value) { @@ -728,7 +802,52 @@ final class _$HttpTestService extends HttpTestService { $url, client.baseUrl, parameters: $params, - useBrackets: true, + listFormat: ListFormat.brackets, + ); + return client.send($request); + } + + @override + Future> getUsingMapQueryParamWithIndices( + Map value) { + final Uri $url = Uri.parse('/test/map_query_param_with_indices'); + final Map $params = {'value': value}; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + listFormat: ListFormat.indices, + ); + return client.send($request); + } + + @override + Future> getUsingMapQueryParamWithRepeat( + Map value) { + final Uri $url = Uri.parse('/test/map_query_param_with_repeat'); + final Map $params = {'value': value}; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + listFormat: ListFormat.repeat, + ); + return client.send($request); + } + + @override + Future> getUsingMapQueryParamWithComma( + Map value) { + final Uri $url = Uri.parse('/test/map_query_param_with_comma'); + final Map $params = {'value': value}; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + listFormat: ListFormat.comma, ); return client.send($request); } diff --git a/chopper_generator/test/test_service.dart b/chopper_generator/test/test_service.dart index 7f818dae..9ee8b877 100644 --- a/chopper_generator/test/test_service.dart +++ b/chopper_generator/test/test_service.dart @@ -193,11 +193,31 @@ abstract class HttpTestService extends ChopperService { @Query('value') List value, ); - @Get(path: '/list_query_param_with_brackets', useBrackets: true) + @Get(path: '/list_query_param_with_brackets_legacy', useBrackets: true) + Future> getUsingListQueryParamWithBracketsLegacy( + @Query('value') List value, + ); + + @Get(path: '/list_query_param_with_brackets', listFormat: ListFormat.brackets) Future> getUsingListQueryParamWithBrackets( @Query('value') List value, ); + @Get(path: '/list_query_param_with_indices', listFormat: ListFormat.indices) + Future> getUsingListQueryParamWithIndices( + @Query('value') List value, + ); + + @Get(path: '/list_query_param_with_repeat', listFormat: ListFormat.repeat) + Future> getUsingListQueryParamWithRepeat( + @Query('value') List value, + ); + + @Get(path: '/list_query_param_with_comma', listFormat: ListFormat.comma) + Future> getUsingListQueryParamWithComma( + @Query('value') List value, + ); + @Get(path: '/map_query_param') Future> getUsingMapQueryParam( @Query('value') Map value, @@ -211,11 +231,31 @@ abstract class HttpTestService extends ChopperService { @Query('value') Map value, ); - @Get(path: '/map_query_param_with_brackets', useBrackets: true) + @Get(path: '/map_query_param_with_brackets_legacy', useBrackets: true) + Future> getUsingMapQueryParamWithBracketsLegacy( + @Query('value') Map value, + ); + + @Get(path: '/map_query_param_with_brackets', listFormat: ListFormat.brackets) Future> getUsingMapQueryParamWithBrackets( @Query('value') Map value, ); + @Get(path: '/map_query_param_with_indices', listFormat: ListFormat.indices) + Future> getUsingMapQueryParamWithIndices( + @Query('value') Map value, + ); + + @Get(path: '/map_query_param_with_repeat', listFormat: ListFormat.repeat) + Future> getUsingMapQueryParamWithRepeat( + @Query('value') Map value, + ); + + @Get(path: '/map_query_param_with_comma', listFormat: ListFormat.comma) + Future> getUsingMapQueryParamWithComma( + @Query('value') Map value, + ); + @Get(path: 'headers') Future> getHeaders({ @Header('x-string') required String stringHeader, diff --git a/chopper_generator/test/test_service_variable.chopper.dart b/chopper_generator/test/test_service_variable.chopper.dart index 9c69ffa0..c60e766e 100644 --- a/chopper_generator/test/test_service_variable.chopper.dart +++ b/chopper_generator/test/test_service_variable.chopper.dart @@ -581,6 +581,22 @@ final class _$HttpTestServiceVariable extends HttpTestServiceVariable { return client.send($request); } + @override + Future> getUsingListQueryParamWithBracketsLegacy( + List value) { + final Uri $url = + Uri.parse('${service}/list_query_param_with_brackets_legacy'); + final Map $params = {'value': value}; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + useBrackets: true, + ); + return client.send($request); + } + @override Future> getUsingListQueryParamWithBrackets( List value) { @@ -591,7 +607,51 @@ final class _$HttpTestServiceVariable extends HttpTestServiceVariable { $url, client.baseUrl, parameters: $params, - useBrackets: true, + listFormat: ListFormat.brackets, + ); + return client.send($request); + } + + @override + Future> getUsingListQueryParamWithIndices( + List value) { + final Uri $url = Uri.parse('${service}/list_query_param_with_indices'); + final Map $params = {'value': value}; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + listFormat: ListFormat.indices, + ); + return client.send($request); + } + + @override + Future> getUsingListQueryParamWithRepeat( + List value) { + final Uri $url = Uri.parse('${service}/list_query_param_with_repeat'); + final Map $params = {'value': value}; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + listFormat: ListFormat.repeat, + ); + return client.send($request); + } + + @override + Future> getUsingListQueryParamWithComma(List value) { + final Uri $url = Uri.parse('${service}/list_query_param_with_comma'); + final Map $params = {'value': value}; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + listFormat: ListFormat.comma, ); return client.send($request); } @@ -625,6 +685,22 @@ final class _$HttpTestServiceVariable extends HttpTestServiceVariable { return client.send($request); } + @override + Future> getUsingMapQueryParamWithBracketsLegacy( + Map value) { + final Uri $url = + Uri.parse('${service}/map_query_param_with_brackets_legacy'); + final Map $params = {'value': value}; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + useBrackets: true, + ); + return client.send($request); + } + @override Future> getUsingMapQueryParamWithBrackets( Map value) { @@ -635,7 +711,52 @@ final class _$HttpTestServiceVariable extends HttpTestServiceVariable { $url, client.baseUrl, parameters: $params, - useBrackets: true, + listFormat: ListFormat.brackets, + ); + return client.send($request); + } + + @override + Future> getUsingMapQueryParamWithIndices( + Map value) { + final Uri $url = Uri.parse('${service}/map_query_param_with_indices'); + final Map $params = {'value': value}; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + listFormat: ListFormat.indices, + ); + return client.send($request); + } + + @override + Future> getUsingMapQueryParamWithRepeat( + Map value) { + final Uri $url = Uri.parse('${service}/map_query_param_with_repeat'); + final Map $params = {'value': value}; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + listFormat: ListFormat.repeat, + ); + return client.send($request); + } + + @override + Future> getUsingMapQueryParamWithComma( + Map value) { + final Uri $url = Uri.parse('${service}/map_query_param_with_comma'); + final Map $params = {'value': value}; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + listFormat: ListFormat.comma, ); return client.send($request); } diff --git a/chopper_generator/test/test_service_variable.dart b/chopper_generator/test/test_service_variable.dart index 251b48cb..c878ada8 100644 --- a/chopper_generator/test/test_service_variable.dart +++ b/chopper_generator/test/test_service_variable.dart @@ -168,11 +168,31 @@ abstract class HttpTestServiceVariable extends ChopperService { @Query('value') List value, ); - @Get(path: '/list_query_param_with_brackets', useBrackets: true) + @Get(path: '/list_query_param_with_brackets_legacy', useBrackets: true) + Future> getUsingListQueryParamWithBracketsLegacy( + @Query('value') List value, + ); + + @Get(path: '/list_query_param_with_brackets', listFormat: ListFormat.brackets) Future> getUsingListQueryParamWithBrackets( @Query('value') List value, ); + @Get(path: '/list_query_param_with_indices', listFormat: ListFormat.indices) + Future> getUsingListQueryParamWithIndices( + @Query('value') List value, + ); + + @Get(path: '/list_query_param_with_repeat', listFormat: ListFormat.repeat) + Future> getUsingListQueryParamWithRepeat( + @Query('value') List value, + ); + + @Get(path: '/list_query_param_with_comma', listFormat: ListFormat.comma) + Future> getUsingListQueryParamWithComma( + @Query('value') List value, + ); + @Get(path: '/map_query_param') Future> getUsingMapQueryParam( @Query('value') Map value, @@ -186,10 +206,30 @@ abstract class HttpTestServiceVariable extends ChopperService { @Query('value') Map value, ); - @Get(path: '/map_query_param_with_brackets', useBrackets: true) + @Get(path: '/map_query_param_with_brackets_legacy', useBrackets: true) + Future> getUsingMapQueryParamWithBracketsLegacy( + @Query('value') Map value, + ); + + @Get(path: '/map_query_param_with_brackets', listFormat: ListFormat.brackets) Future> getUsingMapQueryParamWithBrackets( @Query('value') Map value, ); + + @Get(path: '/map_query_param_with_indices', listFormat: ListFormat.indices) + Future> getUsingMapQueryParamWithIndices( + @Query('value') Map value, + ); + + @Get(path: '/map_query_param_with_repeat', listFormat: ListFormat.repeat) + Future> getUsingMapQueryParamWithRepeat( + @Query('value') Map value, + ); + + @Get(path: '/map_query_param_with_comma', listFormat: ListFormat.comma) + Future> getUsingMapQueryParamWithComma( + @Query('value') Map value, + ); } Request customConvertRequest(Request req) { diff --git a/chopper_generator/test/test_without_response_service.chopper.dart b/chopper_generator/test/test_without_response_service.chopper.dart index c7b8462a..2bc8fcbd 100644 --- a/chopper_generator/test/test_without_response_service.chopper.dart +++ b/chopper_generator/test/test_without_response_service.chopper.dart @@ -616,6 +616,22 @@ final class _$HttpTestService extends HttpTestService { return $response.bodyOrThrow; } + @override + Future getUsingListQueryParamWithBracketsLegacy( + List value) async { + final Uri $url = Uri.parse('/test/list_query_param_with_brackets_legacy'); + final Map $params = {'value': value}; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + useBrackets: true, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + @override Future getUsingListQueryParamWithBrackets(List value) async { final Uri $url = Uri.parse('/test/list_query_param_with_brackets'); @@ -625,7 +641,52 @@ final class _$HttpTestService extends HttpTestService { $url, client.baseUrl, parameters: $params, - useBrackets: true, + listFormat: ListFormat.brackets, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + + @override + Future getUsingListQueryParamWithIndices(List value) async { + final Uri $url = Uri.parse('/test/list_query_param_with_indices'); + final Map $params = {'value': value}; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + listFormat: ListFormat.indices, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + + @override + Future getUsingListQueryParamWithRepeat(List value) async { + final Uri $url = Uri.parse('/test/list_query_param_with_repeat'); + final Map $params = {'value': value}; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + listFormat: ListFormat.repeat, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + + @override + Future getUsingListQueryParamWithComma(List value) async { + final Uri $url = Uri.parse('/test/list_query_param_with_comma'); + final Map $params = {'value': value}; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + listFormat: ListFormat.comma, ); final Response $response = await client.send($request); return $response.bodyOrThrow; @@ -661,6 +722,22 @@ final class _$HttpTestService extends HttpTestService { return $response.bodyOrThrow; } + @override + Future getUsingMapQueryParamWithBracketsLegacy( + Map value) async { + final Uri $url = Uri.parse('/test/map_query_param_with_brackets_legacy'); + final Map $params = {'value': value}; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + useBrackets: true, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + @override Future getUsingMapQueryParamWithBrackets( Map value) async { @@ -671,7 +748,55 @@ final class _$HttpTestService extends HttpTestService { $url, client.baseUrl, parameters: $params, - useBrackets: true, + listFormat: ListFormat.brackets, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + + @override + Future getUsingMapQueryParamWithIndices( + Map value) async { + final Uri $url = Uri.parse('/test/map_query_param_with_indices'); + final Map $params = {'value': value}; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + listFormat: ListFormat.indices, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + + @override + Future getUsingMapQueryParamWithRepeat( + Map value) async { + final Uri $url = Uri.parse('/test/map_query_param_with_repeat'); + final Map $params = {'value': value}; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + listFormat: ListFormat.repeat, + ); + final Response $response = await client.send($request); + return $response.bodyOrThrow; + } + + @override + Future getUsingMapQueryParamWithComma( + Map value) async { + final Uri $url = Uri.parse('/test/map_query_param_with_comma'); + final Map $params = {'value': value}; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + listFormat: ListFormat.comma, ); final Response $response = await client.send($request); return $response.bodyOrThrow; diff --git a/chopper_generator/test/test_without_response_service.dart b/chopper_generator/test/test_without_response_service.dart index d8a95417..d29c0eb9 100644 --- a/chopper_generator/test/test_without_response_service.dart +++ b/chopper_generator/test/test_without_response_service.dart @@ -166,11 +166,31 @@ abstract class HttpTestService extends ChopperService { @Query('value') List value, ); - @Get(path: '/list_query_param_with_brackets', useBrackets: true) + @Get(path: '/list_query_param_with_brackets_legacy', useBrackets: true) + Future getUsingListQueryParamWithBracketsLegacy( + @Query('value') List value, + ); + + @Get(path: '/list_query_param_with_brackets', listFormat: ListFormat.brackets) Future getUsingListQueryParamWithBrackets( @Query('value') List value, ); + @Get(path: '/list_query_param_with_indices', listFormat: ListFormat.indices) + Future getUsingListQueryParamWithIndices( + @Query('value') List value, + ); + + @Get(path: '/list_query_param_with_repeat', listFormat: ListFormat.repeat) + Future getUsingListQueryParamWithRepeat( + @Query('value') List value, + ); + + @Get(path: '/list_query_param_with_comma', listFormat: ListFormat.comma) + Future getUsingListQueryParamWithComma( + @Query('value') List value, + ); + @Get(path: '/map_query_param') Future getUsingMapQueryParam( @Query('value') Map value, @@ -184,11 +204,31 @@ abstract class HttpTestService extends ChopperService { @Query('value') Map value, ); - @Get(path: '/map_query_param_with_brackets', useBrackets: true) + @Get(path: '/map_query_param_with_brackets_legacy', useBrackets: true) + Future getUsingMapQueryParamWithBracketsLegacy( + @Query('value') Map value, + ); + + @Get(path: '/map_query_param_with_brackets', listFormat: ListFormat.brackets) Future getUsingMapQueryParamWithBrackets( @Query('value') Map value, ); + @Get(path: '/map_query_param_with_indices', listFormat: ListFormat.indices) + Future getUsingMapQueryParamWithIndices( + @Query('value') Map value, + ); + + @Get(path: '/map_query_param_with_repeat', listFormat: ListFormat.repeat) + Future getUsingMapQueryParamWithRepeat( + @Query('value') Map value, + ); + + @Get(path: '/map_query_param_with_comma', listFormat: ListFormat.comma) + Future getUsingMapQueryParamWithComma( + @Query('value') Map value, + ); + @Get(path: 'headers') Future getHeaders({ @Header('x-string') required String stringHeader, diff --git a/example/analysis_options.yaml b/example/analysis_options.yaml index 1e603f80..8ed20e77 100644 --- a/example/analysis_options.yaml +++ b/example/analysis_options.yaml @@ -3,6 +3,7 @@ include: package:lints/recommended.yaml analyzer: exclude: - "**.g.dart" + - "**.chopper.dart" - "**.mocks.dart" linter: From 46e8c990c7e84bb26c473ed16d1f68d0816f9cea Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Fri, 5 Apr 2024 09:06:29 +0200 Subject: [PATCH 147/171] :arrow_up: update Github actions (#594) --- .github/workflows/dart.yml | 24 ++++++++++++------------ .github/workflows/publish.yml | 4 ++-- .github/workflows/publish_dry_run.yml | 4 ++-- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/.github/workflows/dart.yml b/.github/workflows/dart.yml index d10566f1..e2661e66 100644 --- a/.github/workflows/dart.yml +++ b/.github/workflows/dart.yml @@ -22,7 +22,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Cache Pub hosted dependencies - uses: actions/cache@ab5e6d0c87105b4c9c2047343972218f562e4319 + uses: actions/cache@v4 with: path: "~/.pub-cache/hosted" key: "os:ubuntu-latest;pub-cache-hosted;sdk:stable" @@ -30,12 +30,12 @@ jobs: os:ubuntu-latest;pub-cache-hosted os:ubuntu-latest - name: Setup Dart SDK - uses: dart-lang/setup-dart@d6a63dab3335f427404425de0fbfed4686d93c4f + uses: dart-lang/setup-dart@v1 with: sdk: stable - id: checkout name: Checkout repository - uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 + uses: actions/checkout@v4 - name: mono_repo self validate run: dart pub global activate mono_repo 6.6.1 - name: mono_repo self validate @@ -45,7 +45,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Cache Pub hosted dependencies - uses: actions/cache@ab5e6d0c87105b4c9c2047343972218f562e4319 + uses: actions/cache@v4 with: path: "~/.pub-cache/hosted" key: "os:ubuntu-latest;pub-cache-hosted;sdk:stable;packages:chopper-chopper_built_value-chopper_generator;commands:format-analyze" @@ -55,12 +55,12 @@ jobs: os:ubuntu-latest;pub-cache-hosted os:ubuntu-latest - name: Setup Dart SDK - uses: dart-lang/setup-dart@d6a63dab3335f427404425de0fbfed4686d93c4f + uses: dart-lang/setup-dart@v1 with: sdk: stable - id: checkout name: Checkout repository - uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 + uses: actions/checkout@v4 - id: chopper_pub_upgrade name: chopper; dart pub upgrade run: dart pub upgrade @@ -105,7 +105,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Cache Pub hosted dependencies - uses: actions/cache@ab5e6d0c87105b4c9c2047343972218f562e4319 + uses: actions/cache@v4 with: path: "~/.pub-cache/hosted" key: "os:ubuntu-latest;pub-cache-hosted;sdk:stable;packages:chopper-chopper_built_value-chopper_generator;commands:test_with_coverage" @@ -115,14 +115,14 @@ jobs: os:ubuntu-latest;pub-cache-hosted os:ubuntu-latest - name: Setup Dart SDK - uses: dart-lang/setup-dart@d6a63dab3335f427404425de0fbfed4686d93c4f + uses: dart-lang/setup-dart@v1 with: sdk: stable - name: "Activate package:coverage" run: "dart pub global activate coverage '>=1.5.0'" - id: checkout name: Checkout repository - uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 + uses: actions/checkout@v4 - id: chopper_pub_upgrade name: chopper; dart pub upgrade run: dart pub upgrade @@ -176,7 +176,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Cache Pub hosted dependencies - uses: actions/cache@ab5e6d0c87105b4c9c2047343972218f562e4319 + uses: actions/cache@v4 with: path: "~/.pub-cache/hosted" key: "os:ubuntu-latest;pub-cache-hosted;sdk:stable;packages:chopper-chopper_built_value;commands:test" @@ -186,12 +186,12 @@ jobs: os:ubuntu-latest;pub-cache-hosted os:ubuntu-latest - name: Setup Dart SDK - uses: dart-lang/setup-dart@d6a63dab3335f427404425de0fbfed4686d93c4f + uses: dart-lang/setup-dart@v1 with: sdk: stable - id: checkout name: Checkout repository - uses: actions/checkout@9bb56186c3b09b4f86b1c65136769dd318469633 + uses: actions/checkout@v4 - id: chopper_pub_upgrade name: chopper; dart pub upgrade run: dart pub upgrade diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 58ab9265..478734f5 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -27,7 +27,7 @@ jobs: with: sdk: stable - id: checkout - uses: actions/checkout@v4.1.2 + uses: actions/checkout@v4 with: fetch-depth: 2 - run: git checkout HEAD^ @@ -53,7 +53,7 @@ jobs: with: sdk: stable - id: checkout - uses: actions/checkout@v4.1.2 + uses: actions/checkout@v4 - name: Load this version id: load_this_version working-directory: ${{ matrix.package }} diff --git a/.github/workflows/publish_dry_run.yml b/.github/workflows/publish_dry_run.yml index 6afcb3a2..8a23a866 100644 --- a/.github/workflows/publish_dry_run.yml +++ b/.github/workflows/publish_dry_run.yml @@ -27,7 +27,7 @@ jobs: with: sdk: stable - id: checkout - uses: actions/checkout@v4.1.2 + uses: actions/checkout@v4 with: ref: ${{ github.event.pull_request.base.ref }} - name: Load base version @@ -50,7 +50,7 @@ jobs: with: sdk: stable - id: checkout - uses: actions/checkout@v4.1.2 + uses: actions/checkout@v4 - name: Load this version id: load_this_version working-directory: ${{ matrix.package }} From 1e6a8ab45288b22340fef3bfcae209799e3a7616 Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Fri, 5 Apr 2024 10:22:34 +0200 Subject: [PATCH 148/171] :twisted_rightwards_arrows: post release branch sync (#596) --- chopper/CHANGELOG.md | 4 ++++ chopper/pubspec.yaml | 4 ++-- chopper_generator/CHANGELOG.md | 4 ++++ chopper_generator/pubspec.yaml | 4 ++-- 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/chopper/CHANGELOG.md b/chopper/CHANGELOG.md index c5b30513..59e402fa 100644 --- a/chopper/CHANGELOG.md +++ b/chopper/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 7.4.0 + +- Use [qs_dart](https://pub.dev/packages/qs_dart) for query string encoding for query string encoding in order to support complex query objects ([#592](https://github.com/lejard-h/chopper/pull/592)) + ## 7.3.0 - Add support for `@Tag` annotation ([#586](https://github.com/lejard-h/chopper/pull/586)) diff --git a/chopper/pubspec.yaml b/chopper/pubspec.yaml index dd8f83d2..c64405ab 100644 --- a/chopper/pubspec.yaml +++ b/chopper/pubspec.yaml @@ -1,6 +1,6 @@ name: chopper description: Chopper is an http client generator using source_gen, inspired by Retrofit -version: 7.3.0 +version: 7.4.0 documentation: https://hadrien-lejard.gitbook.io/chopper repository: https://github.com/lejard-h/chopper @@ -26,7 +26,7 @@ dev_dependencies: lints: ">=2.1.1 <4.0.0" test: ^1.24.4 transparent_image: ^2.0.1 - chopper_generator: ^7.2.0 + chopper_generator: ^7.3.0 dependency_overrides: chopper_generator: diff --git a/chopper_generator/CHANGELOG.md b/chopper_generator/CHANGELOG.md index cac791d0..d281a86c 100644 --- a/chopper_generator/CHANGELOG.md +++ b/chopper_generator/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 7.4.0 + +- Use [qs_dart](https://pub.dev/packages/qs_dart) for query string encoding in order to support complex query objects ([#592](https://github.com/lejard-h/chopper/pull/592)) + ## 7.3.0 - Add support for `@Tag` annotation ([#586](https://github.com/lejard-h/chopper/pull/586)) diff --git a/chopper_generator/pubspec.yaml b/chopper_generator/pubspec.yaml index 0df81516..22dca592 100644 --- a/chopper_generator/pubspec.yaml +++ b/chopper_generator/pubspec.yaml @@ -1,6 +1,6 @@ name: chopper_generator description: Chopper is an http client generator using source_gen, inspired by Retrofit -version: 7.3.0 +version: 7.4.0 documentation: https://hadrien-lejard.gitbook.io/chopper repository: https://github.com/lejard-h/chopper @@ -11,7 +11,7 @@ dependencies: analyzer: ">=5.13.0 <7.0.0" build: ^2.4.1 built_collection: ^5.1.1 - chopper: ^7.3.0 + chopper: ^7.4.0 code_builder: ^4.5.0 dart_style: ^2.3.2 logging: ^1.2.0 From 03f67696fd25df5c761ea7b3c1bf8bed7489324c Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Fri, 5 Apr 2024 16:10:42 +0200 Subject: [PATCH 149/171] :arrow_up: update cli_script to 1.0.0 (was 0.3.1) (#597) --- tool/pubspec.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tool/pubspec.yaml b/tool/pubspec.yaml index 5ad9b46a..f0defe63 100644 --- a/tool/pubspec.yaml +++ b/tool/pubspec.yaml @@ -2,11 +2,11 @@ name: compare_versions publish_to: 'none' -version: 1.0.0 +version: 1.0.1 environment: sdk: ">=2.17.0 <4.0.0" dependencies: - cli_script: ^0.3.1 - pub_semver: ^2.1.4 \ No newline at end of file + cli_script: ^1.0.0 + pub_semver: ^2.1.4 From ab5a80f6418783ac07c85064b2bc1467d29e77bd Mon Sep 17 00:00:00 2001 From: Job Guldemeester Date: Fri, 5 Apr 2024 19:25:38 +0200 Subject: [PATCH 150/171] :sparkles: Restructure interceptors (#547) --- chopper/lib/chopper.dart | 6 +- chopper/lib/src/annotations.dart | 2 +- chopper/lib/src/authenticator.dart | 2 +- chopper/lib/src/base.dart | 217 ++--------------- chopper/lib/src/chain/call.dart | 58 +++++ chopper/lib/src/chain/chain.dart | 21 ++ chopper/lib/src/chain/interceptor_chain.dart | 74 ++++++ chopper/lib/src/chopper_exception.dart | 24 ++ chopper/lib/src/chopper_http_exception.dart | 4 + .../src/{interceptor.dart => converters.dart} | 128 +--------- chopper/lib/src/extensions.dart | 4 + .../authenticator_interceptor.dart | 46 ++++ .../src/interceptors/curl_interceptor.dart | 44 ++++ .../src/interceptors/headers_interceptor.dart | 31 +++ .../interceptors/http_call_interceptor.dart | 36 +++ .../http_logging_interceptor.dart | 84 +++---- chopper/lib/src/interceptors/interceptor.dart | 56 +++++ .../interceptors/internal_interceptor.dart | 4 + .../request_converter_interceptor.dart | 47 ++++ .../request_stream_interceptor.dart | 20 ++ .../response_converter_interceptor.dart | 89 +++++++ chopper/test/authenticator_test.dart | 6 +- chopper/test/base_test.dart | 52 +--- .../chain/authenticator_interceptor_test.dart | 136 +++++++++++ .../test/chain/interceptor_chain_test.dart | 229 ++++++++++++++++++ .../request_converter_interceptor_test.dart | 146 +++++++++++ .../response_converter_interceptor_test.dart | 201 +++++++++++++++ chopper/test/client_test.dart | 6 +- chopper/test/converter_test.dart | 5 +- chopper/test/form_test.dart | 3 +- chopper/test/helpers/fake_chain.dart | 20 ++ .../test/http_logging_interceptor_test.dart | 62 ++--- chopper/test/interceptors_test.dart | 129 ++-------- chopper/test/json_test.dart | 3 +- chopper/test/multipart_test.dart | 5 +- example/bin/main_json_serializable.dart | 20 +- ...son_serializable_squadron_worker_pool.dart | 6 +- faq.md | 66 +++-- interceptors.md | 88 +++++-- 39 files changed, 1578 insertions(+), 602 deletions(-) create mode 100644 chopper/lib/src/chain/call.dart create mode 100644 chopper/lib/src/chain/chain.dart create mode 100644 chopper/lib/src/chain/interceptor_chain.dart create mode 100644 chopper/lib/src/chopper_exception.dart rename chopper/lib/src/{interceptor.dart => converters.dart} (61%) create mode 100644 chopper/lib/src/interceptors/authenticator_interceptor.dart create mode 100644 chopper/lib/src/interceptors/curl_interceptor.dart create mode 100644 chopper/lib/src/interceptors/headers_interceptor.dart create mode 100644 chopper/lib/src/interceptors/http_call_interceptor.dart rename chopper/lib/src/{ => interceptors}/http_logging_interceptor.dart (62%) create mode 100644 chopper/lib/src/interceptors/interceptor.dart create mode 100644 chopper/lib/src/interceptors/internal_interceptor.dart create mode 100644 chopper/lib/src/interceptors/request_converter_interceptor.dart create mode 100644 chopper/lib/src/interceptors/request_stream_interceptor.dart create mode 100644 chopper/lib/src/interceptors/response_converter_interceptor.dart create mode 100644 chopper/test/chain/authenticator_interceptor_test.dart create mode 100644 chopper/test/chain/interceptor_chain_test.dart create mode 100644 chopper/test/chain/request_converter_interceptor_test.dart create mode 100644 chopper/test/chain/response_converter_interceptor_test.dart create mode 100644 chopper/test/helpers/fake_chain.dart diff --git a/chopper/lib/chopper.dart b/chopper/lib/chopper.dart index 38fd56aa..a2b503ab 100644 --- a/chopper/lib/chopper.dart +++ b/chopper/lib/chopper.dart @@ -7,11 +7,13 @@ export 'src/annotations.dart'; export 'src/authenticator.dart'; export 'src/base.dart'; export 'src/chopper_http_exception.dart'; +export 'src/chopper_exception.dart'; export 'src/chopper_log_record.dart'; export 'src/constants.dart'; export 'src/extensions.dart'; -export 'src/http_logging_interceptor.dart'; -export 'src/interceptor.dart'; +export 'src/chain/chain.dart'; +export 'src/interceptors/interceptor.dart'; +export 'src/converters.dart'; export 'src/list_format.dart'; export 'src/request.dart'; export 'src/response.dart'; diff --git a/chopper/lib/src/annotations.dart b/chopper/lib/src/annotations.dart index 899b5ecb..1f10db7f 100644 --- a/chopper/lib/src/annotations.dart +++ b/chopper/lib/src/annotations.dart @@ -376,7 +376,7 @@ typedef ConvertRequest = FutureOr Function(Request request); /// A function that should convert the body of a [Response] from the HTTP /// representation to a Dart object. -typedef ConvertResponse = FutureOr Function(Response response); +typedef ConvertResponse = FutureOr> Function(Response response); /// {@template FactoryConverter} /// Defines custom [Converter] methods for a single network API endpoint. diff --git a/chopper/lib/src/authenticator.dart b/chopper/lib/src/authenticator.dart index 1d0ba176..5845ba67 100644 --- a/chopper/lib/src/authenticator.dart +++ b/chopper/lib/src/authenticator.dart @@ -33,7 +33,7 @@ abstract class Authenticator { /// Returns a [Request] that includes credentials to satisfy /// an authentication challenge received in [response], based on /// the incoming [request] or optionally, the [originalRequest] - /// (which was not modified with any previous [RequestInterceptor]s). + /// (which was not modified with any previous [Interceptor]s). /// /// Otherwise, return `null` if the challenge cannot be satisfied. /// diff --git a/chopper/lib/src/base.dart b/chopper/lib/src/base.dart index 066f9cc7..d917f081 100644 --- a/chopper/lib/src/base.dart +++ b/chopper/lib/src/base.dart @@ -2,24 +2,15 @@ import 'dart:async'; import 'package:chopper/src/annotations.dart'; import 'package:chopper/src/authenticator.dart'; +import 'package:chopper/src/chain/call.dart'; import 'package:chopper/src/constants.dart'; -import 'package:chopper/src/interceptor.dart'; +import 'package:chopper/src/converters.dart'; +import 'package:chopper/src/interceptors/interceptor.dart'; import 'package:chopper/src/request.dart'; import 'package:chopper/src/response.dart'; -import 'package:chopper/src/utils.dart'; import 'package:http/http.dart' as http; import 'package:meta/meta.dart'; -@visibleForTesting -const List allowedInterceptorsType = [ - RequestInterceptor, - RequestInterceptorFunc, - ResponseInterceptor, - ResponseInterceptorFunc1, - ResponseInterceptorFunc2, - DynamicResponseInterceptorFunc, -]; - /// ChopperClient is the main class of the Chopper API. /// /// It manages registered services, encodes and decodes data, and intercepts @@ -46,8 +37,7 @@ base class ChopperClient { final ErrorConverter? errorConverter; late final Map _services; - late final List _requestInterceptors; - late final List _responseInterceptors; + late final List interceptors; final StreamController _requestController = StreamController.broadcast(); final StreamController _responseController = @@ -79,11 +69,10 @@ base class ChopperClient { /// ); /// ``` /// - /// [RequestInterceptor]s and [ResponseInterceptor]s can be added to the client + /// [Interceptor]s can be added to the client /// with the [interceptors] parameter. /// - /// See [RequestInterceptor], [ResponseInterceptor], [HttpLoggingInterceptor], - /// [HeadersInterceptor], [CurlInterceptor] + /// See [HttpLoggingInterceptor], [HeadersInterceptor], [CurlInterceptor] /// /// ```dart /// final chopper = ChopperClient( @@ -114,7 +103,7 @@ base class ChopperClient { ChopperClient({ Uri? baseUrl, http.Client? client, - Iterable? interceptors, + this.interceptors = const [], this.authenticator, this.converter, this.errorConverter, @@ -126,36 +115,13 @@ base class ChopperClient { ), baseUrl = baseUrl ?? Uri(), httpClient = client ?? http.Client(), - _clientIsInternal = client == null, - assert( - interceptors?.every(_isAnInterceptor) ?? true, - 'Unsupported type for interceptors, it only support the following types:\n' - ' - ${allowedInterceptorsType.join('\n - ')}', - ), - _requestInterceptors = [ - ...?interceptors?.where(_isRequestInterceptor), - ], - _responseInterceptors = [ - ...?interceptors?.where(_isResponseInterceptor), - ] { + _clientIsInternal = client == null { _services = { for (final ChopperService service in services?.toSet() ?? []) service.definitionType: service..client = this }; } - static bool _isRequestInterceptor(value) => - value is RequestInterceptor || value is RequestInterceptorFunc; - - static bool _isResponseInterceptor(value) => - value is ResponseInterceptor || - value is ResponseInterceptorFunc1 || - value is ResponseInterceptorFunc2 || - value is DynamicResponseInterceptorFunc; - - static bool _isAnInterceptor(value) => - _isResponseInterceptor(value) || _isRequestInterceptor(value); - /// Retrieve any service included in the [ChopperClient] /// /// ```dart @@ -183,100 +149,6 @@ base class ChopperClient { return service as ServiceType; } - Future _encodeRequest(Request request) async => - converter?.convertRequest(request) ?? request; - - static Future> _decodeResponse( - Response response, - Converter withConverter, - ) async => - await withConverter.convertResponse(response); - - Future _interceptRequest(Request req) async { - final body = req.body; - for (final i in _requestInterceptors) { - if (i is RequestInterceptor) { - req = await i.onRequest(req); - } else if (i is RequestInterceptorFunc) { - req = await i(req); - } - } - - assert( - body == req.body, - 'Interceptors should not transform the body of the request' - 'Use Request converter instead', - ); - - return req; - } - - Future> _interceptResponse( - Response res, - ) async { - final body = res.body; - for (final i in _responseInterceptors) { - if (i is ResponseInterceptor) { - res = await i.onResponse(res) as Response; - } else if (i is ResponseInterceptorFunc1) { - res = await i(res); - } else if (i is ResponseInterceptorFunc2) { - res = await i(res); - } else if (i is DynamicResponseInterceptorFunc) { - res = await i(res) as Response; - } - } - - assert( - body == res.body, - 'Interceptors should not transform the body of the response' - 'Use Response converter instead', - ); - - return res; - } - - Future> _handleErrorResponse( - Response response, - ) async { - var error = response.body; - if (errorConverter != null) { - final errorRes = await errorConverter?.convertError( - response, - ); - error = errorRes?.error ?? errorRes?.body; - } - - return Response(response.base, null, error: error); - } - - Future> _handleSuccessResponse( - Response response, - ConvertResponse? responseConverter, - ) async { - if (responseConverter != null) { - response = await responseConverter(response); - } else if (converter != null) { - response = - await _decodeResponse(response, converter!); - } - - return Response( - response.base, - response.body, - ); - } - - Future _handleRequestConverter( - Request request, - ConvertRequest? requestConverter, - ) async => - request.body != null || request.parts.isNotEmpty - ? requestConverter != null - ? await requestConverter(request) - : await _encodeRequest(request) - : request; - /// Sends a pre-build [Request], applying all provided [Interceptor]s and /// [Converter]s. /// @@ -292,63 +164,22 @@ base class ChopperClient { Future> send( Request request, { ConvertRequest? requestConverter, - ConvertResponse? responseConverter, + ConvertResponse? responseConverter, }) async { - final Request req = await _interceptRequest( - await _handleRequestConverter(request, requestConverter), + final call = Call( + request: request, + client: this, + requestCallback: _requestController.add, ); - _requestController.add(req); - - final streamRes = await httpClient.send(await req.toBaseRequest()); - if (isTypeOf>>()) { - return Response(streamRes, (streamRes.stream) as BodyType); - } - - final response = await http.Response.fromStream(streamRes); - dynamic res = Response(response, response.body); - - if (authenticator != null) { - final Request? updatedRequest = - await authenticator!.authenticate(req, res, request); - - if (updatedRequest != null) { - res = await send( - updatedRequest, - requestConverter: requestConverter, - responseConverter: responseConverter, - ); - // To prevent double call with typed response - if (_responseIsSuccessful(res.statusCode)) { - await authenticator!.onAuthenticationSuccessful - ?.call(updatedRequest, res, request); - return _processResponse(res); - } else { - res = await _handleErrorResponse(res); - await authenticator!.onAuthenticationFailed - ?.call(updatedRequest, res, request); - return _processResponse(res); - } - } - } - - res = _responseIsSuccessful(res.statusCode) - ? await _handleSuccessResponse( - res, - responseConverter, - ) - : await _handleErrorResponse(res); - - return _processResponse(res); - } + final response = await call.execute( + requestConverter, + responseConverter, + ); - Future> _processResponse( - dynamic res, - ) async { - res = await _interceptResponse(res); - _responseController.add(res); + _responseController.add(response); - return res; + return response; } /// Makes a HTTP GET request using the [send] function. @@ -501,20 +332,17 @@ base class ChopperClient { _responseController.close(); _services.clear(); - _requestInterceptors.clear(); - _responseInterceptors.clear(); - if (_clientIsInternal) { httpClient.close(); } } /// A stream of processed [Request]s, as in after all [Converter]s, and - /// [RequestInterceptor]s have been run. + /// [Interceptor]s have been run. Stream get onRequest => _requestController.stream; /// A stream of processed [Response]s, as in after all [Converter]s and - /// [ResponseInterceptor]s have been run. + /// [Interceptor]s have been run. Stream get onResponse => _responseController.stream; } @@ -548,6 +376,3 @@ abstract class ChopperService { // TODO: use runtimeType Type get definitionType; } - -bool _responseIsSuccessful(int statusCode) => - statusCode >= 200 && statusCode < 300; diff --git a/chopper/lib/src/chain/call.dart b/chopper/lib/src/chain/call.dart new file mode 100644 index 00000000..4bb277a7 --- /dev/null +++ b/chopper/lib/src/chain/call.dart @@ -0,0 +1,58 @@ +import 'package:chopper/src/annotations.dart'; +import 'package:chopper/src/base.dart'; +import 'package:chopper/src/chain/interceptor_chain.dart'; +import 'package:chopper/src/interceptors/authenticator_interceptor.dart'; +import 'package:chopper/src/interceptors/http_call_interceptor.dart'; +import 'package:chopper/src/interceptors/interceptor.dart'; +import 'package:chopper/src/interceptors/request_converter_interceptor.dart'; +import 'package:chopper/src/interceptors/request_stream_interceptor.dart'; +import 'package:chopper/src/interceptors/response_converter_interceptor.dart'; +import 'package:chopper/src/request.dart'; +import 'package:chopper/src/response.dart'; + +/// {@template Call} +/// A single call to a HTTP endpoint. It holds the [request] and the [client]. +/// {@endtemplate} +class Call { + /// {@macro Call} + Call({ + required this.request, + required this.client, + required this.requestCallback, + }); + + /// Request to be executed. + final Request request; + + /// Chopper client that created this call. + final ChopperClient client; + + /// Callback to send intercepted and converted request to the stream controller. + final void Function(Request event) requestCallback; + + Future> execute( + ConvertRequest? requestConverter, + ConvertResponse? responseConverter, + ) async { + final interceptors = [ + RequestConverterInterceptor(client.converter, requestConverter), + ...client.interceptors, + RequestStreamInterceptor(requestCallback), + if (client.authenticator != null) + AuthenticatorInterceptor(client.authenticator!), + ResponseConverterInterceptor( + converter: client.converter, + errorConverter: client.errorConverter, + responseConverter: responseConverter, + ), + HttpCallInterceptor(client.httpClient), + ]; + + final interceptorChain = InterceptorChain( + request: request, + interceptors: interceptors, + ); + + return await interceptorChain.proceed(request); + } +} diff --git a/chopper/lib/src/chain/chain.dart b/chopper/lib/src/chain/chain.dart new file mode 100644 index 00000000..327c2e5a --- /dev/null +++ b/chopper/lib/src/chain/chain.dart @@ -0,0 +1,21 @@ +import 'dart:async'; + +import 'package:chopper/src/request.dart'; +import 'package:chopper/src/response.dart'; + +/// A single chain instance in the chain of interceptors that is called in order to process requests and responses. +/// +/// The chain is used to proceed to the next interceptor in the chain. +/// Call [proceed] to proceed to the next interceptor in the chain. +/// ```dart +/// await chain.proceed(request); +/// ``` +abstract interface class Chain { + /// Proceed to the next interceptor in the chain. + /// Provide the [request] to be processed by the next interceptor. + FutureOr> proceed(Request request); + + /// The request to be processed by the chain up to this point. + /// The request is provide by the previous interceptor in the chain. + Request get request; +} diff --git a/chopper/lib/src/chain/interceptor_chain.dart b/chopper/lib/src/chain/interceptor_chain.dart new file mode 100644 index 00000000..116299ff --- /dev/null +++ b/chopper/lib/src/chain/interceptor_chain.dart @@ -0,0 +1,74 @@ +import 'dart:async'; + +import 'package:chopper/src/chain/chain.dart'; +import 'package:chopper/src/chopper_exception.dart'; +import 'package:chopper/src/interceptors/interceptor.dart'; +import 'package:chopper/src/interceptors/internal_interceptor.dart'; +import 'package:chopper/src/request.dart'; +import 'package:chopper/src/response.dart'; + +/// {@template InterceptorChain} +/// A chain of interceptors that are called in order to process requests and responses. +/// {@endtemplate} +class InterceptorChain implements Chain { + /// {@macro InterceptorChain} + InterceptorChain({ + required this.interceptors, + required this.request, + this.index = 0, + }) : assert(interceptors.isNotEmpty, 'Interceptors list must not be empty'); + + @override + final Request request; + + /// Response received from the next interceptor in the chain. + Response? response; + + /// List of interceptors to be called in order. + final List interceptors; + + /// Index of the current interceptor in the chain. + final int index; + + @override + FutureOr> proceed(Request request) async { + assert(index < interceptors.length, 'Interceptor index out of bounds'); + if (index - 1 >= 0 && interceptors[index - 1] is! InternalInterceptor) { + assert( + this.request.body == request.body, + 'Interceptor [${interceptors[index - 1].runtimeType}] should not transform the body of the request, ' + 'Use Request converter instead', + ); + } + + final interceptor = interceptors[index]; + final next = copyWith(request: request, index: index + 1); + response = await interceptor.intercept(next); + + if (index + 1 < interceptors.length && + interceptor is! InternalInterceptor) { + if (response == null) { + throw ChopperException('Response is null', request: request); + } + + assert( + response?.body == next.response?.body, + 'Interceptor [${interceptor.runtimeType}] should not transform the body of the response, ' + 'Use Response converter instead', + ); + } + + return response!; + } + + /// Copy the current [InterceptorChain]. With updated [request] or [index]. + InterceptorChain copyWith({ + Request? request, + int? index, + }) => + InterceptorChain( + request: request ?? this.request, + index: index ?? this.index, + interceptors: interceptors, + ); +} diff --git a/chopper/lib/src/chopper_exception.dart b/chopper/lib/src/chopper_exception.dart new file mode 100644 index 00000000..de26ef24 --- /dev/null +++ b/chopper/lib/src/chopper_exception.dart @@ -0,0 +1,24 @@ +import 'package:chopper/src/request.dart'; +import 'package:chopper/src/response.dart'; + +/// {@template ChopperException} +/// An exception thrown when something goes wrong with Chopper. +/// {@endtemplate} +class ChopperException implements Exception { + /// {@macro ChopperException} + ChopperException(this.message, {this.response, this.request}); + + /// The response that caused the exception. + final Response? response; + + /// The request that caused the exception. + final Request? request; + + /// The message of the exception. + final String message; + + @override + String toString() { + return 'ChopperException: $message ${response != null ? ', \nResponse: $response' : ''}${request != null ? ', \nRequest: $request' : ''}'; + } +} diff --git a/chopper/lib/src/chopper_http_exception.dart b/chopper/lib/src/chopper_http_exception.dart index cae57ce2..c07a3bd4 100644 --- a/chopper/lib/src/chopper_http_exception.dart +++ b/chopper/lib/src/chopper_http_exception.dart @@ -1,9 +1,13 @@ import 'package:chopper/src/response.dart'; +/// {@template ChopperHttpException} /// An exception thrown when a [Response] is unsuccessful < 200 or > 300. +/// {@endtemplate} class ChopperHttpException implements Exception { + /// {@macro ChopperHttpException} ChopperHttpException(this.response); + /// The response that caused the exception. final Response response; @override diff --git a/chopper/lib/src/interceptor.dart b/chopper/lib/src/converters.dart similarity index 61% rename from chopper/lib/src/interceptor.dart rename to chopper/lib/src/converters.dart index d76e2920..d9a9c130 100644 --- a/chopper/lib/src/interceptor.dart +++ b/chopper/lib/src/converters.dart @@ -1,74 +1,20 @@ import 'dart:async'; import 'dart:convert'; -import 'package:chopper/src/constants.dart'; import 'package:chopper/src/request.dart'; import 'package:chopper/src/response.dart'; import 'package:chopper/src/utils.dart'; -import 'package:http/http.dart' as http; import 'package:meta/meta.dart'; -/// An interface for implementing response interceptors. -/// -/// [ResponseInterceptor]s are called after [Converter.convertResponse]. -/// -/// While [ResponseInterceptor]s *can* modify the body of responses, -/// converting (decoding) the response body should be handled by [Converter]s. -/// -/// See built-in [HttpLoggingInterceptor] for a fully functional example implementation. -/// -/// A short example for extracting a header value from a response: -/// -/// ```dart -/// class MyResponseInterceptor implements ResponseInterceptor { -/// String _token; -/// -/// @override -/// FutureOr onResponse(Response response) { -/// _token = response.headers['auth_token']; -/// return response; -/// } -/// } -/// ``` -@immutable -abstract interface class ResponseInterceptor { - FutureOr onResponse(Response response); -} - -/// An interface for implementing request interceptors. -/// -/// [RequestInterceptor]s are called after [Converter.convertRequest]. -/// -/// While [RequestInterceptor]s *can* modify the body of requests, -/// converting (encoding) the request body should be handled by [Converter]s. -/// -/// See built-in [CurlInterceptor] and [HttpLoggingInterceptor] for fully -/// functional example implementations. -/// -/// A short example for adding an authentication token to every request: -/// -/// ```dart -/// class MyRequestInterceptor implements ResponseInterceptor { -/// @override -/// FutureOr onRequest(Request request) { -/// return applyHeader(request, 'auth_token', 'Bearer $token'); -/// } -/// } -/// ``` -/// -/// (See [applyHeader(request, name, value)] and [applyHeaders(request, headers)].) -@immutable -abstract interface class RequestInterceptor { - FutureOr onRequest(Request request); -} +import 'constants.dart'; /// An interface for implementing request and response converters. /// /// [Converter]s convert objects to and from their representation in HTTP. /// -/// [convertRequest] is called before [RequestInterceptor]s +/// [convertRequest] is called before [Interceptor]s /// and [convertResponse] is called just after the HTTP response, -/// before [ResponseInterceptor]s. +/// before returning through the [Interceptor]s. /// /// See [JsonConverter] and [FormUrlEncodedConverter] for example implementations. @immutable @@ -93,79 +39,13 @@ abstract interface class Converter { /// An interface for implementing error response converters. /// /// An `ErrorConverter` is called only on error responses -/// (statusCode < 200 || statusCode >= 300) and before any [ResponseInterceptor]s. +/// (statusCode < 200 || statusCode >= 300) and before returning to any [Interceptor]s. abstract interface class ErrorConverter { /// Converts the received [Response] to a [Response] which has a body with the /// HTTP representation of the original body. FutureOr convertError(Response response); } -/// {@template HeadersInterceptor} -/// A [RequestInterceptor] that adds [headers] to every request. -/// -/// Note that this interceptor will overwrite existing headers having the same -/// keys as [headers]. -/// {@endtemplate} -@immutable -class HeadersInterceptor implements RequestInterceptor { - final Map headers; - - /// {@macro HeadersInterceptor} - const HeadersInterceptor(this.headers); - - @override - Future onRequest(Request request) async => - applyHeaders(request, headers); -} - -typedef ResponseInterceptorFunc1 = FutureOr> - Function( - Response response, -); -typedef ResponseInterceptorFunc2 = FutureOr> - Function( - Response response, -); -typedef DynamicResponseInterceptorFunc = FutureOr Function( - Response response, -); -typedef RequestInterceptorFunc = FutureOr Function(Request request); - -/// A [RequestInterceptor] implementation that prints a curl request equivalent -/// to the network call channeled through it for debugging purposes. -/// -/// Thanks, @edwardaux -@immutable -class CurlInterceptor implements RequestInterceptor { - @override - Future onRequest(Request request) async { - final http.BaseRequest baseRequest = await request.toBaseRequest(); - final List curlParts = ['curl -v -X ${baseRequest.method}']; - for (final MapEntry header in baseRequest.headers.entries) { - curlParts.add("-H '${header.key}: ${header.value}'"); - } - // this is fairly naive, but it should cover most cases - if (baseRequest is http.Request) { - final String body = baseRequest.body; - if (body.isNotEmpty) { - curlParts.add("-d '$body'"); - } - } - if (baseRequest is http.MultipartRequest) { - for (final MapEntry field in baseRequest.fields.entries) { - curlParts.add("-f '${field.key}: ${field.value}'"); - } - for (final http.MultipartFile file in baseRequest.files) { - curlParts.add("-f '${file.field}: ${file.filename ?? ''}'"); - } - } - curlParts.add('"${baseRequest.url}"'); - chopperLogger.info(curlParts.join(' ')); - - return request; - } -} - /// {@template JsonConverter} /// A [Converter] implementation that calls [json.encode] on [Request]s and /// [json.decode] on [Response]s using the [dart:convert](https://api.dart.dev/stable/2.10.3/dart-convert/dart-convert-library.html) diff --git a/chopper/lib/src/extensions.dart b/chopper/lib/src/extensions.dart index 8d5586c2..3586938f 100644 --- a/chopper/lib/src/extensions.dart +++ b/chopper/lib/src/extensions.dart @@ -25,3 +25,7 @@ extension StripStringExtension on String { String strip([String? character]) => character != null ? leftStrip(character).rightStrip(character) : trim(); } + +extension StatusCodeIntExtension on int { + bool get isSuccessfulStatusCode => this >= 200 && this < 300; +} diff --git a/chopper/lib/src/interceptors/authenticator_interceptor.dart b/chopper/lib/src/interceptors/authenticator_interceptor.dart new file mode 100644 index 00000000..91b558e9 --- /dev/null +++ b/chopper/lib/src/interceptors/authenticator_interceptor.dart @@ -0,0 +1,46 @@ +import 'dart:async'; + +import 'package:chopper/src/authenticator.dart'; +import 'package:chopper/src/chain/chain.dart'; +import 'package:chopper/src/extensions.dart'; +import 'package:chopper/src/interceptors/internal_interceptor.dart'; +import 'package:chopper/src/request.dart'; +import 'package:chopper/src/response.dart'; + +/// {@template AuthenticatorInterceptor} +/// Internal interceptor that handles authentication provided by [authenticator]. +/// {@endtemplate} +class AuthenticatorInterceptor implements InternalInterceptor { + /// {@macro AuthenticatorInterceptor} + AuthenticatorInterceptor(this._authenticator); + + /// Authenticator to be used for authentication. + final Authenticator _authenticator; + + @override + FutureOr> intercept( + Chain chain) async { + final originalRequest = chain.request; + + Response response = await chain.proceed(originalRequest); + + final Request? updatedRequest = await _authenticator.authenticate( + originalRequest, + response, + originalRequest, + ); + + if (updatedRequest != null) { + response = await chain.proceed(updatedRequest); + if (response.statusCode.isSuccessfulStatusCode) { + await _authenticator.onAuthenticationSuccessful + ?.call(updatedRequest, response, originalRequest); + } else { + await _authenticator.onAuthenticationFailed + ?.call(updatedRequest, response, originalRequest); + } + } + + return response; + } +} diff --git a/chopper/lib/src/interceptors/curl_interceptor.dart b/chopper/lib/src/interceptors/curl_interceptor.dart new file mode 100644 index 00000000..9eb4256c --- /dev/null +++ b/chopper/lib/src/interceptors/curl_interceptor.dart @@ -0,0 +1,44 @@ +import 'dart:async'; + +import 'package:chopper/src/chain/chain.dart'; +import 'package:chopper/src/interceptors/interceptor.dart'; +import 'package:chopper/src/response.dart'; +import 'package:chopper/src/utils.dart'; +import 'package:http/http.dart' as http; +import 'package:meta/meta.dart'; + +/// A [Interceptor] implementation that prints a curl request equivalent +/// to the network call channeled through it for debugging purposes. +/// +/// Thanks, @edwardaux +@immutable +class CurlInterceptor implements Interceptor { + @override + FutureOr> intercept( + Chain chain) async { + final http.BaseRequest baseRequest = await chain.request.toBaseRequest(); + final List curlParts = ['curl -v -X ${baseRequest.method}']; + for (final MapEntry header in baseRequest.headers.entries) { + curlParts.add("-H '${header.key}: ${header.value}'"); + } + // this is fairly naive, but it should cover most cases + if (baseRequest is http.Request) { + final String body = baseRequest.body; + if (body.isNotEmpty) { + curlParts.add("-d '$body'"); + } + } + if (baseRequest is http.MultipartRequest) { + for (final MapEntry field in baseRequest.fields.entries) { + curlParts.add("-f '${field.key}: ${field.value}'"); + } + for (final http.MultipartFile file in baseRequest.files) { + curlParts.add("-f '${file.field}: ${file.filename ?? ''}'"); + } + } + curlParts.add('"${baseRequest.url}"'); + chopperLogger.info(curlParts.join(' ')); + + return chain.proceed(chain.request); + } +} diff --git a/chopper/lib/src/interceptors/headers_interceptor.dart b/chopper/lib/src/interceptors/headers_interceptor.dart new file mode 100644 index 00000000..7f7057e0 --- /dev/null +++ b/chopper/lib/src/interceptors/headers_interceptor.dart @@ -0,0 +1,31 @@ +import 'dart:async'; + +import 'package:chopper/src/chain/chain.dart'; +import 'package:chopper/src/interceptors/interceptor.dart'; +import 'package:chopper/src/response.dart'; +import 'package:chopper/src/utils.dart'; +import 'package:meta/meta.dart'; + +/// {@template HeadersInterceptor} +/// A [Interceptor] that adds [headers] to every request. +/// +/// Note that this interceptor will overwrite existing headers having the same +/// keys as [headers]. +/// {@endtemplate} +@immutable +class HeadersInterceptor implements Interceptor { + final Map headers; + + /// {@macro HeadersInterceptor} + const HeadersInterceptor(this.headers); + + @override + FutureOr> intercept( + Chain chain) async => + chain.proceed( + applyHeaders( + chain.request, + headers, + ), + ); +} diff --git a/chopper/lib/src/interceptors/http_call_interceptor.dart b/chopper/lib/src/interceptors/http_call_interceptor.dart new file mode 100644 index 00000000..b7b204bf --- /dev/null +++ b/chopper/lib/src/interceptors/http_call_interceptor.dart @@ -0,0 +1,36 @@ +import 'dart:async'; + +import 'package:chopper/src/chain/chain.dart'; +import 'package:chopper/src/chopper_exception.dart'; +import 'package:chopper/src/interceptors/internal_interceptor.dart'; +import 'package:chopper/src/response.dart'; +import 'package:http/http.dart' as http; + +import '../utils.dart'; + +/// {@template HttpCallInterceptor} +/// Internal interceptor that handles the actual HTTP calls. HTTP calls are handled by [_httpClient] for http package. +/// {@endtemplate} +class HttpCallInterceptor implements InternalInterceptor { + /// {@macro HttpCallInterceptor} + const HttpCallInterceptor(this._httpClient); + + /// HTTP client to be used for making the actual HTTP calls. + final http.Client _httpClient; + + @override + FutureOr> intercept( + Chain chain) async { + final finalRequest = await chain.request.toBaseRequest(); + final streamRes = await _httpClient.send(finalRequest); + + if (isTypeOf>>()) { + return Response(streamRes, (streamRes.stream) as BodyType); + } else if (isTypeOf()) { + final response = await http.Response.fromStream(streamRes); + return Response(response, response.body as BodyType); + } else { + throw ChopperException('Unsupported type', request: chain.request); + } + } +} diff --git a/chopper/lib/src/http_logging_interceptor.dart b/chopper/lib/src/interceptors/http_logging_interceptor.dart similarity index 62% rename from chopper/lib/src/http_logging_interceptor.dart rename to chopper/lib/src/interceptors/http_logging_interceptor.dart index bdf37b35..95bb6b2c 100644 --- a/chopper/lib/src/http_logging_interceptor.dart +++ b/chopper/lib/src/interceptors/http_logging_interceptor.dart @@ -1,8 +1,8 @@ import 'dart:async'; +import 'package:chopper/src/chain/chain.dart'; import 'package:chopper/src/chopper_log_record.dart'; -import 'package:chopper/src/interceptor.dart'; -import 'package:chopper/src/request.dart'; +import 'package:chopper/src/interceptors/interceptor.dart'; import 'package:chopper/src/response.dart'; import 'package:chopper/src/utils.dart'; import 'package:http/http.dart' as http; @@ -61,7 +61,7 @@ enum Level { } /// {@template http_logging_interceptor} -/// A [RequestInterceptor] and [ResponseInterceptor] implementation which logs +/// A [Interceptor] implementation which logs /// HTTP request and response data. /// /// Log levels can be set by applying [level] for more fine grained control @@ -73,8 +73,7 @@ enum Level { /// or in a non-production environment. /// {@endtemplate} @immutable -class HttpLoggingInterceptor - implements RequestInterceptor, ResponseInterceptor { +class HttpLoggingInterceptor implements Interceptor { /// {@macro http_logging_interceptor} HttpLoggingInterceptor({this.level = Level.body, Logger? logger}) : _logger = logger ?? chopperLogger, @@ -87,18 +86,21 @@ class HttpLoggingInterceptor final bool _logHeaders; @override - FutureOr onRequest(Request request) async { - if (level == Level.none) return request; - final http.BaseRequest base = await request.toBaseRequest(); - - String startRequestMessage = '--> ${base.method} ${base.url.toString()}'; - String bodyMessage = ''; - if (base is http.Request) { - if (base.body.isNotEmpty) { - bodyMessage = base.body; + FutureOr> intercept( + Chain chain) async { + final request = chain.request; + if (level == Level.none) return chain.proceed(request); + final http.BaseRequest baseRequest = await request.toBaseRequest(); + + String startRequestMessage = + '--> ${baseRequest.method} ${baseRequest.url.toString()}'; + String bodyRequestMessage = ''; + if (baseRequest is http.Request) { + if (baseRequest.body.isNotEmpty) { + bodyRequestMessage = baseRequest.body; if (!_logHeaders) { - startRequestMessage += ' (${base.bodyBytes.length}-byte body)'; + startRequestMessage += ' (${baseRequest.bodyBytes.length}-byte body)'; } } } @@ -108,53 +110,53 @@ class HttpLoggingInterceptor _logger.info(ChopperLogRecord(startRequestMessage, request: request)); if (_logHeaders) { - base.headers.forEach( + baseRequest.headers.forEach( (k, v) => _logger.info(ChopperLogRecord('$k: $v', request: request)), ); - if (base.contentLength != null && - base.headers['content-length'] == null) { + if (baseRequest.contentLength != null && + baseRequest.headers['content-length'] == null) { _logger.info(ChopperLogRecord( - 'content-length: ${base.contentLength}', + 'content-length: ${baseRequest.contentLength}', request: request, )); } } - if (_logBody && bodyMessage.isNotEmpty) { + if (_logBody && bodyRequestMessage.isNotEmpty) { _logger.info(ChopperLogRecord('', request: request)); - _logger.info(ChopperLogRecord(bodyMessage, request: request)); + _logger.info(ChopperLogRecord(bodyRequestMessage, request: request)); } if (_logHeaders || _logBody) { _logger.info(ChopperLogRecord( - '--> END ${base.method}', + '--> END ${baseRequest.method}', request: request, )); } + final stopWatch = Stopwatch()..start(); - return request; - } + final response = await chain.proceed(request); + + stopWatch.stop(); - @override - FutureOr onResponse(Response response) { if (level == Level.none) return response; - final base = response.base; + final baseResponse = response.base; String bytes = ''; String reasonPhrase = response.statusCode.toString(); - String bodyMessage = ''; - if (base is http.Response) { - if (base.reasonPhrase != null) { + String bodyResponseMessage = ''; + if (baseResponse is http.Response) { + if (baseResponse.reasonPhrase != null) { reasonPhrase += - ' ${base.reasonPhrase != reasonPhrase ? base.reasonPhrase : ''}'; + ' ${baseResponse.reasonPhrase != reasonPhrase ? baseResponse.reasonPhrase : ''}'; } - if (base.body.isNotEmpty) { - bodyMessage = base.body; + if (baseResponse.body.isNotEmpty) { + bodyResponseMessage = baseResponse.body; if (!_logBody && !_logHeaders) { - bytes = ' (${response.bodyBytes.length}-byte body)'; + bytes = ', ${response.bodyBytes.length}-byte body'; } } } @@ -162,27 +164,27 @@ class HttpLoggingInterceptor // Always start on a new line _logger.info(ChopperLogRecord('', response: response)); _logger.info(ChopperLogRecord( - '<-- $reasonPhrase ${base.request?.method} ${base.request?.url.toString()}$bytes', + '<-- $reasonPhrase ${baseResponse.request?.method} ${baseResponse.request?.url.toString()} (${stopWatch.elapsedMilliseconds}ms$bytes)', response: response, )); if (_logHeaders) { - base.headers.forEach( + baseResponse.headers.forEach( (k, v) => _logger.info(ChopperLogRecord('$k: $v', response: response)), ); - if (base.contentLength != null && - base.headers['content-length'] == null) { + if (baseResponse.contentLength != null && + baseResponse.headers['content-length'] == null) { _logger.info(ChopperLogRecord( - 'content-length: ${base.contentLength}', + 'content-length: ${baseResponse.contentLength}', response: response, )); } } - if (_logBody && bodyMessage.isNotEmpty) { + if (_logBody && bodyResponseMessage.isNotEmpty) { _logger.info(ChopperLogRecord('', response: response)); - _logger.info(ChopperLogRecord(bodyMessage, response: response)); + _logger.info(ChopperLogRecord(bodyResponseMessage, response: response)); } if (_logBody || _logHeaders) { diff --git a/chopper/lib/src/interceptors/interceptor.dart b/chopper/lib/src/interceptors/interceptor.dart new file mode 100644 index 00000000..9671d398 --- /dev/null +++ b/chopper/lib/src/interceptors/interceptor.dart @@ -0,0 +1,56 @@ +import 'dart:async'; + +import 'package:chopper/chopper.dart'; +import 'package:meta/meta.dart'; + +export 'package:chopper/src/interceptors/curl_interceptor.dart'; +export 'package:chopper/src/interceptors/headers_interceptor.dart'; +export 'package:chopper/src/interceptors/http_logging_interceptor.dart'; + +/// The interface for implementing interceptors. +/// Interceptors are used for intercepting request, responses and preforming operations on them. +/// +/// Interceptor are called in a Chain order. +/// The first interceptor in the chain calls the next interceptor in the chain and so on. +/// The last interceptor in the chain return the response back to the previous interceptor in the chain and so on. +/// This means the request are processed in the order defined by the chain. +/// The responses are process in the reverse order defined by the chain. +/// +/// Chopper has a few built-in interceptors which can be inspected as fully working examples: +/// [HttpLoggingInterceptor], [CurlInterceptor] and [HeaderInterceptor]. +/// +/// A short example for adding an authentication token to every request: +/// +/// ```dart +/// class MyRequestInterceptor implements Interceptor { +/// final String token; +/// +/// @override +/// FutureOr> intercept(Chain chain) async { +/// final request = applyHeader(chain.request, 'auth_token', 'Bearer $token'); +/// return chain.proceed(request); +/// } +/// } +/// ``` +/// A short example for extracting a header value from a response: +/// +/// ```dart +/// class MyResponseInterceptor implements Interceptor { +/// String _token; +/// +/// @override +/// FutureOr> intercept(Chain chain) async { +/// final response = await chain.proceed(chain.request); +/// +/// _token = response.headers['auth_token']; +/// return response; +/// } +/// } +/// ``` +/// +/// **While [Interceptor]s *can* modify the body of requests and responses, +/// converting (encoding) the request/response body should be handled by [Converter]s.** +@immutable +abstract interface class Interceptor { + FutureOr> intercept(Chain chain); +} diff --git a/chopper/lib/src/interceptors/internal_interceptor.dart b/chopper/lib/src/interceptors/internal_interceptor.dart new file mode 100644 index 00000000..a6a80201 --- /dev/null +++ b/chopper/lib/src/interceptors/internal_interceptor.dart @@ -0,0 +1,4 @@ +import 'package:chopper/src/interceptors/interceptor.dart'; + +/// An interface for implementing Internal interceptors only used by Chopper itself. +abstract interface class InternalInterceptor implements Interceptor {} diff --git a/chopper/lib/src/interceptors/request_converter_interceptor.dart b/chopper/lib/src/interceptors/request_converter_interceptor.dart new file mode 100644 index 00000000..beb05993 --- /dev/null +++ b/chopper/lib/src/interceptors/request_converter_interceptor.dart @@ -0,0 +1,47 @@ +import 'dart:async'; + +import 'package:chopper/src/annotations.dart'; +import 'package:chopper/src/chain/chain.dart'; +import 'package:chopper/src/converters.dart'; +import 'package:chopper/src/interceptors/internal_interceptor.dart'; +import 'package:chopper/src/request.dart'; +import 'package:chopper/src/response.dart'; + +/// {@template RequestConverterInterceptor} +/// Internal interceptor that handles request conversion provided by [_requestConverter] or [_converter]. +/// {@endtemplate} +class RequestConverterInterceptor implements InternalInterceptor { + /// {@macro RequestConverterInterceptor} + RequestConverterInterceptor(this._converter, this._requestConverter); + + /// Converter to be used for request conversion. + final Converter? _converter; + + /// Request converter to be used for request conversion. + final ConvertRequest? _requestConverter; + + @override + FutureOr> intercept( + Chain chain) async => + await chain.proceed( + await _handleRequestConverter( + chain.request, + _requestConverter, + ), + ); + + /// Converts the [request] using [_requestConverter] if it is not null, otherwise uses [_converter]. + Future _handleRequestConverter( + Request request, + ConvertRequest? requestConverter, + ) async => + request.body != null || request.parts.isNotEmpty + ? requestConverter != null + ? await requestConverter(request) + : await _encodeRequest(request) + : request; + + /// Encodes the [request] using [_converter] if not null. + Future _encodeRequest(Request request) async => + _converter?.convertRequest(request) ?? request; +} diff --git a/chopper/lib/src/interceptors/request_stream_interceptor.dart b/chopper/lib/src/interceptors/request_stream_interceptor.dart new file mode 100644 index 00000000..377d5310 --- /dev/null +++ b/chopper/lib/src/interceptors/request_stream_interceptor.dart @@ -0,0 +1,20 @@ +import 'dart:async'; + +import 'package:chopper/src/chain/chain.dart'; +import 'package:chopper/src/interceptors/internal_interceptor.dart'; +import 'package:chopper/src/request.dart'; +import 'package:chopper/src/response.dart'; + +class RequestStreamInterceptor implements InternalInterceptor { + const RequestStreamInterceptor(this.callback); + + final FutureOr Function(Request event) callback; + + @override + FutureOr> intercept( + Chain chain) async { + await callback(chain.request); + + return chain.proceed(chain.request); + } +} diff --git a/chopper/lib/src/interceptors/response_converter_interceptor.dart b/chopper/lib/src/interceptors/response_converter_interceptor.dart new file mode 100644 index 00000000..5d2dd188 --- /dev/null +++ b/chopper/lib/src/interceptors/response_converter_interceptor.dart @@ -0,0 +1,89 @@ +import 'dart:async'; + +import 'package:chopper/src/annotations.dart'; +import 'package:chopper/src/chain/chain.dart'; +import 'package:chopper/src/chain/interceptor_chain.dart'; +import 'package:chopper/src/converters.dart'; +import 'package:chopper/src/extensions.dart'; +import 'package:chopper/src/interceptors/internal_interceptor.dart'; +import 'package:chopper/src/response.dart'; +import 'package:chopper/src/utils.dart'; + +/// {@template ResponseConverterInterceptor} +/// Internal interceptor that handles response conversion provided by [_converter], [_responseConverter] or converts error instead with provided [_errorConverter]. +/// {@endtemplate} +class ResponseConverterInterceptor implements InternalInterceptor { + /// {@macro ResponseConverterInterceptor} + ResponseConverterInterceptor({ + Converter? converter, + ErrorConverter? errorConverter, + FutureOr> Function(Response)? responseConverter, + }) : _responseConverter = responseConverter, + _errorConverter = errorConverter, + _converter = converter; + + /// Converter to be used for response conversion. + final Converter? _converter; + + /// Error converter to be used for error conversion. + final ErrorConverter? _errorConverter; + + /// Response converter to be used for response conversion. + final ConvertResponse? _responseConverter; + + @override + FutureOr> intercept( + Chain chain) async { + final realChain = chain as InterceptorChain; + final typedChain = switch (isTypeOf>>()) { + true => realChain, + false => realChain.copyWith(), + }; + + final response = await typedChain.proceed(chain.request); + + return response.statusCode.isSuccessfulStatusCode + ? _handleSuccessResponse(response, _responseConverter) + : _handleErrorResponse(response); + } + + /// Handles the successful response by converting it using [_responseConverter] or [_converter]. + Future> _handleSuccessResponse( + Response response, + ConvertResponse? responseConverter, + ) async { + Response? newResponse; + if (responseConverter != null) { + newResponse = await responseConverter(response); + } else if (_converter != null) { + newResponse = await _decodeResponse(response, _converter!); + } + + return Response( + newResponse?.base ?? response.base, + newResponse?.body ?? response.body, + ); + } + + /// Converts the [response] using [_converter]. + Future> _decodeResponse( + Response response, + Converter withConverter, + ) async => + await withConverter.convertResponse(response); + + /// Handles the error response by converting it using [_errorConverter]. + Future> _handleErrorResponse( + Response response, + ) async { + var error = response.body; + if (_errorConverter != null) { + final errorRes = await _errorConverter?.convertError( + response, + ); + error = errorRes?.error ?? errorRes?.body; + } + + return Response(response.base, null, error: error); + } +} diff --git a/chopper/test/authenticator_test.dart b/chopper/test/authenticator_test.dart index 7b454462..d1e6e1a6 100644 --- a/chopper/test/authenticator_test.dart +++ b/chopper/test/authenticator_test.dart @@ -1,6 +1,8 @@ import 'dart:convert' show jsonEncode; -import 'package:chopper/chopper.dart'; +import 'package:chopper/src/base.dart'; +import 'package:chopper/src/converters.dart'; +import 'package:chopper/src/interceptors/headers_interceptor.dart'; import 'package:http/http.dart' as http; import 'package:http/testing.dart'; import 'package:test/test.dart'; @@ -14,7 +16,7 @@ void main() async { baseUrl: baseUrl, client: httpClient, interceptors: [ - (Request req) => applyHeader(req, 'foo', 'bar'), + HeadersInterceptor({'foo': 'bar'}), ], converter: JsonConverter(), authenticator: FakeAuthenticator(), diff --git a/chopper/test/base_test.dart b/chopper/test/base_test.dart index d3e0fee2..8d34bbfb 100644 --- a/chopper/test/base_test.dart +++ b/chopper/test/base_test.dart @@ -3,7 +3,11 @@ import 'dart:async'; import 'dart:convert'; -import 'package:chopper/chopper.dart'; +import 'package:chopper/src/base.dart'; +import 'package:chopper/src/constants.dart'; +import 'package:chopper/src/converters.dart'; +import 'package:chopper/src/request.dart'; +import 'package:chopper/src/utils.dart'; import 'package:http/http.dart' as http; import 'package:http/testing.dart'; import 'package:test/test.dart'; @@ -810,52 +814,6 @@ void main() { } }); - test('wrong type for interceptor', () { - expect( - () => ChopperClient(interceptors: [(bool foo) => 'bar']), - throwsA(isA()), - ); - - try { - ChopperClient( - interceptors: [ - (bool foo) => 'bar', - ], - ); - } on AssertionError catch (error) { - expect( - error.toString(), - contains( - 'Unsupported type for interceptors, it only support the following types:\n' - ' - ${allowedInterceptorsType.join('\n - ')}', - ), - ); - } - }, testOn: 'vm'); - - test('wrong type for interceptor', () { - expect( - () => ChopperClient(interceptors: [(bool foo) => 'bar']), - throwsA(isA()), - ); - - try { - ChopperClient( - interceptors: [ - (bool foo) => 'bar', - ], - ); - } on AssertionError catch (error) { - expect( - error.toString(), - contains( - 'Unsupported type for interceptors, it only support the following types:\\n' - ' - ${allowedInterceptorsType.join('\\n - ')}', - ), - ); - } - }, testOn: 'browser'); - test('Query Map 1', () async { final httpClient = MockClient((request) async { expect( diff --git a/chopper/test/chain/authenticator_interceptor_test.dart b/chopper/test/chain/authenticator_interceptor_test.dart new file mode 100644 index 00000000..79e1b18b --- /dev/null +++ b/chopper/test/chain/authenticator_interceptor_test.dart @@ -0,0 +1,136 @@ +import 'dart:async'; + +import 'package:chopper/chopper.dart'; +import 'package:chopper/src/interceptors/authenticator_interceptor.dart'; +import 'package:http/http.dart' as http; +import 'package:test/test.dart'; + +void main() { + late MockAuthenticator authenticator; + late AuthenticatorInterceptor authenticatorInterceptor; + late MockChain chain; + final request = Request('GET', Uri.parse('bar'), Uri.parse('foo')); + + setUp(() { + chain = MockChain( + request, + () => Response( + http.Response('', 200), + '', + ), + ); + authenticator = MockAuthenticator(() => null); + authenticatorInterceptor = AuthenticatorInterceptor(authenticator); + }); + + test('Intercepted response is authenticated, chain.proceed called once', + () async { + await authenticatorInterceptor.intercept(chain); + + expect(authenticator.authenticateCalled, 1); + expect(chain.proceedCalled, 1); + }); + + test('Intercepted response is not authenticated, chain.proceed called twice', + () async { + authenticator = MockAuthenticator(() => request); + authenticatorInterceptor = AuthenticatorInterceptor(authenticator); + + await authenticatorInterceptor.intercept(chain); + + expect(authenticator.authenticateCalled, 1); + expect(chain.proceedCalled, 2); + }); + + test( + 'Intercepted response is not authenticated, authentication is successful', + () async { + authenticator = MockAuthenticator(() => request); + authenticatorInterceptor = AuthenticatorInterceptor(authenticator); + + await authenticatorInterceptor.intercept(chain); + + expect(authenticator.authenticateCalled, 1); + expect(chain.proceedCalled, 2); + expect(authenticator.onAuthenticationSuccessfulCalled, 1); + }); + + test('Intercepted response is not authenticated, authentication failed', + () async { + chain = MockChain( + request, + () => Response( + http.Response('', 400), + '', + ), + ); + authenticator = MockAuthenticator(() => request); + authenticatorInterceptor = AuthenticatorInterceptor(authenticator); + + await authenticatorInterceptor.intercept(chain); + + expect(authenticator.authenticateCalled, 1); + expect(chain.proceedCalled, 2); + expect(authenticator.onAuthenticationFailedCalled, 1); + }); +} + +class MockChain implements Chain { + MockChain(this.request, this.onProceed); + + int proceedCalled = 0; + + final Response Function() onProceed; + + @override + FutureOr> proceed(Request request) async { + proceedCalled++; + return onProceed(); + } + + @override + final Request request; +} + +class MockAuthenticator implements Authenticator { + MockAuthenticator(this.onAuthenticate) { + onAuthenticationFailed = ( + Request request, + Response response, [ + Request? originalRequest, + ]) { + onAuthenticationFailedCalled++; + return; + }; + + onAuthenticationSuccessful = ( + Request request, + Response response, [ + Request? originalRequest, + ]) { + onAuthenticationSuccessfulCalled++; + return; + }; + } + + final Request? Function() onAuthenticate; + + int authenticateCalled = 0; + int onAuthenticationFailedCalled = 0; + int onAuthenticationSuccessfulCalled = 0; + @override + AuthenticationCallback? onAuthenticationFailed; + + @override + AuthenticationCallback? onAuthenticationSuccessful; + + @override + FutureOr authenticate( + Request request, + Response response, [ + Request? originalRequest, + ]) async { + authenticateCalled++; + return onAuthenticate(); + } +} diff --git a/chopper/test/chain/interceptor_chain_test.dart b/chopper/test/chain/interceptor_chain_test.dart new file mode 100644 index 00000000..c7dd28b5 --- /dev/null +++ b/chopper/test/chain/interceptor_chain_test.dart @@ -0,0 +1,229 @@ +import 'dart:async'; + +import 'package:chopper/src/chain/chain.dart'; +import 'package:chopper/src/chain/interceptor_chain.dart'; +import 'package:chopper/src/interceptors/interceptor.dart'; +import 'package:chopper/src/interceptors/internal_interceptor.dart'; +import 'package:chopper/src/request.dart'; +import 'package:chopper/src/response.dart'; +import 'package:http/http.dart' as http; +import 'package:test/test.dart'; + +void main() { + group('InterceptorChain', () { + late Request mockRequest; + late MockInterceptor mockInterceptor; + late InterceptorChain interceptorChain; + + setUp(() { + mockRequest = + Request('GET', Uri.parse('bar'), Uri.parse('http://localhost')); + mockInterceptor = MockInterceptor(); + interceptorChain = InterceptorChain( + interceptors: [mockInterceptor], + request: mockRequest, + ); + }); + + test('is created correctly', () { + expect(interceptorChain.interceptors, [mockInterceptor]); + expect(interceptorChain.request, mockRequest); + }); + + test('copyWith method works as expected', () { + final newRequest = + Request('GET', Uri.parse('foo'), Uri.parse('http://localhost')); + final copiedChain = + interceptorChain.copyWith(request: newRequest, index: 666); + expect(copiedChain.request, newRequest); + expect(copiedChain.interceptors, [mockInterceptor]); + expect(copiedChain.index, 666); + }); + + test('A empty Interceptor chain throws assertion', () { + expect( + () => InterceptorChain( + interceptors: [], + request: mockRequest, + ), + throwsA(isA())); + }); + + test( + 'Intercept chain proceed called with index out of bounds throws assertion', + () async { + final chain = InterceptorChain( + interceptors: [mockInterceptor], + request: mockRequest, + index: 666, + ); + expect(chain.proceed(mockRequest), throwsA(isA())); + }); + }); + + group('interceptor chain proceed tests', () { + late Request mockRequest; + late MockInterceptor mockInterceptor; + late InterceptorChain interceptorChain; + setUp(() { + mockRequest = Request( + 'GET', + Uri.parse('bar'), + Uri.parse('http://localhost'), + body: 'Test', + ); + mockInterceptor = MockInterceptor(); + interceptorChain = InterceptorChain( + interceptors: [mockInterceptor], + request: mockRequest, + ); + }); + + test('proceed method works as expected, invokes the interceptor', () async { + final response = await interceptorChain.proceed(mockRequest); + expect(response.base.request, mockRequest); + expect(response.body, 'TestResponse'); + expect(mockInterceptor.called, 1); + }); + + test('proceed modifies request body, throws assertion', () async { + interceptorChain = InterceptorChain( + interceptors: [RequestModifierInterceptor(), mockInterceptor], + request: mockRequest, + ); + + expect( + () => interceptorChain.proceed(mockRequest), + throwsA( + isA().having( + (e) => e.message, + 'assertion', + 'Interceptor [RequestModifierInterceptor] should not transform the body of the request, ' + 'Use Request converter instead'), + ), + ); + }); + + test('proceed modifies response body, throws assertion', () async { + interceptorChain = InterceptorChain( + interceptors: [ResponseModifierInterceptor(), mockInterceptor], + request: mockRequest, + ); + + expect( + () => interceptorChain.proceed(mockRequest), + throwsA( + isA().having( + (e) => e.message, + 'assertion', + 'Interceptor [ResponseModifierInterceptor] should not transform the body of the response, ' + 'Use Response converter instead'), + ), + ); + }); + + test( + 'Internal interceptor is allowed modify request/response when proceeding, return normally', + () async { + interceptorChain = InterceptorChain( + interceptors: [InternalModifierInterceptor(), mockInterceptor], + request: mockRequest, + ); + + expect( + () => interceptorChain.proceed(mockRequest), + returnsNormally, + ); + }); + + test('proceed chain is broken before reaching the end, returns normally', + () { + interceptorChain = InterceptorChain( + interceptors: [ + PassthroughInterceptor(), + mockInterceptor, + PassthroughInterceptor(), + ], + request: mockRequest, + ); + + expect( + () => interceptorChain.proceed(mockRequest), + returnsNormally, + ); + }); + }); +} + +class RequestModifierInterceptor implements Interceptor { + @override + FutureOr> intercept(Chain chain) { + return chain.proceed( + chain.request.copyWith( + body: '${chain.request.body} modified!', + ), + ); + } +} + +class ResponseModifierInterceptor implements Interceptor { + @override + FutureOr> intercept( + Chain chain) async { + final response = await chain.proceed(chain.request); + + return response.copyWith( + body: '${response.body ?? ''} modified!' as BodyType); + } +} + +class DoubleProceedInterceptor implements Interceptor { + @override + FutureOr> intercept( + Chain chain) async { + final _ = await chain.proceed(chain.request); + final response2 = await chain.proceed(chain.request); + + return response2; + } +} + +class PassthroughInterceptor implements Interceptor { + @override + FutureOr> intercept( + Chain chain) async { + return await chain.proceed(chain.request); + } +} + +class InternalModifierInterceptor implements InternalInterceptor { + @override + FutureOr> intercept( + Chain chain) async { + final request = chain.request.copyWith( + body: '${chain.request.body} modified!', + ); + + final response = await chain.proceed(request); + + return response.copyWith( + body: '${response.body ?? ''} modified!' as BodyType); + } +} + +// ignore: must_be_immutable +class MockInterceptor implements InternalInterceptor { + MockInterceptor({this.response}); + + int called = 0; + + final Response? response; + + @override + FutureOr> intercept(Chain chain) { + called++; + return response as Response? ?? + Response(http.Response('TestResponse', 200, request: chain.request), + 'TestResponse' as BodyType); + } +} diff --git a/chopper/test/chain/request_converter_interceptor_test.dart b/chopper/test/chain/request_converter_interceptor_test.dart new file mode 100644 index 00000000..dede0994 --- /dev/null +++ b/chopper/test/chain/request_converter_interceptor_test.dart @@ -0,0 +1,146 @@ +import 'dart:async'; + +import 'package:chopper/src/chain/chain.dart'; +import 'package:chopper/src/chain/interceptor_chain.dart'; +import 'package:chopper/src/converters.dart'; +import 'package:chopper/src/interceptors/interceptor.dart'; +import 'package:chopper/src/interceptors/request_converter_interceptor.dart'; +import 'package:chopper/src/request.dart'; +import 'package:chopper/src/response.dart'; +import 'package:http/http.dart' as http; +import 'package:test/test.dart'; + +void main() { + late InterceptorChain interceptorChain; + + test('request body is null and parts is empty, is not converted', () async { + final testRequest = Request('GET', Uri.parse('foo'), Uri.parse('bar')); + final converter = RequestConverter(); + interceptorChain = InterceptorChain( + interceptors: [ + RequestConverterInterceptor( + converter, + null, + ), + RequestInterceptor(onRequest: (request) { + expect(request.body, null); + }), + ], + request: testRequest, + ); + + await interceptorChain.proceed(testRequest); + + expect(converter.called, 0); + }); + + test( + 'request body is not null and parts is empty, requestConverter is not provided, request is converted by converter', + () async { + final testRequest = Request('GET', Uri.parse('foo'), Uri.parse('bar'), + body: 'not converted'); + final converter = RequestConverter(); + interceptorChain = InterceptorChain( + interceptors: [ + RequestConverterInterceptor( + converter, + null, + ), + RequestInterceptor(onRequest: (request) { + expect(request.body, 'converted'); + }), + ], + request: testRequest, + ); + + await interceptorChain.proceed(testRequest); + + expect(converter.called, 1); + }); + + test( + 'request body is null and parts is not empty, requestConverter is not provided, request is converted by converter', + () async { + final testRequest = Request('GET', Uri.parse('foo'), Uri.parse('bar'), + parts: [PartValue('not converted', 1)]); + final converter = RequestConverter(); + interceptorChain = InterceptorChain( + interceptors: [ + RequestConverterInterceptor( + converter, + null, + ), + RequestInterceptor(onRequest: (request) { + expect(request.body, 'converted'); + }), + ], + request: testRequest, + ); + + await interceptorChain.proceed(testRequest); + + expect(converter.called, 1); + }); + + test( + 'request body is not null and parts is empty, requestConverter is provided, request is converted by requestConverter', + () async { + final testRequest = Request('GET', Uri.parse('foo'), Uri.parse('bar'), + body: 'not converted'); + final converter = RequestConverter(); + int called = 0; + interceptorChain = InterceptorChain( + interceptors: [ + RequestConverterInterceptor( + converter, + (req) { + called++; + return req.copyWith(body: 'foo'); + }, + ), + RequestInterceptor(onRequest: (request) { + expect(request.body, 'foo'); + }), + ], + request: testRequest, + ); + + await interceptorChain.proceed(testRequest); + + expect(called, 1); + expect(converter.called, 0); + }); +} + +// ignore mutability warning for test class. +//ignore: must_be_immutable +class RequestConverter implements Converter { + int called = 0; + @override + FutureOr convertRequest(Request request) { + called++; + return request.copyWith(body: 'converted'); + } + + @override + FutureOr> convertResponse( + Response response) { + return response as Response; + } +} + +// ignore: must_be_immutable +class RequestInterceptor implements Interceptor { + RequestInterceptor({this.onRequest}); + + final void Function(Request)? onRequest; + int called = 0; + + @override + FutureOr> intercept(Chain chain) { + called++; + onRequest?.call(chain.request); + return Response(http.Response('TestResponse', 200, request: chain.request), + 'TestResponse' as BodyType); + } +} diff --git a/chopper/test/chain/response_converter_interceptor_test.dart b/chopper/test/chain/response_converter_interceptor_test.dart new file mode 100644 index 00000000..1edfe680 --- /dev/null +++ b/chopper/test/chain/response_converter_interceptor_test.dart @@ -0,0 +1,201 @@ +import 'dart:async'; + +import 'package:chopper/src/chain/chain.dart'; +import 'package:chopper/src/chain/interceptor_chain.dart'; +import 'package:chopper/src/converters.dart'; +import 'package:chopper/src/interceptors/interceptor.dart'; +import 'package:chopper/src/interceptors/response_converter_interceptor.dart'; +import 'package:chopper/src/request.dart'; +import 'package:chopper/src/response.dart'; +import 'package:http/http.dart' as http; +import 'package:test/test.dart'; + +void main() { + late InterceptorChain interceptorChain; + final testRequest = Request('GET', Uri.parse('foo'), Uri.parse('bar')); + + group('response converter tests', () { + test( + 'response is successful converter is null and response converter is null, response is not converted', + () async { + interceptorChain = InterceptorChain( + interceptors: [ + ResponseConverterInterceptor(), + ResponseInterceptor(), + ], + request: testRequest, + ); + + final response = await interceptorChain.proceed(testRequest); + + expect(response.body, 'TestResponse'); + }); + + test( + 'response is successful converter is not null and response converter is null, response is converted', + () async { + final converter = ResponseConverter(); + interceptorChain = InterceptorChain( + interceptors: [ + ResponseConverterInterceptor(converter: converter), + ResponseInterceptor(), + ], + request: testRequest, + ); + + final response = await interceptorChain.proceed(testRequest); + + expect(response.body, 'converted'); + expect(converter.called, 1); + }); + + test( + 'response is successful converter is not null and response converter is not null, response is converted by response converter', + () async { + final converter = ResponseConverter(); + interceptorChain = InterceptorChain( + interceptors: [ + ResponseConverterInterceptor( + converter: converter, + responseConverter: (response) => + response.copyWith(body: 'response converted')), + ResponseInterceptor(), + ], + request: testRequest, + ); + + final response = await interceptorChain.proceed(testRequest); + + expect(response.body, 'response converted'); + expect(converter.called, 0); + }); + + test( + 'response is unsuccessful converter is not null and response converter is not null, response is not converted', + () async { + final converter = ResponseConverter(); + interceptorChain = InterceptorChain( + interceptors: [ + ResponseConverterInterceptor( + converter: converter, + responseConverter: (response) => + response.copyWith(body: 'response converted')), + ResponseInterceptor( + response: Response( + http.Response('error base', 500, request: testRequest), + 'error')), + ], + request: testRequest, + ); + + final response = await interceptorChain.proceed(testRequest); + + expect(response.body, null); + expect(response.error, 'error'); + expect(converter.called, 0); + }); + }); + + group('response error converter tests', () { + final errorResponse = Response( + http.Response('error base', 500, request: testRequest), 'error'); + test( + 'response is unsuccessful converter is null, response is not converted', + () async { + interceptorChain = InterceptorChain( + interceptors: [ + ResponseConverterInterceptor(), + ResponseInterceptor(response: errorResponse), + ], + request: testRequest, + ); + + final response = await interceptorChain.proceed(testRequest); + + expect(response.body, null); + expect(response.error, 'error'); + }); + + test( + 'response is unsuccessful converter is not null, response is converted', + () async { + final converter = ResponseErrorConverter(); + interceptorChain = InterceptorChain( + interceptors: [ + ResponseConverterInterceptor(errorConverter: converter), + ResponseInterceptor(response: errorResponse), + ], + request: testRequest, + ); + + final response = await interceptorChain.proceed(testRequest); + + expect(response.body, null); + expect(response.error, 'converted'); + expect(converter.called, 1); + }); + + test( + 'response is successful converter is not null, response is not converter', + () async { + final converter = ResponseErrorConverter(); + interceptorChain = InterceptorChain( + interceptors: [ + ResponseConverterInterceptor(errorConverter: converter), + ResponseInterceptor(), + ], + request: testRequest, + ); + + final response = await interceptorChain.proceed(testRequest); + + expect(response.body, 'TestResponse'); + expect(converter.called, 0); + }); + }); +} + +// ignore mutability warning for test class. +//ignore: must_be_immutable +class ResponseConverter implements Converter { + int called = 0; + + @override + FutureOr convertRequest(Request request) { + return request; + } + + @override + FutureOr> convertResponse( + Response response) { + called++; + return response.copyWith(body: 'converted' as BodyType); + } +} + +// ignore mutability warning for test class. +//ignore: must_be_immutable +class ResponseErrorConverter implements ErrorConverter { + int called = 0; + + @override + FutureOr> convertError( + Response response) { + called++; + return response.copyWith(body: 'converted' as BodyType); + } +} + +class ResponseInterceptor implements Interceptor { + ResponseInterceptor({this.response}); + + final Response? response; + + @override + FutureOr> intercept(Chain chain) { + return response as Response? ?? + Response( + http.Response('TestResponse base', 200, request: chain.request), + 'TestResponse' as BodyType); + } +} diff --git a/chopper/test/client_test.dart b/chopper/test/client_test.dart index 88b0d2a1..b8e5497a 100644 --- a/chopper/test/client_test.dart +++ b/chopper/test/client_test.dart @@ -1,6 +1,8 @@ import 'dart:convert'; -import 'package:chopper/chopper.dart'; +import 'package:chopper/src/base.dart'; +import 'package:chopper/src/converters.dart'; +import 'package:chopper/src/interceptors/headers_interceptor.dart'; import 'package:http/http.dart' as http; import 'package:http/testing.dart'; import 'package:test/test.dart'; @@ -12,7 +14,7 @@ void main() { baseUrl: baseUrl, client: httpClient, interceptors: [ - (Request req) => applyHeader(req, 'foo', 'bar'), + HeadersInterceptor({'foo': 'bar'}), ], converter: JsonConverter(), ); diff --git a/chopper/test/converter_test.dart b/chopper/test/converter_test.dart index e1b44d33..17281cc3 100644 --- a/chopper/test/converter_test.dart +++ b/chopper/test/converter_test.dart @@ -1,6 +1,9 @@ import 'dart:convert' as dart_convert; -import 'package:chopper/chopper.dart'; +import 'package:chopper/src/base.dart'; +import 'package:chopper/src/converters.dart'; +import 'package:chopper/src/request.dart'; +import 'package:chopper/src/response.dart'; import 'package:http/http.dart' as http; import 'package:http/testing.dart'; import 'package:test/test.dart'; diff --git a/chopper/test/form_test.dart b/chopper/test/form_test.dart index ef5859d1..4f4a77a2 100644 --- a/chopper/test/form_test.dart +++ b/chopper/test/form_test.dart @@ -1,4 +1,5 @@ -import 'package:chopper/chopper.dart'; +import 'package:chopper/src/base.dart'; +import 'package:chopper/src/converters.dart'; import 'package:http/http.dart' as http; import 'package:http/testing.dart'; import 'package:test/test.dart'; diff --git a/chopper/test/helpers/fake_chain.dart b/chopper/test/helpers/fake_chain.dart new file mode 100644 index 00000000..c3ec8372 --- /dev/null +++ b/chopper/test/helpers/fake_chain.dart @@ -0,0 +1,20 @@ +import 'dart:async'; + +import 'package:chopper/src/chain/chain.dart'; +import 'package:chopper/src/request.dart'; +import 'package:chopper/src/response.dart'; +import 'package:http/http.dart' as http; + +class FakeChain implements Chain { + FakeChain(this.request, {this.response}); + + @override + final Request request; + final Response? response; + + @override + FutureOr> proceed(Request request) { + return response as Response? ?? + Response(http.Response('TestChain', 200), 'TestChain' as BodyType); + } +} diff --git a/chopper/test/http_logging_interceptor_test.dart b/chopper/test/http_logging_interceptor_test.dart index 9a2d49f4..b2ba8aaa 100644 --- a/chopper/test/http_logging_interceptor_test.dart +++ b/chopper/test/http_logging_interceptor_test.dart @@ -1,10 +1,12 @@ -import 'package:chopper/src/http_logging_interceptor.dart'; +import 'package:chopper/src/interceptors/http_logging_interceptor.dart'; import 'package:chopper/src/request.dart'; import 'package:chopper/src/response.dart'; import 'package:chopper/src/utils.dart'; import 'package:http/http.dart' as http; import 'package:test/test.dart'; +import 'helpers/fake_chain.dart'; + void main() { final fakeRequest = Request( 'POST', @@ -20,7 +22,7 @@ void main() { final logs = []; chopperLogger.onRecord.listen((r) => logs.add(r.message)); - await logger.onRequest(fakeRequest); + await logger.intercept(FakeChain(fakeRequest)); expect( logs, @@ -35,11 +37,11 @@ void main() { final logs = []; chopperLogger.onRecord.listen((r) => logs.add(r.message)); - await logger.onRequest(fakeRequest); + await logger.intercept(FakeChain(fakeRequest)); expect( logs, - equals( + containsAll( [ '', '--> POST base/ (4-byte body)', @@ -53,11 +55,11 @@ void main() { final logs = []; chopperLogger.onRecord.listen((r) => logs.add(r.message)); - await logger.onRequest(fakeRequest); + await logger.intercept(FakeChain(fakeRequest)); expect( logs, - equals( + containsAll( [ '', '--> POST base/', @@ -75,11 +77,11 @@ void main() { final logs = []; chopperLogger.onRecord.listen((r) => logs.add(r.message)); - await logger.onRequest(fakeRequest); + await logger.intercept(FakeChain(fakeRequest)); expect( logs, - equals( + containsAll( [ '', '--> POST base/', @@ -115,7 +117,7 @@ void main() { final logs = []; chopperLogger.onRecord.listen((r) => logs.add(r.message)); - await logger.onResponse(fakeResponse); + await logger.intercept(FakeChain(fakeRequest)); expect( logs, @@ -130,14 +132,14 @@ void main() { final logs = []; chopperLogger.onRecord.listen((r) => logs.add(r.message)); - await logger.onResponse(fakeResponse); + await logger.intercept(FakeChain(fakeRequest, response: fakeResponse)); expect( logs, - equals( + containsAll( [ '', - '<-- 200 POST base/ (16-byte body)', + '<-- 200 POST base/ (0ms, 16-byte body)', ], ), ); @@ -148,14 +150,14 @@ void main() { final logs = []; chopperLogger.onRecord.listen((r) => logs.add(r.message)); - await logger.onResponse(fakeResponse); + await logger.intercept(FakeChain(fakeRequest, response: fakeResponse)); expect( logs, - equals( + containsAll( [ '', - '<-- 200 POST base/', + '<-- 200 POST base/ (0ms)', 'foo: bar', 'content-length: 16', '<-- END HTTP', @@ -169,14 +171,14 @@ void main() { final logs = []; chopperLogger.onRecord.listen((r) => logs.add(r.message)); - await logger.onResponse(fakeResponse); + await logger.intercept(FakeChain(fakeRequest, response: fakeResponse)); expect( logs, - equals( + containsAll( [ '', - '<-- 200 POST base/', + '<-- 200 POST base/ (0ms)', 'foo: bar', 'content-length: 16', '', @@ -212,12 +214,12 @@ void main() { final logs = []; chopperLogger.onRecord.listen((r) => logs.add(r.message)); - await logger.onRequest(fakeRequest - .copyWith(headers: {...fakeRequest.headers, 'content-length': '42'})); + await logger.intercept(FakeChain(fakeRequest.copyWith( + headers: {...fakeRequest.headers, 'content-length': '42'}))); expect( logs, - equals( + containsAll( [ '', '--> POST base/', @@ -236,12 +238,12 @@ void main() { final logs = []; chopperLogger.onRecord.listen((r) => logs.add(r.message)); - await logger.onRequest(fakeRequest - .copyWith(headers: {...fakeRequest.headers, 'content-length': '42'})); + await logger.intercept(FakeChain(fakeRequest.copyWith( + headers: {...fakeRequest.headers, 'content-length': '42'}))); expect( logs, - equals( + containsAll( [ '', '--> POST base/', @@ -261,14 +263,14 @@ void main() { final logs = []; chopperLogger.onRecord.listen((r) => logs.add(r.message)); - await logger.onResponse(fakeResponse); + await logger.intercept(FakeChain(fakeRequest, response: fakeResponse)); expect( logs, - equals( + containsAll( [ '', - '<-- 200 POST base/', + '<-- 200 POST base/ (0ms)', 'foo: bar', 'content-length: 42', '<-- END HTTP', @@ -281,14 +283,14 @@ void main() { final logs = []; chopperLogger.onRecord.listen((r) => logs.add(r.message)); - await logger.onResponse(fakeResponse); + await logger.intercept(FakeChain(fakeRequest, response: fakeResponse)); expect( logs, - equals( + containsAll( [ '', - '<-- 200 POST base/', + '<-- 200 POST base/ (0ms)', 'foo: bar', 'content-length: 42', '', diff --git a/chopper/test/interceptors_test.dart b/chopper/test/interceptors_test.dart index 3ee651d0..a1d1a2ac 100644 --- a/chopper/test/interceptors_test.dart +++ b/chopper/test/interceptors_test.dart @@ -1,10 +1,16 @@ import 'dart:async'; -import 'package:chopper/chopper.dart'; +import 'package:chopper/src/base.dart'; +import 'package:chopper/src/chain/chain.dart'; +import 'package:chopper/src/interceptors/interceptor.dart'; +import 'package:chopper/src/request.dart'; +import 'package:chopper/src/response.dart'; +import 'package:chopper/src/utils.dart'; import 'package:http/http.dart' as http; import 'package:http/testing.dart'; import 'package:test/test.dart'; +import 'helpers/fake_chain.dart'; import 'test_service.dart'; void main() { @@ -44,25 +50,6 @@ void main() { ); }); - test('RequestInterceptorFunc', () async { - final chopper = ChopperClient( - interceptors: [ - (Request request) => request.copyWith( - uri: request.uri.replace(path: '${request.uri.path}/intercept'), - ), - ], - services: [ - HttpTestService.create(), - ], - client: requestClient, - ); - - await chopper.getService().getTest( - '1234', - dynamicHeader: '', - ); - }); - test('ResponseInterceptor', () async { final chopper = ChopperClient( interceptors: [ResponseIntercept()], @@ -80,85 +67,6 @@ void main() { expect(ResponseIntercept.intercepted, isA<_Intercepted>()); }); - test('ResponseInterceptorFunc', () async { - dynamic intercepted; - - final chopper = ChopperClient( - interceptors: [ - (Response response) { - intercepted = _Intercepted(response.body); - - return response; - }, - ], - services: [ - HttpTestService.create(), - ], - client: responseClient, - ); - - await chopper.getService().getTest( - '1234', - dynamicHeader: '', - ); - - expect(intercepted, isA<_Intercepted>()); - }); - - test('TypedResponseInterceptorFunc1', () async { - dynamic intercepted; - - final chopper = ChopperClient( - interceptors: [ - (Response response) { - intercepted = _Intercepted(response.body); - - return response; - }, - ], - services: [ - HttpTestService.create(), - ], - client: responseClient, - ); - - await chopper.getService().getTest( - '1234', - dynamicHeader: '', - ); - - expect(intercepted, isA<_Intercepted>()); - }); - - test('TypedResponseInterceptorFunc2', () async { - final client = MockClient((http.Request req) async { - return http.Response('["1","2"]', 200); - }); - - dynamic intercepted; - - final chopper = ChopperClient( - client: client, - converter: JsonConverter(), - interceptors: [ - (Response response) { - expect(isTypeOf(), isTrue); - expect(isTypeOf>(), isTrue); - intercepted = _Intercepted(response.body as BodyType); - - return response; - }, - ], - services: [ - HttpTestService.create(), - ], - ); - - await chopper.getService().listString(); - - expect(intercepted, isA<_Intercepted>>()); - }); - test('headers', () async { final client = MockClient((http.Request req) async { expect(req.headers.containsKey('foo'), isTrue); @@ -195,7 +103,7 @@ void main() { final curl = CurlInterceptor(); var log = ''; chopperLogger.onRecord.listen((r) => log = r.message); - await curl.onRequest(fakeRequest); + await curl.intercept(FakeChain(fakeRequest)); expect( log, @@ -224,7 +132,7 @@ void main() { final curl = CurlInterceptor(); var log = ''; chopperLogger.onRecord.listen((r) => log = r.message); - await curl.onRequest(fakeRequestMultipart); + await curl.intercept(FakeChain(fakeRequestMultipart)); expect( log, @@ -236,22 +144,31 @@ void main() { }); } -class ResponseIntercept implements ResponseInterceptor { +class ResponseIntercept implements Interceptor { static dynamic intercepted; @override - FutureOr onResponse(Response response) { + FutureOr> intercept( + Chain chain) async { + final response = await chain.proceed(chain.request); + intercepted = _Intercepted(response.body); return response; } } -class RequestIntercept implements RequestInterceptor { +class RequestIntercept implements Interceptor { @override - FutureOr onRequest(Request request) => request.copyWith( + FutureOr> intercept( + Chain chain) async { + final request = chain.request; + return chain.proceed( + request.copyWith( uri: request.uri.replace(path: '${request.uri}/intercept'), - ); + ), + ); + } } class _Intercepted { diff --git a/chopper/test/json_test.dart b/chopper/test/json_test.dart index 6a8c637f..640ece6d 100644 --- a/chopper/test/json_test.dart +++ b/chopper/test/json_test.dart @@ -1,6 +1,7 @@ import 'dart:convert'; -import 'package:chopper/chopper.dart'; +import 'package:chopper/src/base.dart'; +import 'package:chopper/src/converters.dart'; import 'package:http/http.dart' as http; import 'package:http/testing.dart'; import 'package:test/test.dart'; diff --git a/chopper/test/multipart_test.dart b/chopper/test/multipart_test.dart index 8943a412..34a66b00 100644 --- a/chopper/test/multipart_test.dart +++ b/chopper/test/multipart_test.dart @@ -1,4 +1,7 @@ -import 'package:chopper/chopper.dart'; +import 'package:chopper/src/base.dart'; +import 'package:chopper/src/constants.dart'; +import 'package:chopper/src/converters.dart'; +import 'package:chopper/src/request.dart'; import 'package:http/http.dart' as http; import 'package:http/testing.dart'; import 'package:http_parser/http_parser.dart'; diff --git a/example/bin/main_json_serializable.dart b/example/bin/main_json_serializable.dart index d95b7f24..08ab2639 100644 --- a/example/bin/main_json_serializable.dart +++ b/example/bin/main_json_serializable.dart @@ -32,8 +32,8 @@ main() async { // the generated service MyService.create(), ], - /* ResponseInterceptorFunc | RequestInterceptorFunc | ResponseInterceptor | RequestInterceptor */ - interceptors: [authHeader], + /* Interceptors */ + interceptors: [AuthInterceptor()], ); final myService = chopper.getService(); @@ -57,11 +57,19 @@ main() async { } } -Future authHeader(Request request) async => applyHeader( - request, - 'Authorization', - '42', +class AuthInterceptor implements Interceptor { + @override + FutureOr> intercept( + Chain chain) async { + return chain.proceed( + applyHeader( + chain.request, + 'Authorization', + '42', + ), ); + } +} typedef JsonFactory = T Function(Map json); diff --git a/example/bin/main_json_serializable_squadron_worker_pool.dart b/example/bin/main_json_serializable_squadron_worker_pool.dart index e9564899..f2e268cd 100644 --- a/example/bin/main_json_serializable_squadron_worker_pool.dart +++ b/example/bin/main_json_serializable_squadron_worker_pool.dart @@ -13,7 +13,7 @@ import 'package:http/testing.dart'; import 'package:squadron/squadron.dart'; import 'package:http/http.dart' as http; -import 'main_json_serializable.dart' show authHeader; +import 'main_json_serializable.dart' show AuthInterceptor; typedef JsonFactory = T Function(Map json); @@ -134,8 +134,8 @@ Future main() async { // the generated service MyService.create(), ], - /* ResponseInterceptorFunc | RequestInterceptorFunc | ResponseInterceptor | RequestInterceptor */ - interceptors: [authHeader], + /* Interceptor */ + interceptors: [AuthInterceptor()], ); final myService = chopper.getService(); diff --git a/faq.md b/faq.md index a79ed397..aaf23c00 100644 --- a/faq.md +++ b/faq.md @@ -36,11 +36,20 @@ final chopper = ChopperClient( interceptors: [_addQuery], ); -Request _addQuery(Request req) { - final params = Map.from(req.parameters); - params['key'] = '123'; +class QueryInterceptor implements Interceptor { - return req.copyWith(parameters: params); + @override + FutureOr> intercept(Chain chain) async { + final request = _addQuery(chain.request); + return chain.proceed(request); + } + + Request _addQuery(Request req) { + final params = Map.from(req.parameters); + params['key'] = '123'; + + return req.copyWith(parameters: params); + } } ``` @@ -67,13 +76,17 @@ Future postRequest(@Body() Map data); You may need to change the base URL of your network calls during runtime, for example, if you have to use different servers or routes dynamically in your app in case of a "regular" or a "paid" user. You can store the current server base url in your SharedPreferences (encrypt/decrypt it if needed) and use it in an interceptor like this: ```dart -... -(Request request) async => - SharedPreferences.containsKey('baseUrl') - ? request.copyWith( - baseUri: Uri.parse(SharedPreferences.getString('baseUrl')) - ): request -... +class BaseUrlInterceptor implements Interceptor { + @override + FutureOr> intercept(Chain chain) async { + final request = SharedPreferences.containsKey('baseUrl') + ? chain.request.copyWith( + baseUri: Uri.parse(SharedPreferences.getString('baseUrl'))) + : chain.request; + + return chain.proceed(request); + } +} ``` ## Mock ChopperClient for testing @@ -152,22 +165,35 @@ if the refresh token is not valid anymore, drop the session (and navigate to the Simple code example: ```dart -interceptors: [ - // Auth Interceptor - (Request request) async => applyHeader(request, 'authorization', - SharedPrefs.localStorage.getString(tokenHeader), - override: false), - (Response response) async { +class AuthInterceptor implements Interceptor { + + @override + FutureOr> intercept(Chain chain) async { + final request = applyHeader(chain.request, 'authorization', + SharedPrefs.localStorage.getString(tokenHeader), + override: false); + + final response = await chain.proceed(request); + if (response?.statusCode == 401) { SharedPrefs.localStorage.remove(tokenHeader); // Navigate to some login page or just request new token } + return response; - }, -] + } +} + +... +interceptors: [ + AuthInterceptor(), + // ... other interceptors + ] +... ``` The actual implementation of the algorithm above may vary based on how the backend API - more precisely the login and session handling - of your app looks like. +Breaking out of the authentication flow/inteceptor can be achieved in multiple ways. For example by throwing an exception or by using a service handles navigation. See [interceptor](interceptors.md) for more info. ### Authorized HTTP requests using the special Authenticator interceptor @@ -406,7 +432,7 @@ Future main() async { // the generated service MyService.create(), ], - /* ResponseInterceptorFunc | RequestInterceptorFunc | ResponseInterceptor | RequestInterceptor */ + /* Interceptor */ interceptors: [authHeader], ); diff --git a/interceptors.md b/interceptors.md index b8121023..c6834d7d 100644 --- a/interceptors.md +++ b/interceptors.md @@ -2,38 +2,92 @@ ## **Request** -Implement `RequestInterceptor` class or define function with following signature `FutureOr RequestInterceptorFunc(Request request)` +Implement `Interceptor` class. -Request interceptor are called just before sending request +{% hint style="info" %} +Request interceptor are called just before sending request. +{% endhint %} ```dart -final chopper = ChopperClient( - interceptors: [ - (request) async => request.copyWith(body: {}), - ] -); +class MyRequestInterceptor implements Interceptor { + + MyRequestInterceptor(this.token); + + final String token; + + @override + FutureOr> intercept(Chain chain) async { + final request = applyHeader(chain.request, 'auth_token', 'Bearer $token'); + return chain.proceed(request); + } +} ``` ## **Response** -Implement `ResponseInterceptor` class or define function with following signature `FutureOr ResponseInterceptorFunc(Response response)` +Implement `Interceptor` class. {% hint style="info" %} -Called after successful or failed request +Called after successful or failed request. {% endhint %} ```dart -final chopper = ChopperClient( - interceptors: [ - (Response response) async => response.replace(body: {}), - ] -); +class MyResponseInterceptor implements Interceptor { + MyResponseInterceptor(this._token); + + String _token; + + @override + FutureOr> intercept(Chain chain) async { + final response = await chain.proceed(chain.request); + _token = response.headers['auth_token']; + return response; + } +} ``` -## Builtins +## Breaking out of an interceptor + +In some cases you may run into a case where it's not possible to continue within an interceptor and want to break out/cancel the request. This can be achieved by throwing an exception. +This will not return a response and the request will not be executed. + +>Keep in mind that when throwing an exception you also need to handle/catch the exception in calling code. + +For example if you want to stop the request if the token is expired: -* [CurlInterceptor](https://pub.dev/documentation/chopper/latest/chopper/CurlInterceptor-class.html) -* [HttpLoggingInterceptor](https://pub.dev/documentation/chopper/latest/chopper/HttpLoggingInterceptor-class.html) +```dart +class AuthInterceptor implements Interceptor { + + @override + FutureOr> intercept(Chain chain) async { + final request = applyHeader(chain.request, 'authorization', + SharedPrefs.localStorage.getString(tokenHeader), + override: false); + + final response = await chain.proceed(request); + + if (response?.statusCode == 401) { + // Refreshing fails + final bool isRefreshed = await _refreshToken(); + if(!isRefreshed){ + // Throw a exception to stop the request. + throw Exception('Token expired'); + } + } + + return response; + } +} +``` + +It's not strictly needed to throw an exception in order to break out of the interceptor. +Other construction can also be used depending on how the project is structured. +Another could be calling a service that is injected or providing a callback that handles the state of the app. + +## Builtins +* [CurlInterceptor](https://pub.dev/documentation/chopper/latest/chopper/CurlInterceptor-class.html): Interceptor that prints curl commands for each execute request +* [HeadersInterceptor](https://pub.dev/documentation/chopper/latest/chopper/HeadersInterceptor-class.html): Interceptor that adds headers to each request +* [HttpLoggingInterceptor](https://pub.dev/documentation/chopper/latest/chopper/HttpLoggingInterceptor-class.html): Interceptor that logs request and response data Both the `CurlInterceptor` and `HttpLoggingInterceptor` use the dart [logging package](https://pub.dev/packages/logging). In order to see logging in console the logging package also needs to be added to your project and configured. From d5a8d4ada9155f093d8b46cd48bcb568fedca990 Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Sat, 6 Apr 2024 09:22:22 +0200 Subject: [PATCH 151/171] :twisted_rightwards_arrows: post pre-release branch sync (#599) --- chopper/CHANGELOG.md | 4 ++++ chopper/pubspec.yaml | 18 +++++++++--------- chopper_generator/CHANGELOG.md | 4 ++++ chopper_generator/pubspec.yaml | 22 +++++++++++----------- 4 files changed, 28 insertions(+), 20 deletions(-) diff --git a/chopper/CHANGELOG.md b/chopper/CHANGELOG.md index 59e402fa..e5cfc448 100644 --- a/chopper/CHANGELOG.md +++ b/chopper/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 8.0.0-rc.1 + +- Restructure interceptors ([#547](https://github.com/lejard-h/chopper/pull/547)) + ## 7.4.0 - Use [qs_dart](https://pub.dev/packages/qs_dart) for query string encoding for query string encoding in order to support complex query objects ([#592](https://github.com/lejard-h/chopper/pull/592)) diff --git a/chopper/pubspec.yaml b/chopper/pubspec.yaml index c64405ab..603d75ef 100644 --- a/chopper/pubspec.yaml +++ b/chopper/pubspec.yaml @@ -1,32 +1,32 @@ name: chopper description: Chopper is an http client generator using source_gen, inspired by Retrofit -version: 7.4.0 +version: 8.0.0-rc.1 documentation: https://hadrien-lejard.gitbook.io/chopper repository: https://github.com/lejard-h/chopper environment: - sdk: ">=3.0.0 <4.0.0" + sdk: ^3.0.0 dependencies: equatable: ^2.0.5 http: ^1.1.0 logging: ^1.2.0 meta: ^1.9.1 - qs_dart: ^1.0.3 + qs_dart: ^1.0.4 dev_dependencies: - build_runner: ^2.4.6 - build_test: ^2.2.0 + build_runner: ^2.4.9 + build_test: ^2.2.2 build_verify: ^3.1.0 collection: ^1.18.0 - coverage: ^1.6.3 + coverage: ^1.7.2 data_fixture_dart: ^2.2.0 faker: ^2.1.0 http_parser: ^4.0.2 - lints: ">=2.1.1 <4.0.0" - test: ^1.24.4 + lints: ^3.0.0 + test: ^1.25.2 transparent_image: ^2.0.1 - chopper_generator: ^7.3.0 + chopper_generator: ^8.0.0 dependency_overrides: chopper_generator: diff --git a/chopper_generator/CHANGELOG.md b/chopper_generator/CHANGELOG.md index d281a86c..c211c08e 100644 --- a/chopper_generator/CHANGELOG.md +++ b/chopper_generator/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 8.0.0-rc.1 + +- Restructure interceptors ([#547](https://github.com/lejard-h/chopper/pull/547)) + ## 7.4.0 - Use [qs_dart](https://pub.dev/packages/qs_dart) for query string encoding in order to support complex query objects ([#592](https://github.com/lejard-h/chopper/pull/592)) diff --git a/chopper_generator/pubspec.yaml b/chopper_generator/pubspec.yaml index 22dca592..51cbd05a 100644 --- a/chopper_generator/pubspec.yaml +++ b/chopper_generator/pubspec.yaml @@ -1,32 +1,32 @@ name: chopper_generator description: Chopper is an http client generator using source_gen, inspired by Retrofit -version: 7.4.0 +version: 8.0.0-rc.1 documentation: https://hadrien-lejard.gitbook.io/chopper repository: https://github.com/lejard-h/chopper environment: - sdk: ">=3.0.0 <4.0.0" + sdk: ^3.0.0 dependencies: - analyzer: ">=5.13.0 <7.0.0" + analyzer: ^6.4.1 build: ^2.4.1 built_collection: ^5.1.1 - chopper: ^7.4.0 - code_builder: ^4.5.0 - dart_style: ^2.3.2 + chopper: ^8.0.0 + code_builder: ^4.10.0 + dart_style: ^2.3.6 logging: ^1.2.0 meta: ^1.9.1 - source_gen: ^1.4.0 + source_gen: ^1.5.0 yaml: ^3.1.2 - qs_dart: ^1.0.3 + qs_dart: ^1.0.4 collection: ^1.18.0 dev_dependencies: - build_runner: ^2.4.6 + build_runner: ^2.4.9 build_verify: ^3.1.0 http: ^1.1.0 - lints: ">=2.1.1 <4.0.0" - test: ^1.24.4 + lints: ^3.0.0 + test: ^1.25.2 dependency_overrides: chopper: From 4fe8ccfc10b3758485d0406429a8ec20a97058ec Mon Sep 17 00:00:00 2001 From: Job Guldemeester Date: Sat, 6 Apr 2024 22:44:07 +0200 Subject: [PATCH 152/171] :white_check_mark: Added test to see if exception are passed up (#601) --- .../test/chain/interceptor_chain_test.dart | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/chopper/test/chain/interceptor_chain_test.dart b/chopper/test/chain/interceptor_chain_test.dart index c7dd28b5..fa3502ed 100644 --- a/chopper/test/chain/interceptor_chain_test.dart +++ b/chopper/test/chain/interceptor_chain_test.dart @@ -4,6 +4,8 @@ import 'package:chopper/src/chain/chain.dart'; import 'package:chopper/src/chain/interceptor_chain.dart'; import 'package:chopper/src/interceptors/interceptor.dart'; import 'package:chopper/src/interceptors/internal_interceptor.dart'; +import 'package:chopper/src/interceptors/request_converter_interceptor.dart'; +import 'package:chopper/src/interceptors/response_converter_interceptor.dart'; import 'package:chopper/src/request.dart'; import 'package:chopper/src/response.dart'; import 'package:http/http.dart' as http; @@ -153,6 +155,50 @@ void main() { ); }); }); + + group('Chain exception tests', () { + late Request mockRequest; + late InterceptorChain interceptorChain; + setUp(() { + mockRequest = Request( + 'GET', + Uri.parse('bar'), + Uri.parse('http://localhost'), + body: 'Test', + ); + }); + + test('Exception thrown inside the interceptor chain will be passed up', + () async { + interceptorChain = InterceptorChain( + interceptors: [ + RequestConverterInterceptor(null, null), + PassthroughInterceptor(), + PassthroughInterceptor(), + ResponseConverterInterceptor( + converter: null, + errorConverter: null, + responseConverter: null, + ), + ExceptionThrowingInterceptor(), + ], + request: mockRequest, + ); + + expect( + () => interceptorChain.proceed(mockRequest), + throwsA(isA().having( + (e) => e.toString(), 'message', 'Exception: Test exception')), + ); + }); + }); +} + +class ExceptionThrowingInterceptor implements Interceptor { + @override + FutureOr> intercept(Chain chain) { + throw Exception('Test exception'); + } } class RequestModifierInterceptor implements Interceptor { From c558b3b21793cb225f123936441302f140106948 Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Thu, 2 May 2024 09:09:44 +0100 Subject: [PATCH 153/171] :sparkles: add per-request timeout (#604) --- chopper/lib/src/annotations.dart | 11 ++++++ chopper/test/test_service.chopper.dart | 39 +++++++++++++++++++ chopper/test/test_service.dart | 9 +++++ chopper_generator/lib/src/generator.dart | 11 +++++- chopper_generator/lib/src/utils.dart | 15 +++++++ .../test/test_service.chopper.dart | 39 +++++++++++++++++++ chopper_generator/test/test_service.dart | 9 +++++ 7 files changed, 132 insertions(+), 1 deletion(-) diff --git a/chopper/lib/src/annotations.dart b/chopper/lib/src/annotations.dart index 1f10db7f..7c205346 100644 --- a/chopper/lib/src/annotations.dart +++ b/chopper/lib/src/annotations.dart @@ -235,6 +235,9 @@ sealed class Method { /// The above code produces hxxp://path/to/script&foo=foo_var&bar=&baz=baz_var final bool? includeNullQueryVars; + /// Set a timeout for the request + final Duration? timeout; + /// {@macro Method} const Method( this.method, { @@ -244,6 +247,7 @@ sealed class Method { this.listFormat, @Deprecated('Use listFormat instead') this.useBrackets, this.includeNullQueryVars, + this.timeout, }); } @@ -261,6 +265,7 @@ final class Get extends Method { super.listFormat, super.useBrackets, super.includeNullQueryVars, + super.timeout, }) : super(HttpMethod.Get); } @@ -280,6 +285,7 @@ final class Post extends Method { super.listFormat, super.useBrackets, super.includeNullQueryVars, + super.timeout, }) : super(HttpMethod.Post); } @@ -297,6 +303,7 @@ final class Delete extends Method { super.listFormat, super.useBrackets, super.includeNullQueryVars, + super.timeout, }) : super(HttpMethod.Delete); } @@ -316,6 +323,7 @@ final class Put extends Method { super.listFormat, super.useBrackets, super.includeNullQueryVars, + super.timeout, }) : super(HttpMethod.Put); } @@ -334,6 +342,7 @@ final class Patch extends Method { super.listFormat, super.useBrackets, super.includeNullQueryVars, + super.timeout, }) : super(HttpMethod.Patch); } @@ -351,6 +360,7 @@ final class Head extends Method { super.listFormat, super.useBrackets, super.includeNullQueryVars, + super.timeout, }) : super(HttpMethod.Head); } @@ -368,6 +378,7 @@ final class Options extends Method { super.listFormat, super.useBrackets, super.includeNullQueryVars, + super.timeout, }) : super(HttpMethod.Options); } diff --git a/chopper/test/test_service.chopper.dart b/chopper/test/test_service.chopper.dart index 28dbbb81..6d3f5585 100644 --- a/chopper/test/test_service.chopper.dart +++ b/chopper/test/test_service.chopper.dart @@ -820,4 +820,43 @@ final class _$HttpTestService extends HttpTestService { requestConverter: FormUrlEncodedConverter.requestFactory, ); } + + @override + Future> getTimeoutTest() { + final Uri $url = Uri.parse('/test/get_timeout'); + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + ); + return client + .send($request) + .timeout(const Duration(microseconds: 42000000)); + } + + @override + Future> getTimeoutTestZero() { + final Uri $url = Uri.parse('/test/get_timeout_zero'); + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + ); + return client + .send($request) + .timeout(const Duration(microseconds: 0)); + } + + @override + Future> getTimeoutTestNeg() { + final Uri $url = Uri.parse('/test/get_timeout_neg'); + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + ); + return client + .send($request) + .timeout(const Duration(microseconds: 0)); + } } diff --git a/chopper/test/test_service.dart b/chopper/test/test_service.dart index cca87ca6..fc277049 100644 --- a/chopper/test/test_service.dart +++ b/chopper/test/test_service.dart @@ -253,6 +253,15 @@ abstract class HttpTestService extends ChopperService { @Field() final List positives, [ @Field() final String? signature, ]); + + @Get(path: 'get_timeout', timeout: Duration(seconds: 42)) + Future> getTimeoutTest(); + + @Get(path: 'get_timeout_zero', timeout: Duration(seconds: 0)) + Future> getTimeoutTestZero(); + + @Get(path: 'get_timeout_neg', timeout: Duration(seconds: -1)) + Future> getTimeoutTestNeg(); } Request customConvertRequest(Request req) { diff --git a/chopper_generator/lib/src/generator.dart b/chopper_generator/lib/src/generator.dart index 7c1b4d80..774d8f13 100644 --- a/chopper_generator/lib/src/generator.dart +++ b/chopper_generator/lib/src/generator.dart @@ -410,6 +410,8 @@ final class ChopperGenerator final bool? includeNullQueryVars = Utils.getIncludeNullQueryVars(method); + final Duration? timeout = Utils.getTimeout(method); + blocks.add( declareFinal(Vars.request.toString(), type: refer('Request')) .assign( @@ -454,12 +456,19 @@ final class ChopperGenerator ]); } - final returnStatement = + Expression returnStatement = refer(Vars.client.toString()).property('send').call( [refer(Vars.request.toString())], namedArguments, typeArguments, ); + if (timeout != null) { + returnStatement = returnStatement.property('timeout').call([ + refer('Duration').constInstance([], { + 'microseconds': literalNum(timeout.inMicroseconds), + }), + ]); + } if (isResponseObject) { // Return the response object directly from chopper.send diff --git a/chopper_generator/lib/src/utils.dart b/chopper_generator/lib/src/utils.dart index 1e5f9915..f04ba058 100644 --- a/chopper_generator/lib/src/utils.dart +++ b/chopper_generator/lib/src/utils.dart @@ -1,3 +1,5 @@ +import 'dart:math' show max; + import 'package:analyzer/dart/element/element.dart'; import 'package:chopper_generator/src/extensions.dart'; import 'package:code_builder/code_builder.dart'; @@ -33,6 +35,19 @@ final class Utils { static bool? getIncludeNullQueryVars(ConstantReader method) => method.peek('includeNullQueryVars')?.boolValue; + static Duration? getTimeout(ConstantReader method) { + final ConstantReader? timeout = method.peek('timeout'); + if (timeout != null) { + final int? microseconds = + timeout.objectValue.getField('_duration')?.toIntValue(); + if (microseconds != null) { + return Duration(microseconds: max(microseconds, 0)); + } + } + + return null; + } + /// All positional required params must support nullability static Parameter buildRequiredPositionalParam(ParameterElement p) => Parameter( diff --git a/chopper_generator/test/test_service.chopper.dart b/chopper_generator/test/test_service.chopper.dart index ae0052ea..864baf78 100644 --- a/chopper_generator/test/test_service.chopper.dart +++ b/chopper_generator/test/test_service.chopper.dart @@ -919,4 +919,43 @@ final class _$HttpTestService extends HttpTestService { ); return client.send($request); } + + @override + Future> getTimeoutTest() { + final Uri $url = Uri.parse('/test/get_timeout'); + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + ); + return client + .send($request) + .timeout(const Duration(microseconds: 42000000)); + } + + @override + Future> getTimeoutTestZero() { + final Uri $url = Uri.parse('/test/get_timeout_zero'); + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + ); + return client + .send($request) + .timeout(const Duration(microseconds: 0)); + } + + @override + Future> getTimeoutTestNeg() { + final Uri $url = Uri.parse('/test/get_timeout_neg'); + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + ); + return client + .send($request) + .timeout(const Duration(microseconds: 0)); + } } diff --git a/chopper_generator/test/test_service.dart b/chopper_generator/test/test_service.dart index 9ee8b877..30b04f3c 100644 --- a/chopper_generator/test/test_service.dart +++ b/chopper_generator/test/test_service.dart @@ -279,6 +279,15 @@ abstract class HttpTestService extends ChopperService { @Field('fool') final String foo, @Tag() Object? t1, ); + + @Get(path: 'get_timeout', timeout: Duration(seconds: 42)) + Future> getTimeoutTest(); + + @Get(path: 'get_timeout_zero', timeout: Duration(seconds: 0)) + Future> getTimeoutTestZero(); + + @Get(path: 'get_timeout_neg', timeout: Duration(seconds: -1)) + Future> getTimeoutTestNeg(); } Request customConvertRequest(Request req) { From 707950bbb3379ea2332b040815b9205c3184c40e Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Fri, 3 May 2024 07:17:22 +0100 Subject: [PATCH 154/171] :green_heart: fix codecov (#607) --- .github/workflows/dart.yml | 7 ++++--- mono_repo.yaml | 4 +++- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/.github/workflows/dart.yml b/.github/workflows/dart.yml index e2661e66..0e97fbb4 100644 --- a/.github/workflows/dart.yml +++ b/.github/workflows/dart.yml @@ -14,6 +14,7 @@ defaults: shell: bash env: PUB_ENVIRONMENT: bot.github + CODECOV_TOKEN: "${{ secrets.CODECOV_TOKEN }}" permissions: read-all jobs: @@ -133,7 +134,7 @@ jobs: if: "always() && steps.chopper_pub_upgrade.conclusion == 'success'" working-directory: chopper - name: Upload coverage to codecov.io - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v4 with: files: chopper/coverage/lcov.info fail_ci_if_error: true @@ -148,7 +149,7 @@ jobs: if: "always() && steps.chopper_built_value_pub_upgrade.conclusion == 'success'" working-directory: chopper_built_value - name: Upload coverage to codecov.io - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v4 with: files: chopper_built_value/coverage/lcov.info fail_ci_if_error: true @@ -163,7 +164,7 @@ jobs: if: "always() && steps.chopper_generator_pub_upgrade.conclusion == 'success'" working-directory: chopper_generator - name: Upload coverage to codecov.io - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v4 with: files: chopper_generator/coverage/lcov.info fail_ci_if_error: true diff --git a/mono_repo.yaml b/mono_repo.yaml index 5f0226ff..7fe0e718 100644 --- a/mono_repo.yaml +++ b/mono_repo.yaml @@ -10,10 +10,12 @@ github: branches: - master - develop + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} merge_stages: - analyze_and_format - unit_test coverage_service: - - codecov \ No newline at end of file + - codecov From f5a56b6e37ac28ee75c7ec758368e6a61ca94c00 Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Fri, 3 May 2024 07:55:00 +0100 Subject: [PATCH 155/171] :twisted_rightwards_arrows: post pre-release branch sync (#608) --- chopper/CHANGELOG.md | 4 ++++ chopper/pubspec.yaml | 8 ++++---- chopper_built_value/CHANGELOG.md | 4 ++++ chopper_built_value/pubspec.yaml | 20 ++++++++++---------- chopper_generator/CHANGELOG.md | 4 ++++ chopper_generator/pubspec.yaml | 8 ++++---- 6 files changed, 30 insertions(+), 18 deletions(-) diff --git a/chopper/CHANGELOG.md b/chopper/CHANGELOG.md index e5cfc448..ff514d1b 100644 --- a/chopper/CHANGELOG.md +++ b/chopper/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 8.0.0-rc.2 + +- Add per-request timeout ([#604](https://github.com/lejard-h/chopper/pull/604)) + ## 8.0.0-rc.1 - Restructure interceptors ([#547](https://github.com/lejard-h/chopper/pull/547)) diff --git a/chopper/pubspec.yaml b/chopper/pubspec.yaml index 603d75ef..77e41808 100644 --- a/chopper/pubspec.yaml +++ b/chopper/pubspec.yaml @@ -1,6 +1,6 @@ name: chopper description: Chopper is an http client generator using source_gen, inspired by Retrofit -version: 8.0.0-rc.1 +version: 8.0.0-rc.2 documentation: https://hadrien-lejard.gitbook.io/chopper repository: https://github.com/lejard-h/chopper @@ -12,7 +12,7 @@ dependencies: http: ^1.1.0 logging: ^1.2.0 meta: ^1.9.1 - qs_dart: ^1.0.4 + qs_dart: ^1.0.10 dev_dependencies: build_runner: ^2.4.9 @@ -24,9 +24,9 @@ dev_dependencies: faker: ^2.1.0 http_parser: ^4.0.2 lints: ^3.0.0 - test: ^1.25.2 + test: ^1.25.4 transparent_image: ^2.0.1 - chopper_generator: ^8.0.0 + chopper_generator: ">=8.0.0-rc.1 <9.0.0" # Will be replaced with ^8.0.0 once released dependency_overrides: chopper_generator: diff --git a/chopper_built_value/CHANGELOG.md b/chopper_built_value/CHANGELOG.md index e32d5782..5a05485d 100644 --- a/chopper_built_value/CHANGELOG.md +++ b/chopper_built_value/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 3.0.0-rc.1 + +- Require Chopper ^8.0.0 + ## 2.0.1+2 - Fix Github release workflow permissions ([#512](https://github.com/lejard-h/chopper/pull/512)) diff --git a/chopper_built_value/pubspec.yaml b/chopper_built_value/pubspec.yaml index f5b48036..6c8a60ba 100644 --- a/chopper_built_value/pubspec.yaml +++ b/chopper_built_value/pubspec.yaml @@ -1,24 +1,24 @@ name: chopper_built_value description: A built_value based Converter for Chopper. -version: 2.0.1+2 +version: 3.0.0-rc.1 documentation: https://hadrien-lejard.gitbook.io/chopper/converters/built-value-converter repository: https://github.com/lejard-h/chopper environment: - sdk: ">=3.0.0 <4.0.0" + sdk: ^3.0.0 dependencies: - built_value: ^8.6.1 + built_value: ^8.9.2 built_collection: ^5.1.1 - chopper: ^7.0.0 + chopper: ">=8.0.0-rc.1 <9.0.0" http: ^1.1.0 dev_dependencies: - test: ^1.24.4 - build_runner: ^2.4.6 - build_test: ^2.2.0 - built_value_generator: ^8.6.1 - lints: ">=2.1.1 <4.0.0" + test: ^1.25.4 + build_runner: ^2.4.9 + build_test: ^2.2.2 + built_value_generator: ^8.9.2 + lints: ^3.0.0 dependency_overrides: chopper: @@ -27,4 +27,4 @@ dependency_overrides: topics: - codegen - converter - - built-value \ No newline at end of file + - built-value diff --git a/chopper_generator/CHANGELOG.md b/chopper_generator/CHANGELOG.md index c211c08e..43d7a709 100644 --- a/chopper_generator/CHANGELOG.md +++ b/chopper_generator/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 8.0.0-rc.2 + +- Add per-request timeout ([#604](https://github.com/lejard-h/chopper/pull/604)) + ## 8.0.0-rc.1 - Restructure interceptors ([#547](https://github.com/lejard-h/chopper/pull/547)) diff --git a/chopper_generator/pubspec.yaml b/chopper_generator/pubspec.yaml index 51cbd05a..394c6a30 100644 --- a/chopper_generator/pubspec.yaml +++ b/chopper_generator/pubspec.yaml @@ -1,6 +1,6 @@ name: chopper_generator description: Chopper is an http client generator using source_gen, inspired by Retrofit -version: 8.0.0-rc.1 +version: 8.0.0-rc.2 documentation: https://hadrien-lejard.gitbook.io/chopper repository: https://github.com/lejard-h/chopper @@ -11,14 +11,14 @@ dependencies: analyzer: ^6.4.1 build: ^2.4.1 built_collection: ^5.1.1 - chopper: ^8.0.0 + chopper: ">=8.0.0-rc.1 <9.0.0" # Will be replaced with ^8.0.0 once released code_builder: ^4.10.0 dart_style: ^2.3.6 logging: ^1.2.0 meta: ^1.9.1 source_gen: ^1.5.0 yaml: ^3.1.2 - qs_dart: ^1.0.4 + qs_dart: ^1.0.10 collection: ^1.18.0 dev_dependencies: @@ -26,7 +26,7 @@ dev_dependencies: build_verify: ^3.1.0 http: ^1.1.0 lints: ^3.0.0 - test: ^1.25.2 + test: ^1.25.4 dependency_overrides: chopper: From a7293f47224eaea4369280b1f522c0f5520ca07f Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Wed, 8 May 2024 18:27:03 +0100 Subject: [PATCH 156/171] :sparkles: add `onlyErrors` option to `HttpLoggingInterceptor` (#610) --- .../http_logging_interceptor.dart | 121 ++- .../test/http_logging_interceptor_test.dart | 910 +++++++++++++----- 2 files changed, 723 insertions(+), 308 deletions(-) diff --git a/chopper/lib/src/interceptors/http_logging_interceptor.dart b/chopper/lib/src/interceptors/http_logging_interceptor.dart index 95bb6b2c..d8f5ee0f 100644 --- a/chopper/lib/src/interceptors/http_logging_interceptor.dart +++ b/chopper/lib/src/interceptors/http_logging_interceptor.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'package:chopper/src/chain/chain.dart'; import 'package:chopper/src/chopper_log_record.dart'; import 'package:chopper/src/interceptors/interceptor.dart'; +import 'package:chopper/src/request.dart'; import 'package:chopper/src/response.dart'; import 'package:chopper/src/utils.dart'; import 'package:http/http.dart' as http; @@ -75,12 +76,16 @@ enum Level { @immutable class HttpLoggingInterceptor implements Interceptor { /// {@macro http_logging_interceptor} - HttpLoggingInterceptor({this.level = Level.body, Logger? logger}) - : _logger = logger ?? chopperLogger, + HttpLoggingInterceptor({ + this.level = Level.body, + this.onlyErrors = false, + Logger? logger, + }) : _logger = logger ?? chopperLogger, _logBody = level == Level.body, _logHeaders = level == Level.body || level == Level.headers; final Level level; + final bool onlyErrors; final Logger _logger; final bool _logBody; final bool _logHeaders; @@ -88,103 +93,131 @@ class HttpLoggingInterceptor implements Interceptor { @override FutureOr> intercept( Chain chain) async { - final request = chain.request; - if (level == Level.none) return chain.proceed(request); + final Request request = chain.request; + + final Stopwatch stopWatch = Stopwatch()..start(); + + final Response response = await chain.proceed(request); + + stopWatch.stop(); + + if (level == Level.none || (onlyErrors && response.statusCode < 400)) { + return response; + } + final http.BaseRequest baseRequest = await request.toBaseRequest(); - String startRequestMessage = - '--> ${baseRequest.method} ${baseRequest.url.toString()}'; - String bodyRequestMessage = ''; + final StringBuffer startRequestMessage = StringBuffer( + '--> ${baseRequest.method} ${baseRequest.url.toString()}', + ); + final StringBuffer bodyRequestMessage = StringBuffer(); if (baseRequest is http.Request) { if (baseRequest.body.isNotEmpty) { - bodyRequestMessage = baseRequest.body; + bodyRequestMessage.write(baseRequest.body); if (!_logHeaders) { - startRequestMessage += ' (${baseRequest.bodyBytes.length}-byte body)'; + startRequestMessage.write( + ' (${baseRequest.bodyBytes.length}-byte body)', + ); } } } // Always start on a new line _logger.info(ChopperLogRecord('', request: request)); - _logger.info(ChopperLogRecord(startRequestMessage, request: request)); + _logger.info( + ChopperLogRecord(startRequestMessage.toString(), request: request), + ); if (_logHeaders) { baseRequest.headers.forEach( - (k, v) => _logger.info(ChopperLogRecord('$k: $v', request: request)), + (String k, String v) => _logger.info( + ChopperLogRecord('$k: $v', request: request), + ), ); if (baseRequest.contentLength != null && baseRequest.headers['content-length'] == null) { - _logger.info(ChopperLogRecord( - 'content-length: ${baseRequest.contentLength}', - request: request, - )); + _logger.info( + ChopperLogRecord( + 'content-length: ${baseRequest.contentLength}', + request: request, + ), + ); } } if (_logBody && bodyRequestMessage.isNotEmpty) { _logger.info(ChopperLogRecord('', request: request)); - _logger.info(ChopperLogRecord(bodyRequestMessage, request: request)); + _logger.info( + ChopperLogRecord(bodyRequestMessage.toString(), request: request), + ); } if (_logHeaders || _logBody) { - _logger.info(ChopperLogRecord( - '--> END ${baseRequest.method}', - request: request, - )); + _logger.info( + ChopperLogRecord('--> END ${baseRequest.method}', request: request), + ); } - final stopWatch = Stopwatch()..start(); - - final response = await chain.proceed(request); - - stopWatch.stop(); - if (level == Level.none) return response; - final baseResponse = response.base; + final http.BaseResponse baseResponse = response.base; - String bytes = ''; - String reasonPhrase = response.statusCode.toString(); - String bodyResponseMessage = ''; + final StringBuffer bytes = StringBuffer(); + final StringBuffer reasonPhrase = StringBuffer( + response.statusCode.toString(), + ); + final StringBuffer bodyResponseMessage = StringBuffer(); if (baseResponse is http.Response) { if (baseResponse.reasonPhrase != null) { - reasonPhrase += - ' ${baseResponse.reasonPhrase != reasonPhrase ? baseResponse.reasonPhrase : ''}'; + if (baseResponse.reasonPhrase != reasonPhrase.toString()) { + reasonPhrase.write(' ${baseResponse.reasonPhrase}'); + } } if (baseResponse.body.isNotEmpty) { - bodyResponseMessage = baseResponse.body; + bodyResponseMessage.write(baseResponse.body); if (!_logBody && !_logHeaders) { - bytes = ', ${response.bodyBytes.length}-byte body'; + bytes.write(', ${response.bodyBytes.length}-byte body'); } } } // Always start on a new line _logger.info(ChopperLogRecord('', response: response)); - _logger.info(ChopperLogRecord( - '<-- $reasonPhrase ${baseResponse.request?.method} ${baseResponse.request?.url.toString()} (${stopWatch.elapsedMilliseconds}ms$bytes)', - response: response, - )); + _logger.info( + ChopperLogRecord( + '<-- $reasonPhrase ${baseResponse.request?.method} ${baseResponse.request?.url.toString()} (${stopWatch.elapsedMilliseconds}ms$bytes)', + response: response, + ), + ); if (_logHeaders) { baseResponse.headers.forEach( - (k, v) => _logger.info(ChopperLogRecord('$k: $v', response: response)), + (String k, String v) => _logger.info( + ChopperLogRecord('$k: $v', response: response), + ), ); if (baseResponse.contentLength != null && baseResponse.headers['content-length'] == null) { - _logger.info(ChopperLogRecord( - 'content-length: ${baseResponse.contentLength}', - response: response, - )); + _logger.info( + ChopperLogRecord( + 'content-length: ${baseResponse.contentLength}', + response: response, + ), + ); } } if (_logBody && bodyResponseMessage.isNotEmpty) { _logger.info(ChopperLogRecord('', response: response)); - _logger.info(ChopperLogRecord(bodyResponseMessage, response: response)); + _logger.info( + ChopperLogRecord( + bodyResponseMessage.toString(), + response: response, + ), + ); } if (_logBody || _logHeaders) { diff --git a/chopper/test/http_logging_interceptor_test.dart b/chopper/test/http_logging_interceptor_test.dart index b2ba8aaa..22dc5799 100644 --- a/chopper/test/http_logging_interceptor_test.dart +++ b/chopper/test/http_logging_interceptor_test.dart @@ -8,7 +8,7 @@ import 'package:test/test.dart'; import 'helpers/fake_chain.dart'; void main() { - final fakeRequest = Request( + final Request fakeRequest = Request( 'POST', Uri.parse('/'), Uri.parse('base'), @@ -16,289 +16,671 @@ void main() { headers: {'foo': 'bar'}, ); - group('http logging requests', () { - test('Http logger interceptor none level request', () async { - final logger = HttpLoggingInterceptor(level: Level.none); - - final logs = []; - chopperLogger.onRecord.listen((r) => logs.add(r.message)); - await logger.intercept(FakeChain(fakeRequest)); - - expect( - logs, - equals( - [], - ), - ); + group('standard', () { + group('http logging requests', () { + test('Http logger interceptor none level request', () async { + final logger = HttpLoggingInterceptor(level: Level.none); + + final logs = []; + chopperLogger.onRecord.listen((r) => logs.add(r.message)); + await logger.intercept(FakeChain(fakeRequest)); + + expect( + logs, + equals( + [], + ), + ); + }); + + test('Http logger interceptor basic level request', () async { + final logger = HttpLoggingInterceptor(level: Level.basic); + + final logs = []; + chopperLogger.onRecord.listen((r) => logs.add(r.message)); + await logger.intercept(FakeChain(fakeRequest)); + + expect( + logs, + containsAll( + [ + '', + '--> POST base/ (4-byte body)', + ], + ), + ); + }); + + test('Http logger interceptor basic level request', () async { + final logger = HttpLoggingInterceptor(level: Level.headers); + + final logs = []; + chopperLogger.onRecord.listen((r) => logs.add(r.message)); + await logger.intercept(FakeChain(fakeRequest)); + + expect( + logs, + containsAll( + [ + '', + '--> POST base/', + 'foo: bar', + 'content-type: text/plain; charset=utf-8', + 'content-length: 4', + '--> END POST', + ], + ), + ); + }); + + test('Http logger interceptor body level request', () async { + final logger = HttpLoggingInterceptor(level: Level.body); + + final logs = []; + chopperLogger.onRecord.listen((r) => logs.add(r.message)); + await logger.intercept(FakeChain(fakeRequest)); + + expect( + logs, + containsAll( + [ + '', + '--> POST base/', + 'foo: bar', + 'content-type: text/plain; charset=utf-8', + 'content-length: 4', + '', + 'test', + '--> END POST', + ], + ), + ); + }); }); - test('Http logger interceptor basic level request', () async { - final logger = HttpLoggingInterceptor(level: Level.basic); - - final logs = []; - chopperLogger.onRecord.listen((r) => logs.add(r.message)); - await logger.intercept(FakeChain(fakeRequest)); - - expect( - logs, - containsAll( - [ - '', - '--> POST base/ (4-byte body)', - ], - ), - ); - }); + group('http logging interceptor response logging', () { + late Response fakeResponse; - test('Http logger interceptor basic level request', () async { - final logger = HttpLoggingInterceptor(level: Level.headers); - - final logs = []; - chopperLogger.onRecord.listen((r) => logs.add(r.message)); - await logger.intercept(FakeChain(fakeRequest)); - - expect( - logs, - containsAll( - [ - '', - '--> POST base/', - 'foo: bar', - 'content-type: text/plain; charset=utf-8', - 'content-length: 4', - '--> END POST', - ], - ), - ); + setUp(() async { + fakeResponse = Response( + http.Response( + 'responseBodyBase', + 200, + headers: {'foo': 'bar'}, + request: await fakeRequest.toBaseRequest(), + ), + 'responseBody', + ); + }); + + test('Http logger interceptor none level response', () async { + final logger = HttpLoggingInterceptor(level: Level.none); + + final logs = []; + chopperLogger.onRecord.listen((r) => logs.add(r.message)); + await logger.intercept(FakeChain(fakeRequest)); + + expect( + logs, + equals( + [], + ), + ); + }); + + test('Http logger interceptor basic level response', () async { + final logger = HttpLoggingInterceptor(level: Level.basic); + + final logs = []; + chopperLogger.onRecord.listen((r) => logs.add(r.message)); + await logger.intercept(FakeChain(fakeRequest, response: fakeResponse)); + + expect( + logs, + containsAll( + [ + '', + '<-- 200 POST base/ (0ms, 16-byte body)', + ], + ), + ); + }); + + test('Http logger interceptor headers level response', () async { + final logger = HttpLoggingInterceptor(level: Level.headers); + + final logs = []; + chopperLogger.onRecord.listen((r) => logs.add(r.message)); + await logger.intercept(FakeChain(fakeRequest, response: fakeResponse)); + + expect( + logs, + containsAll( + [ + '', + '<-- 200 POST base/ (0ms)', + 'foo: bar', + 'content-length: 16', + '<-- END HTTP', + ], + ), + ); + }); + + test('Http logger interceptor body level response', () async { + final logger = HttpLoggingInterceptor(level: Level.body); + + final logs = []; + chopperLogger.onRecord.listen((r) => logs.add(r.message)); + await logger.intercept(FakeChain(fakeRequest, response: fakeResponse)); + + expect( + logs, + containsAll( + [ + '', + '<-- 200 POST base/ (0ms)', + 'foo: bar', + 'content-length: 16', + '', + 'responseBodyBase', + '<-- END HTTP', + ], + ), + ); + }); }); - test('Http logger interceptor body level request', () async { - final logger = HttpLoggingInterceptor(level: Level.body); - - final logs = []; - chopperLogger.onRecord.listen((r) => logs.add(r.message)); - await logger.intercept(FakeChain(fakeRequest)); - - expect( - logs, - containsAll( - [ - '', - '--> POST base/', - 'foo: bar', - 'content-type: text/plain; charset=utf-8', - 'content-length: 4', - '', - 'test', - '--> END POST', - ], - ), - ); + group('headers content-length not overridden', () { + late Response fakeResponse; + + setUp(() async { + fakeResponse = Response( + http.Response( + 'responseBodyBase', + 200, + headers: { + 'foo': 'bar', + 'content-length': '42', + }, + request: await fakeRequest.toBaseRequest(), + ), + 'responseBody', + ); + }); + + test('request header level content-length', () async { + final logger = HttpLoggingInterceptor(level: Level.headers); + + final logs = []; + chopperLogger.onRecord.listen((r) => logs.add(r.message)); + + await logger.intercept(FakeChain(fakeRequest.copyWith( + headers: {...fakeRequest.headers, 'content-length': '42'}))); + + expect( + logs, + containsAll( + [ + '', + '--> POST base/', + 'foo: bar', + 'content-length: 42', + 'content-type: text/plain; charset=utf-8', + '--> END POST', + ], + ), + ); + }); + + test('request body level content-length', () async { + final logger = HttpLoggingInterceptor(level: Level.body); + + final logs = []; + chopperLogger.onRecord.listen((r) => logs.add(r.message)); + + await logger.intercept(FakeChain(fakeRequest.copyWith( + headers: {...fakeRequest.headers, 'content-length': '42'}))); + + expect( + logs, + containsAll( + [ + '', + '--> POST base/', + 'foo: bar', + 'content-length: 42', + 'content-type: text/plain; charset=utf-8', + '', + 'test', + '--> END POST', + ], + ), + ); + }); + + test('response header level content-length', () async { + final logger = HttpLoggingInterceptor(level: Level.headers); + + final logs = []; + chopperLogger.onRecord.listen((r) => logs.add(r.message)); + await logger.intercept(FakeChain(fakeRequest, response: fakeResponse)); + + expect( + logs, + containsAll( + [ + '', + '<-- 200 POST base/ (0ms)', + 'foo: bar', + 'content-length: 42', + '<-- END HTTP', + ], + ), + ); + }); + test('response body level content-length', () async { + final logger = HttpLoggingInterceptor(level: Level.body); + + final logs = []; + chopperLogger.onRecord.listen((r) => logs.add(r.message)); + await logger.intercept(FakeChain(fakeRequest, response: fakeResponse)); + + expect( + logs, + containsAll( + [ + '', + '<-- 200 POST base/ (0ms)', + 'foo: bar', + 'content-length: 42', + '', + 'responseBodyBase', + '<-- END HTTP', + ], + ), + ); + }); }); }); - group('http logging interceptor response logging', () { - late Response fakeResponse; - - setUp(() async { - fakeResponse = Response( - http.Response( - 'responseBodyBase', - 200, - headers: {'foo': 'bar'}, - request: await fakeRequest.toBaseRequest(), - ), - 'responseBody', - ); - }); + group('only errors', () { + group('http logging requests', () { + test('Http logger interceptor none level request', () async { + final logger = + HttpLoggingInterceptor(level: Level.none, onlyErrors: true); - test('Http logger interceptor none level response', () async { - final logger = HttpLoggingInterceptor(level: Level.none); + final logs = []; + chopperLogger.onRecord.listen((r) => logs.add(r.message)); + await logger.intercept(FakeChain(fakeRequest)); - final logs = []; - chopperLogger.onRecord.listen((r) => logs.add(r.message)); - await logger.intercept(FakeChain(fakeRequest)); + expect(logs, equals([])); + }); - expect( - logs, - equals( - [], - ), - ); - }); + test('Http logger interceptor basic level request', () async { + final logger = + HttpLoggingInterceptor(level: Level.basic, onlyErrors: true); - test('Http logger interceptor basic level response', () async { - final logger = HttpLoggingInterceptor(level: Level.basic); - - final logs = []; - chopperLogger.onRecord.listen((r) => logs.add(r.message)); - await logger.intercept(FakeChain(fakeRequest, response: fakeResponse)); - - expect( - logs, - containsAll( - [ - '', - '<-- 200 POST base/ (0ms, 16-byte body)', - ], - ), - ); - }); + final logs = []; + chopperLogger.onRecord.listen((r) => logs.add(r.message)); + await logger.intercept(FakeChain(fakeRequest)); - test('Http logger interceptor headers level response', () async { - final logger = HttpLoggingInterceptor(level: Level.headers); - - final logs = []; - chopperLogger.onRecord.listen((r) => logs.add(r.message)); - await logger.intercept(FakeChain(fakeRequest, response: fakeResponse)); - - expect( - logs, - containsAll( - [ - '', - '<-- 200 POST base/ (0ms)', - 'foo: bar', - 'content-length: 16', - '<-- END HTTP', - ], - ), - ); - }); + expect(logs, equals([])); + }); - test('Http logger interceptor body level response', () async { - final logger = HttpLoggingInterceptor(level: Level.body); - - final logs = []; - chopperLogger.onRecord.listen((r) => logs.add(r.message)); - await logger.intercept(FakeChain(fakeRequest, response: fakeResponse)); - - expect( - logs, - containsAll( - [ - '', - '<-- 200 POST base/ (0ms)', - 'foo: bar', - 'content-length: 16', - '', - 'responseBodyBase', - '<-- END HTTP', - ], - ), - ); - }); - }); + test('Http logger interceptor basic level request', () async { + final logger = + HttpLoggingInterceptor(level: Level.headers, onlyErrors: true); - group('headers content-length not overridden', () { - late Response fakeResponse; - - setUp(() async { - fakeResponse = Response( - http.Response( - 'responseBodyBase', - 200, - headers: { - 'foo': 'bar', - 'content-length': '42', - }, - request: await fakeRequest.toBaseRequest(), - ), - 'responseBody', - ); - }); + final logs = []; + chopperLogger.onRecord.listen((r) => logs.add(r.message)); + await logger.intercept(FakeChain(fakeRequest)); - test('request header level content-length', () async { - final logger = HttpLoggingInterceptor(level: Level.headers); - - final logs = []; - chopperLogger.onRecord.listen((r) => logs.add(r.message)); - - await logger.intercept(FakeChain(fakeRequest.copyWith( - headers: {...fakeRequest.headers, 'content-length': '42'}))); - - expect( - logs, - containsAll( - [ - '', - '--> POST base/', - 'foo: bar', - 'content-length: 42', - 'content-type: text/plain; charset=utf-8', - '--> END POST', - ], - ), - ); - }); + expect(logs, equals([])); + }); - test('request body level content-length', () async { - final logger = HttpLoggingInterceptor(level: Level.body); - - final logs = []; - chopperLogger.onRecord.listen((r) => logs.add(r.message)); - - await logger.intercept(FakeChain(fakeRequest.copyWith( - headers: {...fakeRequest.headers, 'content-length': '42'}))); - - expect( - logs, - containsAll( - [ - '', - '--> POST base/', - 'foo: bar', - 'content-length: 42', - 'content-type: text/plain; charset=utf-8', - '', - 'test', - '--> END POST', - ], - ), - ); + test('Http logger interceptor body level request', () async { + final logger = + HttpLoggingInterceptor(level: Level.body, onlyErrors: true); + + final logs = []; + chopperLogger.onRecord.listen((r) => logs.add(r.message)); + await logger.intercept(FakeChain(fakeRequest)); + + expect(logs, equals([])); + }); }); - test('response header level content-length', () async { - final logger = HttpLoggingInterceptor(level: Level.headers); - - final logs = []; - chopperLogger.onRecord.listen((r) => logs.add(r.message)); - await logger.intercept(FakeChain(fakeRequest, response: fakeResponse)); - - expect( - logs, - containsAll( - [ - '', - '<-- 200 POST base/ (0ms)', - 'foo: bar', - 'content-length: 42', - '<-- END HTTP', - ], - ), - ); + group('HTTP 200', () { + group('http logging interceptor response logging', () { + late Response fakeResponse; + + setUp(() async { + fakeResponse = Response( + http.Response( + 'responseBodyBase', + 200, + headers: {'foo': 'bar'}, + request: await fakeRequest.toBaseRequest(), + ), + 'responseBody', + ); + }); + + test('Http logger interceptor none level response', () async { + final logger = + HttpLoggingInterceptor(level: Level.none, onlyErrors: true); + + final logs = []; + chopperLogger.onRecord.listen((r) => logs.add(r.message)); + await logger.intercept(FakeChain(fakeRequest)); + + expect( + logs, + equals( + [], + ), + ); + }); + + test('Http logger interceptor basic level response', () async { + final logger = + HttpLoggingInterceptor(level: Level.basic, onlyErrors: true); + + final logs = []; + chopperLogger.onRecord.listen((r) => logs.add(r.message)); + await logger + .intercept(FakeChain(fakeRequest, response: fakeResponse)); + + expect(logs, equals([])); + }); + + test('Http logger interceptor headers level response', () async { + final logger = + HttpLoggingInterceptor(level: Level.headers, onlyErrors: true); + + final logs = []; + chopperLogger.onRecord.listen((r) => logs.add(r.message)); + await logger + .intercept(FakeChain(fakeRequest, response: fakeResponse)); + + expect(logs, equals([])); + }); + + test('Http logger interceptor body level response', () async { + final logger = + HttpLoggingInterceptor(level: Level.body, onlyErrors: true); + + final logs = []; + chopperLogger.onRecord.listen((r) => logs.add(r.message)); + await logger + .intercept(FakeChain(fakeRequest, response: fakeResponse)); + + expect(logs, equals([])); + }); + }); + + group('headers content-length not overridden', () { + late Response fakeResponse; + + setUp(() async { + fakeResponse = Response( + http.Response( + 'responseBodyBase', + 200, + headers: { + 'foo': 'bar', + 'content-length': '42', + }, + request: await fakeRequest.toBaseRequest(), + ), + 'responseBody', + ); + }); + + test('request header level content-length', () async { + final logger = + HttpLoggingInterceptor(level: Level.headers, onlyErrors: true); + + final logs = []; + chopperLogger.onRecord.listen((r) => logs.add(r.message)); + + await logger.intercept(FakeChain(fakeRequest.copyWith( + headers: {...fakeRequest.headers, 'content-length': '42'}))); + + expect(logs, equals([])); + }); + + test('request body level content-length', () async { + final logger = + HttpLoggingInterceptor(level: Level.body, onlyErrors: true); + + final logs = []; + chopperLogger.onRecord.listen((r) => logs.add(r.message)); + + await logger.intercept(FakeChain(fakeRequest.copyWith( + headers: {...fakeRequest.headers, 'content-length': '42'}))); + + expect(logs, equals([])); + }); + + test('response header level content-length', () async { + final logger = + HttpLoggingInterceptor(level: Level.headers, onlyErrors: true); + + final logs = []; + chopperLogger.onRecord.listen((r) => logs.add(r.message)); + await logger + .intercept(FakeChain(fakeRequest, response: fakeResponse)); + + expect(logs, equals([])); + }); + test('response body level content-length', () async { + final logger = + HttpLoggingInterceptor(level: Level.body, onlyErrors: true); + + final logs = []; + chopperLogger.onRecord.listen((r) => logs.add(r.message)); + await logger + .intercept(FakeChain(fakeRequest, response: fakeResponse)); + + expect(logs, equals([])); + }); + }); }); - test('response body level content-length', () async { - final logger = HttpLoggingInterceptor(level: Level.body); - - final logs = []; - chopperLogger.onRecord.listen((r) => logs.add(r.message)); - await logger.intercept(FakeChain(fakeRequest, response: fakeResponse)); - - expect( - logs, - containsAll( - [ - '', - '<-- 200 POST base/ (0ms)', - 'foo: bar', - 'content-length: 42', - '', - 'responseBodyBase', - '<-- END HTTP', - ], - ), - ); + + group('HTTP 400', () { + group('http logging interceptor response logging', () { + late Response fakeResponse; + + setUp(() async { + fakeResponse = Response( + http.Response( + 'responseBodyBase', + 400, + headers: {'foo': 'bar'}, + request: await fakeRequest.toBaseRequest(), + ), + 'responseBody', + ); + }); + + test('Http logger interceptor none level response', () async { + final logger = + HttpLoggingInterceptor(level: Level.none, onlyErrors: true); + + final logs = []; + chopperLogger.onRecord.listen((r) => logs.add(r.message)); + await logger.intercept(FakeChain(fakeRequest)); + + expect( + logs, + equals( + [], + ), + ); + }); + + test('Http logger interceptor basic level response', () async { + final logger = + HttpLoggingInterceptor(level: Level.basic, onlyErrors: true); + + final logs = []; + chopperLogger.onRecord.listen((r) => logs.add(r.message)); + await logger + .intercept(FakeChain(fakeRequest, response: fakeResponse)); + + expect( + logs, + containsAll( + [ + '', + '<-- 400 POST base/ (0ms, 16-byte body)', + ], + ), + ); + }); + + test('Http logger interceptor headers level response', () async { + final logger = + HttpLoggingInterceptor(level: Level.headers, onlyErrors: true); + + final logs = []; + chopperLogger.onRecord.listen((r) => logs.add(r.message)); + await logger + .intercept(FakeChain(fakeRequest, response: fakeResponse)); + + expect( + logs, + containsAll( + [ + '', + '<-- 400 POST base/ (0ms)', + 'foo: bar', + 'content-length: 16', + '<-- END HTTP', + ], + ), + ); + }); + + test('Http logger interceptor body level response', () async { + final logger = + HttpLoggingInterceptor(level: Level.body, onlyErrors: true); + + final logs = []; + chopperLogger.onRecord.listen((r) => logs.add(r.message)); + await logger + .intercept(FakeChain(fakeRequest, response: fakeResponse)); + + expect( + logs, + containsAll( + [ + '', + '<-- 400 POST base/ (0ms)', + 'foo: bar', + 'content-length: 16', + '', + 'responseBodyBase', + '<-- END HTTP', + ], + ), + ); + }); + }); + + group('headers content-length not overridden', () { + late Response fakeResponse; + + setUp(() async { + fakeResponse = Response( + http.Response( + 'responseBodyBase', + 400, + headers: { + 'foo': 'bar', + 'content-length': '42', + }, + request: await fakeRequest.toBaseRequest(), + ), + 'responseBody', + ); + }); + + test('request header level content-length', () async { + final logger = + HttpLoggingInterceptor(level: Level.headers, onlyErrors: true); + + final logs = []; + chopperLogger.onRecord.listen((r) => logs.add(r.message)); + + await logger.intercept(FakeChain(fakeRequest.copyWith( + headers: {...fakeRequest.headers, 'content-length': '42'}))); + + expect(logs, equals([])); + }); + + test('request body level content-length', () async { + final logger = + HttpLoggingInterceptor(level: Level.body, onlyErrors: true); + + final logs = []; + chopperLogger.onRecord.listen((r) => logs.add(r.message)); + + await logger.intercept(FakeChain(fakeRequest.copyWith( + headers: {...fakeRequest.headers, 'content-length': '42'}))); + + expect(logs, equals([])); + }); + + test('response header level content-length', () async { + final logger = + HttpLoggingInterceptor(level: Level.headers, onlyErrors: true); + + final logs = []; + chopperLogger.onRecord.listen((r) => logs.add(r.message)); + await logger + .intercept(FakeChain(fakeRequest, response: fakeResponse)); + + expect( + logs, + containsAll( + [ + '', + '<-- 400 POST base/ (0ms)', + 'foo: bar', + 'content-length: 42', + '<-- END HTTP', + ], + ), + ); + }); + test('response body level content-length', () async { + final logger = + HttpLoggingInterceptor(level: Level.body, onlyErrors: true); + + final logs = []; + chopperLogger.onRecord.listen((r) => logs.add(r.message)); + await logger + .intercept(FakeChain(fakeRequest, response: fakeResponse)); + + expect( + logs, + containsAll( + [ + '', + '<-- 400 POST base/ (0ms)', + 'foo: bar', + 'content-length: 42', + '', + 'responseBodyBase', + '<-- END HTTP', + ], + ), + ); + }); + }); }); }); } From 5e050642fd9a3e6c1d26d676e2f2dc486b2e11a0 Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Thu, 9 May 2024 11:34:29 +0100 Subject: [PATCH 157/171] :twisted_rightwards_arrows: post release branch sync (#611) --- chopper/CHANGELOG.md | 12 ++++++----- chopper/pubspec.yaml | 4 ++-- chopper_built_value/CHANGELOG.md | 2 +- chopper_built_value/pubspec.yaml | 4 ++-- chopper_generator/CHANGELOG.md | 11 +++++----- chopper_generator/pubspec.yaml | 4 ++-- example/lib/built_value_resource.chopper.dart | 3 ++- .../lib/json_decode_service.activator.g.dart | 2 +- example/lib/json_decode_service.vm.g.dart | 2 +- example/lib/json_decode_service.worker.g.dart | 9 +++++---- example/lib/json_serializable.chopper.dart | 3 ++- example/pubspec.yaml | 20 +++++++++---------- 12 files changed, 41 insertions(+), 35 deletions(-) diff --git a/chopper/CHANGELOG.md b/chopper/CHANGELOG.md index ff514d1b..84af67b0 100644 --- a/chopper/CHANGELOG.md +++ b/chopper/CHANGELOG.md @@ -1,12 +1,14 @@ # Changelog -## 8.0.0-rc.2 +## 8.0.0 - Add per-request timeout ([#604](https://github.com/lejard-h/chopper/pull/604)) - -## 8.0.0-rc.1 - -- Restructure interceptors ([#547](https://github.com/lejard-h/chopper/pull/547)) +- **BREAKING CHANGE**: + - Restructure interceptors ([#547](https://github.com/lejard-h/chopper/pull/547)) + - `RequestInterceptor` and Function `RequestInterceptor`s are removed + - `ResponseInterceptor` and Function `ResponseInterceptor`s are removed + - See [Migrating to 8.0.0](https://docs.google.com/document/d/e/2PACX-1vQFoUDisnSJBzzXCMaf53ffUD1Bvpu-1GZ_stzfaaCa0Xd3WKIegbd1mmavEQcMT6r6v8z02UqloKuC/pub) for more information and examples + - add `onlyErrors` option to `HttpLoggingInterceptor` ([#610](https://github.com/lejard-h/chopper/pull/610)) ## 7.4.0 diff --git a/chopper/pubspec.yaml b/chopper/pubspec.yaml index 77e41808..f71cfdbb 100644 --- a/chopper/pubspec.yaml +++ b/chopper/pubspec.yaml @@ -1,6 +1,6 @@ name: chopper description: Chopper is an http client generator using source_gen, inspired by Retrofit -version: 8.0.0-rc.2 +version: 8.0.0 documentation: https://hadrien-lejard.gitbook.io/chopper repository: https://github.com/lejard-h/chopper @@ -26,7 +26,7 @@ dev_dependencies: lints: ^3.0.0 test: ^1.25.4 transparent_image: ^2.0.1 - chopper_generator: ">=8.0.0-rc.1 <9.0.0" # Will be replaced with ^8.0.0 once released + chopper_generator: ">=8.0.0-rc.2 <9.0.0" # will be replaced by ^8.0.0 in the next release dependency_overrides: chopper_generator: diff --git a/chopper_built_value/CHANGELOG.md b/chopper_built_value/CHANGELOG.md index 5a05485d..f0297951 100644 --- a/chopper_built_value/CHANGELOG.md +++ b/chopper_built_value/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## 3.0.0-rc.1 +## 3.0.0 - Require Chopper ^8.0.0 diff --git a/chopper_built_value/pubspec.yaml b/chopper_built_value/pubspec.yaml index 6c8a60ba..d1303271 100644 --- a/chopper_built_value/pubspec.yaml +++ b/chopper_built_value/pubspec.yaml @@ -1,6 +1,6 @@ name: chopper_built_value description: A built_value based Converter for Chopper. -version: 3.0.0-rc.1 +version: 3.0.0 documentation: https://hadrien-lejard.gitbook.io/chopper/converters/built-value-converter repository: https://github.com/lejard-h/chopper @@ -10,7 +10,7 @@ environment: dependencies: built_value: ^8.9.2 built_collection: ^5.1.1 - chopper: ">=8.0.0-rc.1 <9.0.0" + chopper: ^8.0.0 http: ^1.1.0 dev_dependencies: diff --git a/chopper_generator/CHANGELOG.md b/chopper_generator/CHANGELOG.md index 43d7a709..c41e9fe1 100644 --- a/chopper_generator/CHANGELOG.md +++ b/chopper_generator/CHANGELOG.md @@ -1,12 +1,13 @@ # Changelog -## 8.0.0-rc.2 +## 8.0.0 - Add per-request timeout ([#604](https://github.com/lejard-h/chopper/pull/604)) - -## 8.0.0-rc.1 - -- Restructure interceptors ([#547](https://github.com/lejard-h/chopper/pull/547)) +- **BREAKING CHANGE**: + - Restructure interceptors ([#547](https://github.com/lejard-h/chopper/pull/547)) + - `RequestInterceptor` and Function `RequestInterceptor`s are removed + - `ResponseInterceptor` and Function `ResponseInterceptor`s are removed + - See [Migrating to 8.0.0](https://docs.google.com/document/d/e/2PACX-1vQFoUDisnSJBzzXCMaf53ffUD1Bvpu-1GZ_stzfaaCa0Xd3WKIegbd1mmavEQcMT6r6v8z02UqloKuC/pub) for more information and examples ## 7.4.0 diff --git a/chopper_generator/pubspec.yaml b/chopper_generator/pubspec.yaml index 394c6a30..92e14816 100644 --- a/chopper_generator/pubspec.yaml +++ b/chopper_generator/pubspec.yaml @@ -1,6 +1,6 @@ name: chopper_generator description: Chopper is an http client generator using source_gen, inspired by Retrofit -version: 8.0.0-rc.2 +version: 8.0.0 documentation: https://hadrien-lejard.gitbook.io/chopper repository: https://github.com/lejard-h/chopper @@ -11,7 +11,7 @@ dependencies: analyzer: ^6.4.1 build: ^2.4.1 built_collection: ^5.1.1 - chopper: ">=8.0.0-rc.1 <9.0.0" # Will be replaced with ^8.0.0 once released + chopper: ^8.0.0 code_builder: ^4.10.0 dart_style: ^2.3.6 logging: ^1.2.0 diff --git a/example/lib/built_value_resource.chopper.dart b/example/lib/built_value_resource.chopper.dart index 32264219..01377685 100644 --- a/example/lib/built_value_resource.chopper.dart +++ b/example/lib/built_value_resource.chopper.dart @@ -6,6 +6,7 @@ part of 'built_value_resource.dart'; // ChopperGenerator // ************************************************************************** +// coverage:ignore-file // ignore_for_file: type=lint final class _$MyService extends MyService { _$MyService([ChopperClient? client]) { @@ -14,7 +15,7 @@ final class _$MyService extends MyService { } @override - final definitionType = MyService; + final Type definitionType = MyService; @override Future> getResource(String id) { diff --git a/example/lib/json_decode_service.activator.g.dart b/example/lib/json_decode_service.activator.g.dart index 48ed6298..d7b57dda 100644 --- a/example/lib/json_decode_service.activator.g.dart +++ b/example/lib/json_decode_service.activator.g.dart @@ -1,7 +1,7 @@ // GENERATED CODE - DO NOT MODIFY BY HAND // ************************************************************************** -// Generator: WorkerGenerator 2.4.1 +// Generator: WorkerGenerator 2.4.2 // ************************************************************************** import 'json_decode_service.vm.g.dart'; diff --git a/example/lib/json_decode_service.vm.g.dart b/example/lib/json_decode_service.vm.g.dart index 09d73096..3e2fc437 100644 --- a/example/lib/json_decode_service.vm.g.dart +++ b/example/lib/json_decode_service.vm.g.dart @@ -1,7 +1,7 @@ // GENERATED CODE - DO NOT MODIFY BY HAND // ************************************************************************** -// Generator: WorkerGenerator 2.4.1 +// Generator: WorkerGenerator 2.4.2 // ************************************************************************** import 'package:squadron/squadron.dart'; diff --git a/example/lib/json_decode_service.worker.g.dart b/example/lib/json_decode_service.worker.g.dart index 18a33d72..2bf75a7b 100644 --- a/example/lib/json_decode_service.worker.g.dart +++ b/example/lib/json_decode_service.worker.g.dart @@ -3,7 +3,7 @@ part of 'json_decode_service.dart'; // ************************************************************************** -// Generator: WorkerGenerator 2.4.1 +// Generator: WorkerGenerator 2.4.2 // ************************************************************************** /// WorkerService class for JsonDecodeService @@ -14,9 +14,10 @@ class _$JsonDecodeServiceWorkerService extends JsonDecodeService @override Map get operations => _operations; - late final Map _operations = { - _$jsonDecodeId: ($) => jsonDecode($.args[0]) - }; + late final Map _operations = + Map.unmodifiable({ + _$jsonDecodeId: ($) => jsonDecode($.args[0]), + }); static const int _$jsonDecodeId = 1; } diff --git a/example/lib/json_serializable.chopper.dart b/example/lib/json_serializable.chopper.dart index e6164985..07a9e4c1 100644 --- a/example/lib/json_serializable.chopper.dart +++ b/example/lib/json_serializable.chopper.dart @@ -6,6 +6,7 @@ part of 'json_serializable.dart'; // ChopperGenerator // ************************************************************************** +// coverage:ignore-file // ignore_for_file: type=lint final class _$MyService extends MyService { _$MyService([ChopperClient? client]) { @@ -14,7 +15,7 @@ final class _$MyService extends MyService { } @override - final definitionType = MyService; + final Type definitionType = MyService; @override Future> getResource(String id) { diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 735d046c..df06f946 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -1,28 +1,28 @@ name: chopper_example description: Example usage of the Chopper package -version: 0.0.5 +version: 0.0.6 documentation: https://hadrien-lejard.gitbook.io/chopper/ #author: Hadrien Lejard environment: - sdk: '>=3.0.0 <4.0.0' + sdk: ^3.0.0 dependencies: chopper: - json_annotation: ^4.8.1 + json_annotation: ^4.9.0 built_value: - analyzer: ^5.13.0 + analyzer: ^6.4.1 http: ^1.1.0 built_collection: ^5.1.1 - squadron: ^5.1.3 + squadron: ^5.1.6 dev_dependencies: - build_runner: ^2.4.6 + build_runner: ^2.4.9 chopper_generator: - json_serializable: ^6.7.1 - built_value_generator: ^8.6.1 - lints: ^2.1.1 - squadron_builder: ^2.4.1 + json_serializable: ^6.8.0 + built_value_generator: ^8.9.2 + lints: ^3.0.0 + squadron_builder: ^2.4.5 dependency_overrides: chopper: From 186ce16a9ee56fab7c04befbc489135b3ec005cb Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Tue, 14 May 2024 20:01:12 +0100 Subject: [PATCH 158/171] :arrow_up: update dependencies and linters (#615) --- chopper/pubspec.yaml | 10 +++++----- chopper/test/ensure_build_test.dart | 2 ++ chopper_built_value/pubspec.yaml | 4 ++-- chopper_generator/lib/src/generator.dart | 5 ++--- chopper_generator/lib/src/utils.dart | 2 ++ chopper_generator/pubspec.yaml | 6 +++--- chopper_generator/test/ensure_build_test.dart | 2 ++ .../main_json_serializable_squadron_worker_pool.dart | 8 ++++---- example/lib/json_decode_service.dart | 2 +- example/pubspec.yaml | 2 +- 10 files changed, 24 insertions(+), 19 deletions(-) diff --git a/chopper/pubspec.yaml b/chopper/pubspec.yaml index f71cfdbb..b1edccc5 100644 --- a/chopper/pubspec.yaml +++ b/chopper/pubspec.yaml @@ -12,21 +12,21 @@ dependencies: http: ^1.1.0 logging: ^1.2.0 meta: ^1.9.1 - qs_dart: ^1.0.10 + qs_dart: ^1.1.0 dev_dependencies: build_runner: ^2.4.9 build_test: ^2.2.2 build_verify: ^3.1.0 collection: ^1.18.0 - coverage: ^1.7.2 + coverage: ^1.8.0 data_fixture_dart: ^2.2.0 faker: ^2.1.0 http_parser: ^4.0.2 - lints: ^3.0.0 - test: ^1.25.4 + lints: ^4.0.0 + test: ^1.25.5 transparent_image: ^2.0.1 - chopper_generator: ">=8.0.0-rc.2 <9.0.0" # will be replaced by ^8.0.0 in the next release + chopper_generator: ^8.0.0 dependency_overrides: chopper_generator: diff --git a/chopper/test/ensure_build_test.dart b/chopper/test/ensure_build_test.dart index 77fb9a96..b7cba6fa 100644 --- a/chopper/test/ensure_build_test.dart +++ b/chopper/test/ensure_build_test.dart @@ -1,5 +1,7 @@ @TestOn('vm') @Timeout(Duration(seconds: 120)) +library; + import 'package:build_verify/build_verify.dart'; import 'package:test/test.dart'; diff --git a/chopper_built_value/pubspec.yaml b/chopper_built_value/pubspec.yaml index d1303271..86a94cf9 100644 --- a/chopper_built_value/pubspec.yaml +++ b/chopper_built_value/pubspec.yaml @@ -14,11 +14,11 @@ dependencies: http: ^1.1.0 dev_dependencies: - test: ^1.25.4 + test: ^1.25.5 build_runner: ^2.4.9 build_test: ^2.2.2 built_value_generator: ^8.9.2 - lints: ^3.0.0 + lints: ^4.0.0 dependency_overrides: chopper: diff --git a/chopper_generator/lib/src/generator.dart b/chopper_generator/lib/src/generator.dart index 774d8f13..20be4365 100644 --- a/chopper_generator/lib/src/generator.dart +++ b/chopper_generator/lib/src/generator.dart @@ -1,3 +1,5 @@ +// ignore_for_file: deprecated_member_use + import 'dart:async' show FutureOr; import 'package:analyzer/dart/constant/value.dart'; @@ -518,9 +520,7 @@ final class ChopperGenerator } static String _factoryForFunction(FunctionTypedElement function) => - // ignore: deprecated_member_use function.enclosingElement is ClassElement - // ignore: deprecated_member_use ? '${function.enclosingElement!.name}.${function.name}' : function.name!; @@ -640,7 +640,6 @@ final class ChopperGenerator _typeChecker(Map).isExactlyType(type) || _typeChecker(BuiltMap).isExactlyType(type)) return type; - // ignore: deprecated_member_use if (generic.isDynamic) return null; if (_typeChecker(List).isExactlyType(type) || diff --git a/chopper_generator/lib/src/utils.dart b/chopper_generator/lib/src/utils.dart index f04ba058..52387b45 100644 --- a/chopper_generator/lib/src/utils.dart +++ b/chopper_generator/lib/src/utils.dart @@ -1,3 +1,5 @@ +// ignore_for_file: deprecated_member_use + import 'dart:math' show max; import 'package:analyzer/dart/element/element.dart'; diff --git a/chopper_generator/pubspec.yaml b/chopper_generator/pubspec.yaml index 92e14816..822b2ffb 100644 --- a/chopper_generator/pubspec.yaml +++ b/chopper_generator/pubspec.yaml @@ -18,15 +18,15 @@ dependencies: meta: ^1.9.1 source_gen: ^1.5.0 yaml: ^3.1.2 - qs_dart: ^1.0.10 + qs_dart: ^1.1.0 collection: ^1.18.0 dev_dependencies: build_runner: ^2.4.9 build_verify: ^3.1.0 http: ^1.1.0 - lints: ^3.0.0 - test: ^1.25.4 + lints: ^4.0.0 + test: ^1.25.5 dependency_overrides: chopper: diff --git a/chopper_generator/test/ensure_build_test.dart b/chopper_generator/test/ensure_build_test.dart index 84c677fc..2133b15c 100644 --- a/chopper_generator/test/ensure_build_test.dart +++ b/chopper_generator/test/ensure_build_test.dart @@ -1,5 +1,7 @@ @TestOn('vm') @Timeout(Duration(seconds: 120)) +library; + import 'package:build_verify/build_verify.dart'; import 'package:test/test.dart'; diff --git a/example/bin/main_json_serializable_squadron_worker_pool.dart b/example/bin/main_json_serializable_squadron_worker_pool.dart index f2e268cd..1ea09a4f 100644 --- a/example/bin/main_json_serializable_squadron_worker_pool.dart +++ b/example/bin/main_json_serializable_squadron_worker_pool.dart @@ -1,7 +1,7 @@ -/// This example uses -/// - https://github.com/google/json_serializable.dart -/// - https://github.com/d-markey/squadron -/// - https://github.com/d-markey/squadron_builder +// This example uses +// - https://github.com/google/json_serializable.dart +// - https://github.com/d-markey/squadron +// - https://github.com/d-markey/squadron_builder import 'dart:async' show FutureOr; import 'dart:convert' show jsonDecode; diff --git a/example/lib/json_decode_service.dart b/example/lib/json_decode_service.dart index 299b73c3..293f267e 100644 --- a/example/lib/json_decode_service.dart +++ b/example/lib/json_decode_service.dart @@ -1,4 +1,4 @@ -/// This example uses https://github.com/d-markey/squadron_builder +// This example uses https://github.com/d-markey/squadron_builder import 'dart:async'; import 'dart:convert' show json; diff --git a/example/pubspec.yaml b/example/pubspec.yaml index df06f946..510c6c82 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -21,7 +21,7 @@ dev_dependencies: chopper_generator: json_serializable: ^6.8.0 built_value_generator: ^8.9.2 - lints: ^3.0.0 + lints: ^4.0.0 squadron_builder: ^2.4.5 dependency_overrides: From bb52f3869c26ee88aa5b4823662d50955c708bae Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Wed, 15 May 2024 07:19:28 +0100 Subject: [PATCH 159/171] :memo: update build_runner reference in readme (#616) --- chopper/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/chopper/README.md b/chopper/README.md index d2419358..cf4d27ce 100644 --- a/chopper/README.md +++ b/chopper/README.md @@ -15,7 +15,7 @@ Chopper is an http client generator for Dart and Flutter using source_gen and in In your project's `pubspec.yaml` file, * Add *chopper*'s latest version to your *dependencies*. -* Add `build_runner: ^1.12.2` to your *dev_dependencies*. +* Add `build_runner: ^2.4.9` to your *dev_dependencies*. * *build_runner* may already be in your *dev_dependencies* depending on your project setup and other dependencies. * Add *chopper_generator*'s latest version to your *dev_dependencies*. @@ -26,7 +26,7 @@ dependencies: chopper: ^ dev_dependencies: - build_runner: ^1.12.2 + build_runner: ^2.4.9 chopper_generator: ^ ``` From 117fd80fd7d41c7cb59907681852684ff60d0577 Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Fri, 17 May 2024 06:25:12 +0100 Subject: [PATCH 160/171] :page_facing_up: create symlink to chopper/LICENSE (#617) --- LICENSE | 1 + 1 file changed, 1 insertion(+) create mode 120000 LICENSE diff --git a/LICENSE b/LICENSE new file mode 120000 index 00000000..878a4689 --- /dev/null +++ b/LICENSE @@ -0,0 +1 @@ +chopper/LICENSE \ No newline at end of file From b2b8d65992ca7e5e9eba859e34662fbd97b44c97 Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Fri, 17 May 2024 06:30:28 +0100 Subject: [PATCH 161/171] :busts_in_silhouette: update/fix contributors (#618) --- .all-contributorsrc | 57 +++++++++++++------ CODE-OF-CONDUCT.md | 133 ++++++++++++++++++++++++++++++++++++++++++++ CONTRIBUTING.md | 111 ++++++++++++++++++++++++++++++++++++ README.md | 13 +++-- 4 files changed, 290 insertions(+), 24 deletions(-) create mode 100644 CODE-OF-CONDUCT.md create mode 100644 CONTRIBUTING.md diff --git a/.all-contributorsrc b/.all-contributorsrc index 48790593..eaf8d99a 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -1,44 +1,67 @@ { + "projectName": "chopper", + "projectOwner": "lejard-h", "files": [ "README.md" ], "imageSize": 100, "commit": false, "contributors": [ - "imageSize": 64, - "commit": false, - "contributors": [ - { + { "login": "Vovanella95", - "name": "Uladzimir_Paliukhovich", + "name": "Uladzimir Paliukhovich", "avatar_url": "https://avatars.githubusercontent.com/u/11267533?v=4", "profile": "https://github.com/Vovanella95", "contributions": [ "code" - ] - }, - { + ] + }, + { "login": "fryette", "name": "Eugeny Sampir", "avatar_url": "https://avatars.githubusercontent.com/u/3999503?v=4", "profile": "http://ysampir@gmail.com", "contributions": [ "code" - ] - }, + ] + }, + { + "login": "Guldem", + "name": "Job Guldemeester", + "avatar_url": "https://avatars.githubusercontent.com/u/11982796?v=4", + "profile": "https://github.com/Guldem", + "contributions": [ + "code", + "review", + "test", + "doc" + ] + }, { "login": "JEuler", "name": "Ivan Terekhin", "avatar_url": "https://avatars.githubusercontent.com/u/231950?v=4", "profile": "https://www.upwork.com/freelancers/~01192eefd8a1c267f7", - "contributions": [ + "contributions": [ "code", "review", "test", "doc" ] - }, -{ + }, + { + "login": "techouse", + "name": "Klemen Tusar", + "avatar_url": "https://avatars.githubusercontent.com/u/1174328?v=4", + "profile": "https://github.com/techouse", + "contributions": [ + "code", + "review", + "test", + "doc" + ] + }, + { "login": "stewemetal", "name": "Istvรกn Juhos", "avatar_url": "https://avatars.githubusercontent.com/u/5860632?v=4", @@ -49,8 +72,8 @@ "test", "doc" ] -}, -{ + }, + { "login": "lejard-h", "name": "Hadrien Lejard", "avatar_url": "https://avatars.githubusercontent.com/u/7336262?v=4", @@ -63,10 +86,8 @@ ] } ], - "contributorsPerLine": 7, - "projectName": "chopper", - "projectOwner": "lejard-h", "repoType": "github", + "contributorsPerLine": 7, "repoHost": "https://github.com", "skipCi": true } diff --git a/CODE-OF-CONDUCT.md b/CODE-OF-CONDUCT.md new file mode 100644 index 00000000..0362947b --- /dev/null +++ b/CODE-OF-CONDUCT.md @@ -0,0 +1,133 @@ + +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, caste, color, religion, or sexual +identity and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the overall + community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or advances of + any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email address, + without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official email address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +[techouse@gmail.com](mailto:techouse@gmail.com). +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series of +actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or permanent +ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within the +community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.1, available at +[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. + +Community Impact Guidelines were inspired by +[Mozilla's code of conduct enforcement ladder][Mozilla CoC]. + +For answers to common questions about this code of conduct, see the FAQ at +[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at +[https://www.contributor-covenant.org/translations][translations]. + +[homepage]: https://www.contributor-covenant.org +[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html +[Mozilla CoC]: https://github.com/mozilla/diversity +[FAQ]: https://www.contributor-covenant.org/faq +[translations]: https://www.contributor-covenant.org/translations diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..da70a90b --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,111 @@ +# Contributing + +Thank you for your interest in contributing to this project. This project relies on the help of volunteer developers for +its development and maintenance. + +Before making any changes to this repository, please first discuss the proposed changes with the repository owners +through an issue, email, or any other appropriate method of communication. + +Please note that a [code of conduct](CODE-OF-CONDUCT.md) is in place and should be adhered to during all interactions +related to the project. + +## Dart version support + +Currently, the package supports Dart versions 3.0 and above. Once a new Dart version is released, we will aim to support +it as soon as possible. If you encounter any issues with a new Dart version, please create an issue in the repository. + +## Flutter support + +This package is designed to work with Flutter 3.10 and above. We prioritize and are dedicated to maintaining +compatibility with these versions for a smooth user experience. + +## Testing + +Given the critical nature of correctly generating HTTP requests and handling API responses in the Chopper package, and +the potential for security vulnerabilities if this is not done correctly or consistently across platforms and versions +of Dart and Flutter, thorough testing is of utmost importance. Please remember to write tests for any new code you +create, using the [test](https://pub.dev/packages/test) package for all test cases. + +### Running the test suite + +To run the test suite, follow these commands: + +```bash +git clone https://github.com/lejard-h/chopper.git + +pushd chopper +dart pub get +dart test --platform vm +dart test --platform chrome +popd + +pushd chopper_generator +dart pub get +dart test --platform vm +dart test --platform chrome +popd + +pushd chopper_built_value +dart pub get +dart test --platform vm +dart test --platform chrome +popd +``` + +### Running the test suite with coverage + +```bash +pushd chopper +make show_test_coverage +popd + +pushd chopper_generator +make show_test_coverage +popd + +pushd chopper_built_value +make show_test_coverage +popd +``` + +## Submitting changes + +To contribute to this project, please submit a new pull request and provide a clear list of your changes. For guidance +on creating pull requests, you can refer to this resource. When sending a pull request, we highly appreciate the +inclusion of tests, as we strive to enhance our test coverage. +Following our coding conventions is essential, and it would be ideal if you ensure that each commit focuses on a single +feature. For commits, please write clear log messages. While concise one-line messages are suitable for small changes, +more substantial modifications should follow a format similar to the example below: + +```bash +git commit -m "A brief summary of the commit +> +> A paragraph describing what changed and its impact." +``` + +## Coding standards + +Prioritizing code readability and conciseness is essential. To achieve this, we recommend using `dart format` for code +formatting. Once your work is deemed complete, it is advisable to run the following command: + +```bash +pushd chopper +dart format lib test --output=none --set-exit-if-changed . +dart analyze lib test --fatal-infos +popd + +pushd chopper_generator +dart format lib test --output=none --set-exit-if-changed . +dart analyze lib test --fatal-infos +popd + +pushd chopper_built_value +dart format lib test --output=none --set-exit-if-changed . +dart analyze lib test --fatal-infos +popd +``` + +This command runs the Dart analyzer to identify any potential issues or inconsistencies in your code. By following these +guidelines, you can ensure a high-quality codebase. + +Thanks! diff --git a/README.md b/README.md index a6a0d409..e8482903 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Chopper -[![All Contributors](https://img.shields.io/badge/all_contributors-5-orange.svg?style=flat-square)](#contributors-) +[![All Contributors](https://img.shields.io/badge/all_contributors-7-orange.svg?style=flat-square)](#contributors-) [![pub package](https://img.shields.io/pub/v/chopper.svg)](https://pub.dartlang.org/packages/chopper) [![Dart CI](https://github.com/lejard-h/chopper/workflows/Dart%20CI/badge.svg)](https://github.com/lejard-h/chopper/actions?query=workflow%3A%22Dart+CI%22) @@ -37,12 +37,13 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d - - + + - - - + + + +

Hadrien Lejard

๐Ÿ’ป ๐Ÿ‘€ โš ๏ธ ๐Ÿ“–

Istvรกn Juhos

๐Ÿ’ป ๐Ÿ‘€ โš ๏ธ ๐Ÿ“–

Hadrien Lejard

๐Ÿ’ป ๐Ÿ‘€ โš ๏ธ ๐Ÿ“–

Istvรกn Juhos

๐Ÿ’ป ๐Ÿ‘€ โš ๏ธ ๐Ÿ“–

Klemen Tusar

๐Ÿ’ป ๐Ÿ‘€ โš ๏ธ ๐Ÿ“–

Ivan Terekhin

๐Ÿ’ป ๐Ÿ‘€ โš ๏ธ ๐Ÿ“–

Eugeny Sampir

๐Ÿ’ป

Uladzimir_Paliukhovich

๐Ÿ’ป

Ivan Terekhin

๐Ÿ’ป ๐Ÿ‘€ โš ๏ธ ๐Ÿ“–

Job Guldemeester

๐Ÿ’ป ๐Ÿ‘€ โš ๏ธ ๐Ÿ“–

Eugeny Sampir

๐Ÿ’ป

Uladzimir Paliukhovich

๐Ÿ’ป
From 3482cc6ef7e04dc49df224c5e6d70ba16a7065b1 Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Fri, 17 May 2024 06:32:24 +0100 Subject: [PATCH 162/171] :lock: add Security Policy (#619) --- SECURITY.md | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 SECURITY.md diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000..0decb3bb --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,46 @@ +# Security Policy + +## Supported Versions + +| Version | Supported | +|---------|--------------------| +| 8.x.x | :white_check_mark: | +| 7.x.x | :x: | +| 6.x.x | :x: | +| 5.x.x | :x: | +| 4.x.x | :x: | +| 3.x.x | :x: | +| 2.x.x | :x: | +| 1.x.x | :x: | +| 0.x.x | :x: | + + +## Reporting a Vulnerability + +We take the security of our software seriously. If you believe you have found a security vulnerability, please report it +to us as described below. + +**DO NOT CREATE A GITHUB ISSUE** reporting the vulnerability. + +Instead, send an email to either [techouse@gmail.com](mailto:techouse@gmail.com) or +[i.terhin@gmail.com](mailto:i.terhin@gmail.com). + +In the report, please include the following: + +- Your name and affiliation (if any). +- A description of the technical details of the vulnerabilities. It is very important to let us know how we can + reproduce your findings. +- An explanation who can exploit this vulnerability, and what they gain when doing so -- write an attack scenario. This + will help us evaluate your submission quickly, especially if it is a complex or creative vulnerability. +- Whether this vulnerability is public or known to third parties. If it is, please provide details. + +If you donโ€™t get an acknowledgment from us or have heard nothing from us in a week, please contact us again. + +We will send a response indicating the next steps in handling your report. We will keep you informed about the progress +towards a fix and full announcement. + +We will not disclose your identity to the public without your permission. We strive to credit researchers in our +advisories when we release a fix, but only after getting your permission. + +We appreciate your efforts to responsibly disclose your findings, and will make every effort to acknowledge your +contributions. From 627ff1d73bdb5c3eced3a39a01984c65b9a5838d Mon Sep 17 00:00:00 2001 From: Job Guldemeester Date: Tue, 25 Jun 2024 13:16:29 +0200 Subject: [PATCH 163/171] =?UTF-8?q?=F0=9F=90=9B=20Fix=20null=20body=20conv?= =?UTF-8?q?erter=20(#623)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../response_converter_interceptor.dart | 10 +--- .../response_converter_interceptor_test.dart | 58 +++++++++++++++++++ 2 files changed, 61 insertions(+), 7 deletions(-) diff --git a/chopper/lib/src/interceptors/response_converter_interceptor.dart b/chopper/lib/src/interceptors/response_converter_interceptor.dart index 5d2dd188..07d054e4 100644 --- a/chopper/lib/src/interceptors/response_converter_interceptor.dart +++ b/chopper/lib/src/interceptors/response_converter_interceptor.dart @@ -52,17 +52,13 @@ class ResponseConverterInterceptor implements InternalInterceptor { Response response, ConvertResponse? responseConverter, ) async { - Response? newResponse; if (responseConverter != null) { - newResponse = await responseConverter(response); + response = await responseConverter(response); } else if (_converter != null) { - newResponse = await _decodeResponse(response, _converter!); + response = await _decodeResponse(response, _converter!); } - return Response( - newResponse?.base ?? response.base, - newResponse?.body ?? response.body, - ); + return Response(response.base, response.body); } /// Converts the [response] using [_converter]. diff --git a/chopper/test/chain/response_converter_interceptor_test.dart b/chopper/test/chain/response_converter_interceptor_test.dart index 1edfe680..41a3a968 100644 --- a/chopper/test/chain/response_converter_interceptor_test.dart +++ b/chopper/test/chain/response_converter_interceptor_test.dart @@ -70,6 +70,44 @@ void main() { expect(converter.called, 0); }); + test( + 'response is successful converter is not null and response converter is null, response is converted with null body', + () async { + final converter = ResponseNullBodyConverter(); + interceptorChain = InterceptorChain( + interceptors: [ + ResponseConverterInterceptor(converter: converter), + ResponseInterceptor(), + ], + request: testRequest, + ); + + final response = await interceptorChain.proceed(testRequest); + + expect(response.body, null); + expect(converter.called, 1); + }); + + test( + 'response is successful converter is not null and response converter is not null, response is converted by response converter with null body', + () async { + final converter = ResponseNullBodyConverter(); + interceptorChain = InterceptorChain( + interceptors: [ + ResponseConverterInterceptor( + converter: converter, + responseConverter: (response) => Response(response.base, null)), + ResponseInterceptor(), + ], + request: testRequest, + ); + + final response = await interceptorChain.proceed(testRequest); + + expect(response.body, null); + expect(converter.called, 0); + }); + test( 'response is unsuccessful converter is not null and response converter is not null, response is not converted', () async { @@ -153,6 +191,8 @@ void main() { expect(converter.called, 0); }); }); + + group('response converter returns converted response tests', () {}); } // ignore mutability warning for test class. @@ -173,6 +213,24 @@ class ResponseConverter implements Converter { } } +// ignore mutability warning for test class. +//ignore: must_be_immutable +class ResponseNullBodyConverter implements Converter { + int called = 0; + + @override + FutureOr convertRequest(Request request) { + return request; + } + + @override + FutureOr> convertResponse( + Response response) { + called++; + return Response(response.base, null as BodyType); + } +} + // ignore mutability warning for test class. //ignore: must_be_immutable class ResponseErrorConverter implements ErrorConverter { From 7e1f0f935866cd1c5bc62b4c5a3ca2e7dabe04c2 Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Wed, 26 Jun 2024 12:16:35 +0100 Subject: [PATCH 164/171] :package: directly export `qs.ListFormat` instead of internal wrapper (#624) --- chopper/lib/chopper.dart | 2 +- chopper/lib/src/annotations.dart | 2 +- chopper/lib/src/list_format.dart | 29 ------------------------ chopper/lib/src/request.dart | 2 +- chopper/lib/src/utils.dart | 10 ++++---- chopper/pubspec.yaml | 2 +- chopper/test/utils_test.dart | 2 +- chopper_generator/lib/src/generator.dart | 5 ++-- chopper_generator/lib/src/utils.dart | 2 +- chopper_generator/pubspec.yaml | 1 - 10 files changed, 13 insertions(+), 44 deletions(-) delete mode 100644 chopper/lib/src/list_format.dart diff --git a/chopper/lib/chopper.dart b/chopper/lib/chopper.dart index a2b503ab..0096bbbb 100644 --- a/chopper/lib/chopper.dart +++ b/chopper/lib/chopper.dart @@ -3,6 +3,7 @@ /// [Getting Started](https://hadrien-lejard.gitbook.io/chopper) library chopper; +export 'package:qs_dart/qs_dart.dart' show ListFormat; export 'src/annotations.dart'; export 'src/authenticator.dart'; export 'src/base.dart'; @@ -14,7 +15,6 @@ export 'src/extensions.dart'; export 'src/chain/chain.dart'; export 'src/interceptors/interceptor.dart'; export 'src/converters.dart'; -export 'src/list_format.dart'; export 'src/request.dart'; export 'src/response.dart'; export 'src/utils.dart' hide mapToQuery; diff --git a/chopper/lib/src/annotations.dart b/chopper/lib/src/annotations.dart index 7c205346..c850b3b2 100644 --- a/chopper/lib/src/annotations.dart +++ b/chopper/lib/src/annotations.dart @@ -1,11 +1,11 @@ import 'dart:async'; import 'package:chopper/src/constants.dart'; -import 'package:chopper/src/list_format.dart'; import 'package:chopper/src/request.dart'; import 'package:chopper/src/response.dart'; import 'package:meta/meta.dart'; import 'package:meta/meta_meta.dart'; +import 'package:qs_dart/qs_dart.dart' show ListFormat; /// {@template ChopperApi} /// Defines a Chopper API. diff --git a/chopper/lib/src/list_format.dart b/chopper/lib/src/list_format.dart deleted file mode 100644 index dedb3d72..00000000 --- a/chopper/lib/src/list_format.dart +++ /dev/null @@ -1,29 +0,0 @@ -import 'package:qs_dart/qs_dart.dart' as qs show ListFormat; - -/// An enum of all available list format options. -/// -/// This is a wrapper around the [qs.ListFormat] enum. -enum ListFormat { - /// Use brackets to represent list items, for example - /// `foo[]=123&foo[]=456&foo[]=789` - brackets(qs.ListFormat.brackets), - - /// Use commas to represent list items, for example - /// `foo=123,456,789` - comma(qs.ListFormat.comma), - - /// Repeat the same key to represent list items, for example - /// `foo=123&foo=456&foo=789` - repeat(qs.ListFormat.repeat), - - /// Use indices in brackets to represent list items, for example - /// `foo[0]=123&foo[1]=456&foo[2]=789` - indices(qs.ListFormat.indices); - - const ListFormat(this.qsListFormat); - - final qs.ListFormat qsListFormat; - - @override - String toString() => name; -} diff --git a/chopper/lib/src/request.dart b/chopper/lib/src/request.dart index fbf1891b..511fca01 100644 --- a/chopper/lib/src/request.dart +++ b/chopper/lib/src/request.dart @@ -1,11 +1,11 @@ import 'dart:async' show Stream; import 'package:chopper/src/extensions.dart'; -import 'package:chopper/src/list_format.dart'; import 'package:chopper/src/utils.dart'; import 'package:equatable/equatable.dart' show EquatableMixin; import 'package:http/http.dart' as http; import 'package:meta/meta.dart'; +import 'package:qs_dart/qs_dart.dart' show ListFormat; /// {@template request} /// This class represents an HTTP request that can be made with Chopper. diff --git a/chopper/lib/src/utils.dart b/chopper/lib/src/utils.dart index 4906df9b..57643481 100644 --- a/chopper/lib/src/utils.dart +++ b/chopper/lib/src/utils.dart @@ -1,8 +1,8 @@ import 'dart:collection'; -import 'package:chopper/chopper.dart'; +import 'package:chopper/src/request.dart'; import 'package:logging/logging.dart'; -import 'package:qs_dart/qs_dart.dart' as qs; +import 'package:qs_dart/qs_dart.dart' show encode, EncodeOptions, ListFormat; /// Creates a new [Request] by copying [request] and adding a header with the /// provided key [name] and value [value] to the result. @@ -69,10 +69,10 @@ String mapToQuery( }) { listFormat ??= useBrackets == true ? ListFormat.brackets : ListFormat.repeat; - return qs.encode( + return encode( map, - qs.EncodeOptions( - listFormat: listFormat.qsListFormat, + EncodeOptions( + listFormat: listFormat, allowDots: listFormat == ListFormat.repeat, encodeDotInKeys: listFormat == ListFormat.repeat, encodeValuesOnly: listFormat == ListFormat.repeat, diff --git a/chopper/pubspec.yaml b/chopper/pubspec.yaml index b1edccc5..a4b1036d 100644 --- a/chopper/pubspec.yaml +++ b/chopper/pubspec.yaml @@ -12,7 +12,7 @@ dependencies: http: ^1.1.0 logging: ^1.2.0 meta: ^1.9.1 - qs_dart: ^1.1.0 + qs_dart: ^1.2.0 dev_dependencies: build_runner: ^2.4.9 diff --git a/chopper/test/utils_test.dart b/chopper/test/utils_test.dart index fc4e6810..9ff140db 100644 --- a/chopper/test/utils_test.dart +++ b/chopper/test/utils_test.dart @@ -1,8 +1,8 @@ // ignore_for_file: deprecated_member_use_from_same_package -import 'package:chopper/src/list_format.dart'; import 'package:chopper/src/request.dart'; import 'package:chopper/src/utils.dart'; +import 'package:qs_dart/qs_dart.dart' show ListFormat; import 'package:test/test.dart'; import 'fixtures/example_enum.dart'; diff --git a/chopper_generator/lib/src/generator.dart b/chopper_generator/lib/src/generator.dart index 20be4365..ce3efadf 100644 --- a/chopper_generator/lib/src/generator.dart +++ b/chopper_generator/lib/src/generator.dart @@ -14,7 +14,6 @@ import 'package:chopper_generator/src/vars.dart'; import 'package:code_builder/code_builder.dart'; import 'package:dart_style/dart_style.dart'; import 'package:logging/logging.dart'; -import 'package:qs_dart/qs_dart.dart' show ListFormat; import 'package:source_gen/source_gen.dart'; /// Code generator for [chopper.ChopperApi] annotated classes. @@ -406,7 +405,7 @@ final class ChopperGenerator final bool hasTag = tag.isNotEmpty; - final ListFormat? listFormat = Utils.getListFormat(method); + final chopper.ListFormat? listFormat = Utils.getListFormat(method); final bool? useBrackets = Utils.getUseBrackets(method); @@ -716,7 +715,7 @@ final class ChopperGenerator bool hasParts = false, bool useQueries = false, bool useHeaders = false, - ListFormat? listFormat, + chopper.ListFormat? listFormat, @Deprecated('Use listFormat instead') bool? useBrackets, bool? includeNullQueryVars, Reference? tagRefer, diff --git a/chopper_generator/lib/src/utils.dart b/chopper_generator/lib/src/utils.dart index 52387b45..cb26cfc6 100644 --- a/chopper_generator/lib/src/utils.dart +++ b/chopper_generator/lib/src/utils.dart @@ -3,10 +3,10 @@ import 'dart:math' show max; import 'package:analyzer/dart/element/element.dart'; +import 'package:chopper/chopper.dart' show ListFormat; import 'package:chopper_generator/src/extensions.dart'; import 'package:code_builder/code_builder.dart'; import 'package:collection/collection.dart'; -import 'package:qs_dart/qs_dart.dart' show ListFormat; import 'package:source_gen/source_gen.dart'; final class Utils { diff --git a/chopper_generator/pubspec.yaml b/chopper_generator/pubspec.yaml index 822b2ffb..ab65e440 100644 --- a/chopper_generator/pubspec.yaml +++ b/chopper_generator/pubspec.yaml @@ -18,7 +18,6 @@ dependencies: meta: ^1.9.1 source_gen: ^1.5.0 yaml: ^3.1.2 - qs_dart: ^1.1.0 collection: ^1.18.0 dev_dependencies: From 33372bf395977f20bf0184d8c3b9deb07656461a Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Mon, 1 Jul 2024 11:04:34 +0100 Subject: [PATCH 165/171] :twisted_rightwards_arrows: post release branch sync (#629) --- chopper/CHANGELOG.md | 10 ++++++++++ chopper/pubspec.yaml | 2 +- chopper_built_value/CHANGELOG.md | 4 ++++ chopper_built_value/pubspec.yaml | 2 +- chopper_generator/CHANGELOG.md | 5 +++++ chopper_generator/pubspec.yaml | 4 ++-- 6 files changed, 23 insertions(+), 4 deletions(-) diff --git a/chopper/CHANGELOG.md b/chopper/CHANGELOG.md index 84af67b0..fd922ce4 100644 --- a/chopper/CHANGELOG.md +++ b/chopper/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## 8.0.1+1 + +- Re-remove internal `qs.ListFormat` wrapper + +## 8.0.1 + +- Fix null body converter ([#623](https://github.com/lejard-h/chopper/pull/623)) +- Directly export `qs.ListFormat` instead of internal wrapper ([#624](https://github.com/lejard-h/chopper/pull/624)) +- Update dependencies and linters ([#615](https://github.com/lejard-h/chopper/pull/615)) + ## 8.0.0 - Add per-request timeout ([#604](https://github.com/lejard-h/chopper/pull/604)) diff --git a/chopper/pubspec.yaml b/chopper/pubspec.yaml index a4b1036d..21ec2806 100644 --- a/chopper/pubspec.yaml +++ b/chopper/pubspec.yaml @@ -1,6 +1,6 @@ name: chopper description: Chopper is an http client generator using source_gen, inspired by Retrofit -version: 8.0.0 +version: 8.0.1+1 documentation: https://hadrien-lejard.gitbook.io/chopper repository: https://github.com/lejard-h/chopper diff --git a/chopper_built_value/CHANGELOG.md b/chopper_built_value/CHANGELOG.md index f0297951..62038eff 100644 --- a/chopper_built_value/CHANGELOG.md +++ b/chopper_built_value/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 3.0.1 + +- Update dependencies and linters ([#615](https://github.com/lejard-h/chopper/pull/615)) + ## 3.0.0 - Require Chopper ^8.0.0 diff --git a/chopper_built_value/pubspec.yaml b/chopper_built_value/pubspec.yaml index 86a94cf9..df350ada 100644 --- a/chopper_built_value/pubspec.yaml +++ b/chopper_built_value/pubspec.yaml @@ -1,6 +1,6 @@ name: chopper_built_value description: A built_value based Converter for Chopper. -version: 3.0.0 +version: 3.0.1 documentation: https://hadrien-lejard.gitbook.io/chopper/converters/built-value-converter repository: https://github.com/lejard-h/chopper diff --git a/chopper_generator/CHANGELOG.md b/chopper_generator/CHANGELOG.md index c41e9fe1..23025dac 100644 --- a/chopper_generator/CHANGELOG.md +++ b/chopper_generator/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## 8.0.1 + +- Directly export `qs.ListFormat` instead of internal wrapper ([#624](https://github.com/lejard-h/chopper/pull/624)) +- Update dependencies and linters ([#615](https://github.com/lejard-h/chopper/pull/615)) + ## 8.0.0 - Add per-request timeout ([#604](https://github.com/lejard-h/chopper/pull/604)) diff --git a/chopper_generator/pubspec.yaml b/chopper_generator/pubspec.yaml index ab65e440..dc582979 100644 --- a/chopper_generator/pubspec.yaml +++ b/chopper_generator/pubspec.yaml @@ -1,6 +1,6 @@ name: chopper_generator description: Chopper is an http client generator using source_gen, inspired by Retrofit -version: 8.0.0 +version: 8.0.1 documentation: https://hadrien-lejard.gitbook.io/chopper repository: https://github.com/lejard-h/chopper @@ -11,7 +11,7 @@ dependencies: analyzer: ^6.4.1 build: ^2.4.1 built_collection: ^5.1.1 - chopper: ^8.0.0 + chopper: ^8.0.1 code_builder: ^4.10.0 dart_style: ^2.3.6 logging: ^1.2.0 From 7a1f7c27433f6a1666b5176e67e960efee39c83d Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Fri, 30 Aug 2024 07:37:35 +0100 Subject: [PATCH 166/171] :art: remove duplicate null-coalescing responseTypeReference (#634) --- chopper_generator/lib/src/generator.dart | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/chopper_generator/lib/src/generator.dart b/chopper_generator/lib/src/generator.dart index ce3efadf..3a4f697c 100644 --- a/chopper_generator/lib/src/generator.dart +++ b/chopper_generator/lib/src/generator.dart @@ -193,9 +193,8 @@ final class ChopperGenerator // Set Response with generic types final Reference responseTypeReference = refer( - responseType?.getDisplayString(withNullability: false) ?? - responseType?.getDisplayString(withNullability: false) ?? - 'dynamic'); + responseType?.getDisplayString(withNullability: false) ?? 'dynamic', + ); // Set the return type final returnType = isResponseObject ? refer(m.returnType.getDisplayString(withNullability: false)) From b7ebf67672090de2b3bdb36e3e080237731926c1 Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Wed, 4 Sep 2024 15:24:36 +0100 Subject: [PATCH 167/171] :bug: properly escape single quote in cURL interceptor output (#635) --- .../src/interceptors/curl_interceptor.dart | 4 +- chopper/test/interceptors_test.dart | 67 ++++++++++++------- 2 files changed, 46 insertions(+), 25 deletions(-) diff --git a/chopper/lib/src/interceptors/curl_interceptor.dart b/chopper/lib/src/interceptors/curl_interceptor.dart index 9eb4256c..ef850e92 100644 --- a/chopper/lib/src/interceptors/curl_interceptor.dart +++ b/chopper/lib/src/interceptors/curl_interceptor.dart @@ -25,7 +25,7 @@ class CurlInterceptor implements Interceptor { if (baseRequest is http.Request) { final String body = baseRequest.body; if (body.isNotEmpty) { - curlParts.add("-d '$body'"); + curlParts.add("-d '${body.replaceAll("'", r"'\''")}'"); } } if (baseRequest is http.MultipartRequest) { @@ -36,7 +36,7 @@ class CurlInterceptor implements Interceptor { curlParts.add("-f '${file.field}: ${file.filename ?? ''}'"); } } - curlParts.add('"${baseRequest.url}"'); + curlParts.add("'${baseRequest.url}'"); chopperLogger.info(curlParts.join(' ')); return chain.proceed(chain.request); diff --git a/chopper/test/interceptors_test.dart b/chopper/test/interceptors_test.dart index a1d1a2ac..87567951 100644 --- a/chopper/test/interceptors_test.dart +++ b/chopper/test/interceptors_test.dart @@ -91,15 +91,15 @@ void main() { ); }); - final fakeRequest = Request( - 'POST', - Uri.parse('/'), - Uri.parse('base'), - body: 'test', - headers: {'foo': 'bar'}, - ); - test('Curl interceptors', () async { + final fakeRequest = Request( + 'POST', + Uri.parse('/'), + Uri.parse('base'), + body: 'test', + headers: {'foo': 'bar'}, + ); + final curl = CurlInterceptor(); var log = ''; chopperLogger.onRecord.listen((r) => log = r.message); @@ -108,27 +108,48 @@ void main() { expect( log, equals( - "curl -v -X POST -H 'foo: bar' -H 'content-type: text/plain; charset=utf-8' -d 'test' \"base/\"", + r"curl -v -X POST -H 'foo: bar' -H 'content-type: text/plain; charset=utf-8' -d 'test' 'base/'", ), ); }); - final fakeRequestMultipart = Request( - 'POST', - Uri.parse('/'), - Uri.parse('base'), - headers: {'foo': 'bar'}, - parts: [ - PartValue('p1', 123), - PartValueFile( - 'p2', - http.MultipartFile.fromBytes('file', [0], filename: 'filename'), + test('Curl interceptor with escaped text', () async { + final fakeRequest = Request( + 'POST', + Uri.parse('/'), + Uri.parse('base'), + body: r"""Lorem's ipsum "dolor" sit amet""", + ); + + final curl = CurlInterceptor(); + var log = ''; + chopperLogger.onRecord.listen((r) => log = r.message); + await curl.intercept(FakeChain(fakeRequest)); + + expect( + log, + equals( + r"""curl -v -X POST -H 'content-type: text/plain; charset=utf-8' -d 'Lorem'\''s ipsum "dolor" sit amet' 'base/'""", ), - ], - multipart: true, - ); + ); + }); test('Curl interceptors Multipart', () async { + final fakeRequestMultipart = Request( + 'POST', + Uri.parse('/'), + Uri.parse('base'), + headers: {'foo': 'bar'}, + parts: [ + PartValue('p1', 123), + PartValueFile( + 'p2', + http.MultipartFile.fromBytes('file', [0], filename: 'filename'), + ), + ], + multipart: true, + ); + final curl = CurlInterceptor(); var log = ''; chopperLogger.onRecord.listen((r) => log = r.message); @@ -137,7 +158,7 @@ void main() { expect( log, equals( - "curl -v -X POST -H 'foo: bar' -f 'p1: 123' -f 'file: filename' \"base/\"", + r"curl -v -X POST -H 'foo: bar' -f 'p1: 123' -f 'file: filename' 'base/'", ), ); }); From d608c70539f7f9da4cd1649902952ab55c3be7d6 Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Thu, 5 Sep 2024 06:22:43 +0100 Subject: [PATCH 168/171] :twisted_rightwards_arrows: post release branch sync (#638) --- chopper/CHANGELOG.md | 4 ++++ chopper/pubspec.yaml | 6 +++--- chopper_generator/CHANGELOG.md | 4 ++++ chopper_generator/pubspec.yaml | 2 +- 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/chopper/CHANGELOG.md b/chopper/CHANGELOG.md index fd922ce4..747c42a6 100644 --- a/chopper/CHANGELOG.md +++ b/chopper/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 8.0.2 + +- Properly escape single quote in cURL interceptor output ([#635](https://github.com/lejard-h/chopper/pull/635)) + ## 8.0.1+1 - Re-remove internal `qs.ListFormat` wrapper diff --git a/chopper/pubspec.yaml b/chopper/pubspec.yaml index 21ec2806..9405209d 100644 --- a/chopper/pubspec.yaml +++ b/chopper/pubspec.yaml @@ -1,6 +1,6 @@ name: chopper description: Chopper is an http client generator using source_gen, inspired by Retrofit -version: 8.0.1+1 +version: 8.0.2 documentation: https://hadrien-lejard.gitbook.io/chopper repository: https://github.com/lejard-h/chopper @@ -12,7 +12,7 @@ dependencies: http: ^1.1.0 logging: ^1.2.0 meta: ^1.9.1 - qs_dart: ^1.2.0 + qs_dart: ^1.2.3 dev_dependencies: build_runner: ^2.4.9 @@ -26,7 +26,7 @@ dev_dependencies: lints: ^4.0.0 test: ^1.25.5 transparent_image: ^2.0.1 - chopper_generator: ^8.0.0 + chopper_generator: ^8.0.1 dependency_overrides: chopper_generator: diff --git a/chopper_generator/CHANGELOG.md b/chopper_generator/CHANGELOG.md index 23025dac..35e3c091 100644 --- a/chopper_generator/CHANGELOG.md +++ b/chopper_generator/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 8.0.2 + +- Remove duplicate null-coalescing `responseTypeReference` ([#634](https://github.com/lejard-h/chopper/pull/634)) + ## 8.0.1 - Directly export `qs.ListFormat` instead of internal wrapper ([#624](https://github.com/lejard-h/chopper/pull/624)) diff --git a/chopper_generator/pubspec.yaml b/chopper_generator/pubspec.yaml index dc582979..a2c29f63 100644 --- a/chopper_generator/pubspec.yaml +++ b/chopper_generator/pubspec.yaml @@ -1,6 +1,6 @@ name: chopper_generator description: Chopper is an http client generator using source_gen, inspired by Retrofit -version: 8.0.1 +version: 8.0.2 documentation: https://hadrien-lejard.gitbook.io/chopper repository: https://github.com/lejard-h/chopper From 4e01475947d186904b5c99c699bb463d6584be53 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 30 Sep 2024 21:17:14 +0100 Subject: [PATCH 169/171] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Bump=20lints=20fro?= =?UTF-8?q?m=204.0.0=20to=205.0.0=20in=20/chopper=20(#641)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * :arrow_up: Bump lints from 4.0.0 to 5.0.0 in /chopper Bumps [lints](https://github.com/dart-lang/lints) from 4.0.0 to 5.0.0. - [Release notes](https://github.com/dart-lang/lints/releases) - [Changelog](https://github.com/dart-lang/lints/blob/main/CHANGELOG.md) - [Commits](https://github.com/dart-lang/lints/compare/v4.0.0...v5.0.0) --- updated-dependencies: - dependency-name: lints dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] * :rotating_light: fix linter warnings --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Klemen Tuลกar --- chopper/lib/chopper.dart | 2 +- chopper/pubspec.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/chopper/lib/chopper.dart b/chopper/lib/chopper.dart index 0096bbbb..60b083c6 100644 --- a/chopper/lib/chopper.dart +++ b/chopper/lib/chopper.dart @@ -1,7 +1,7 @@ /// Chopper is an http client generator using source_gen and inspired by Retrofit. /// /// [Getting Started](https://hadrien-lejard.gitbook.io/chopper) -library chopper; +library; export 'package:qs_dart/qs_dart.dart' show ListFormat; export 'src/annotations.dart'; diff --git a/chopper/pubspec.yaml b/chopper/pubspec.yaml index 9405209d..02166ffc 100644 --- a/chopper/pubspec.yaml +++ b/chopper/pubspec.yaml @@ -23,7 +23,7 @@ dev_dependencies: data_fixture_dart: ^2.2.0 faker: ^2.1.0 http_parser: ^4.0.2 - lints: ^4.0.0 + lints: ">=4.0.0 <6.0.0" test: ^1.25.5 transparent_image: ^2.0.1 chopper_generator: ^8.0.1 From 1fd1755170cad7d5db36788de3a8d92d8b724853 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 30 Sep 2024 21:17:27 +0100 Subject: [PATCH 170/171] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Bump=20lints=20fro?= =?UTF-8?q?m=204.0.0=20to=205.0.0=20in=20/chopper=5Fgenerator=20(#639)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * :arrow_up: Bump lints from 4.0.0 to 5.0.0 in /chopper_generator Bumps [lints](https://github.com/dart-lang/lints) from 4.0.0 to 5.0.0. - [Release notes](https://github.com/dart-lang/lints/releases) - [Changelog](https://github.com/dart-lang/lints/blob/main/CHANGELOG.md) - [Commits](https://github.com/dart-lang/lints/compare/v4.0.0...v5.0.0) --- updated-dependencies: - dependency-name: lints dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] * :rotating_light: fix linter warnings --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Klemen Tuลกar --- chopper_generator/lib/chopper_generator.dart | 2 +- chopper_generator/pubspec.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/chopper_generator/lib/chopper_generator.dart b/chopper_generator/lib/chopper_generator.dart index df3d5954..81ecdccb 100644 --- a/chopper_generator/lib/chopper_generator.dart +++ b/chopper_generator/lib/chopper_generator.dart @@ -1,3 +1,3 @@ -library chopper_generator.dart; +library; export 'src/builder_factory.dart'; diff --git a/chopper_generator/pubspec.yaml b/chopper_generator/pubspec.yaml index a2c29f63..56a8a136 100644 --- a/chopper_generator/pubspec.yaml +++ b/chopper_generator/pubspec.yaml @@ -24,7 +24,7 @@ dev_dependencies: build_runner: ^2.4.9 build_verify: ^3.1.0 http: ^1.1.0 - lints: ^4.0.0 + lints: ">=4.0.0 <6.0.0" test: ^1.25.5 dependency_overrides: From c5630ae4ccae78c02476c2e62a14982f3447050c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 30 Sep 2024 21:17:36 +0100 Subject: [PATCH 171/171] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Bump=20lints=20fro?= =?UTF-8?q?m=204.0.0=20to=205.0.0=20in=20/chopper=5Fbuilt=5Fvalue=20(#640)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * :arrow_up: Bump lints from 4.0.0 to 5.0.0 in /chopper_built_value Bumps [lints](https://github.com/dart-lang/lints) from 4.0.0 to 5.0.0. - [Release notes](https://github.com/dart-lang/lints/releases) - [Changelog](https://github.com/dart-lang/lints/blob/main/CHANGELOG.md) - [Commits](https://github.com/dart-lang/lints/compare/v4.0.0...v5.0.0) --- updated-dependencies: - dependency-name: lints dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] * :rotating_light: fix linter warnings --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Klemen Tuลกar --- chopper_built_value/pubspec.yaml | 2 +- chopper_built_value/test/serializers.dart | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/chopper_built_value/pubspec.yaml b/chopper_built_value/pubspec.yaml index df350ada..f2cf5b88 100644 --- a/chopper_built_value/pubspec.yaml +++ b/chopper_built_value/pubspec.yaml @@ -18,7 +18,7 @@ dev_dependencies: build_runner: ^2.4.9 build_test: ^2.2.2 built_value_generator: ^8.9.2 - lints: ^4.0.0 + lints: ">=4.0.0 <6.0.0" dependency_overrides: chopper: diff --git a/chopper_built_value/test/serializers.dart b/chopper_built_value/test/serializers.dart index 7124a7c1..61e3a1f5 100644 --- a/chopper_built_value/test/serializers.dart +++ b/chopper_built_value/test/serializers.dart @@ -1,4 +1,4 @@ -library serializers; +library; import 'package:built_value/serializer.dart';