From d707d3f819b12b50f8a8870ca532671836d15d05 Mon Sep 17 00:00:00 2001 From: Ivan Terekhin Date: Tue, 1 Jun 2021 18:03:29 +0300 Subject: [PATCH 01/60] Release 4.0.1 (#260) (#265) Co-authored-by: Ivan Terekhin Co-authored-by: Youssef Raafat --- chopper/CHANGELOG.md | 15 +++++++++------ chopper/lib/src/base.dart | 6 +++--- chopper/pubspec.yaml | 2 +- chopper_generator/CHANGELOG.md | 13 ++++++++----- chopper_generator/lib/chopper_generator.dart | 2 +- chopper_generator/lib/src/generator.dart | 2 +- chopper_generator/pubspec.yaml | 2 +- 7 files changed, 24 insertions(+), 18 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/lib/src/base.dart b/chopper/lib/src/base.dart index d936c669..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, @@ -340,7 +340,7 @@ class ChopperClient { Map headers = const {}, Map parameters = const {}, String? baseUrl, - dynamic? body, + dynamic body, }) => send( Request( 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/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, 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 c1210bb44f1aa7e6f112e4f5c53ed528631b6080 Mon Sep 17 00:00:00 2001 From: Ivan Terekhin Date: Thu, 23 Sep 2021 14:52:40 +0300 Subject: [PATCH 02/60] Chopper generator release 4.0.2 (#300) Co-authored-by: Ivan Terekhin Co-authored-by: Youssef Raafat Co-authored-by: luis901101 Co-authored-by: melvspace --- chopper_generator/CHANGELOG.md | 5 +++++ chopper_generator/lib/src/generator.dart | 2 +- chopper_generator/pubspec.yaml | 4 ++-- 3 files changed, 8 insertions(+), 3 deletions(-) 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/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), ); }); diff --git a/chopper_generator/pubspec.yaml b/chopper_generator/pubspec.yaml index f399b818..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 @@ -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 d22fe0f91c43b368593a13f18bb1aece7adf3765 Mon Sep 17 00:00:00 2001 From: Ivan Terekhin Date: Sat, 6 Nov 2021 09:53:52 +0300 Subject: [PATCH 03/60] Release 4.0.3 (#305) Co-authored-by: Ivan Terekhin Co-authored-by: Youssef Raafat Co-authored-by: luis901101 Co-authored-by: melvspace --- chopper/CHANGELOG.md | 5 +++++ chopper/lib/src/base.dart | 14 ++++++++++++-- chopper/lib/src/response.dart | 2 +- chopper/lib/src/utils.dart | 2 +- chopper/pubspec.yaml | 2 +- chopper_generator/CHANGELOG.md | 4 ++++ chopper_generator/lib/src/generator.dart | 2 +- chopper_generator/pubspec.yaml | 2 +- 8 files changed, 26 insertions(+), 7 deletions(-) diff --git a/chopper/CHANGELOG.md b/chopper/CHANGELOG.md index 6af68839..1ae2764f 100644 --- a/chopper/CHANGELOG.md +++ b/chopper/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## 4.0.3 + +- Fix for authenticator usage +- Null-safety fixes + ## 4.0.1 - Fix for the null safety support 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; } 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, ); 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; diff --git a/chopper/pubspec.yaml b/chopper/pubspec.yaml index 70d13169..eef79174 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: 4.0.3 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 41173021..4c36fe03 100644 --- a/chopper_generator/CHANGELOG.md +++ b/chopper_generator/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 4.0.3 + +- Interpolation fixes + ## 4.0.2 - Analyzer dependency upgrade 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://')) { diff --git a/chopper_generator/pubspec.yaml b/chopper_generator/pubspec.yaml index 708c683c..78beea6b 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 author: Hadrien Lejard From 898fd3526bab65b1f8b572d5a7e8a455eceb51d7 Mon Sep 17 00:00:00 2001 From: Ivan Terekhin Date: Thu, 9 Dec 2021 21:39:39 +0300 Subject: [PATCH 04/60] Release 4.0.4 (#311) Co-authored-by: Ivan Terekhin Co-authored-by: Youssef Raafat Co-authored-by: luis901101 Co-authored-by: melvspace --- chopper/CHANGELOG.md | 4 ++++ chopper/lib/src/base.dart | 8 ++++++-- chopper/pubspec.yaml | 2 +- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/chopper/CHANGELOG.md b/chopper/CHANGELOG.md index 1ae2764f..b8d2c824 100644 --- a/chopper/CHANGELOG.md +++ b/chopper/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 4.0.4 + +- Fix for authenticator usage + ## 4.0.3 - Fix for authenticator usage 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); diff --git a/chopper/pubspec.yaml b/chopper/pubspec.yaml index eef79174..6b78fdcf 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.3 +version: 4.0.4 documentation: https://hadrien-lejard.gitbook.io/chopper repository: https://github.com/lejard-h/chopper author: Hadrien Lejard From 206f8454c6bdb2267f07e462898a1d80dbd4bf38 Mon Sep 17 00:00:00 2001 From: Ivan Terekhin Date: Mon, 31 Jan 2022 17:02:37 +0300 Subject: [PATCH 05/60] Release 4.0.5 (#325) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ivan Terekhin Co-authored-by: István Juhos Co-authored-by: Youssef Raafat Co-authored-by: luis901101 Co-authored-by: melvspace Co-authored-by: Michal Šrůtek <35694712+michalsrutek@users.noreply.github.com> Co-authored-by: Andre Co-authored-by: John Wimer Co-authored-by: Max Röhrl --- .github/workflows/dart.yml | 167 +++++++++++++---------- chopper/CHANGELOG.md | 4 + chopper/example/main.dart | 1 - chopper/lib/src/authenticator.dart | 3 +- chopper/lib/src/base.dart | 2 +- chopper/mono_pkg.yaml | 10 +- chopper/pubspec.yaml | 3 +- 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/CHANGELOG.md | 5 + chopper_generator/lib/src/generator.dart | 3 +- chopper_generator/mono_pkg.yaml | 8 +- chopper_generator/pubspec.yaml | 5 +- docs/ci/ci_setup.md | 9 ++ interceptors.md | 4 +- tool/ci.sh | 59 ++++---- 19 files changed, 170 insertions(+), 134 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/CHANGELOG.md b/chopper/CHANGELOG.md index b8d2c824..5b23c6a2 100644 --- a/chopper/CHANGELOG.md +++ b/chopper/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 4.0.5 + +- Add additional param for the authenticator + ## 4.0.4 - Fix for authenticator usage 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/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( 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 6b78fdcf..95a5dc3d 100644 --- a/chopper/pubspec.yaml +++ b/chopper/pubspec.yaml @@ -1,9 +1,8 @@ name: chopper description: Chopper is an http client generator using source_gen, inspired by Retrofit -version: 4.0.4 +version: 4.0.5 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/CHANGELOG.md b/chopper_generator/CHANGELOG.md index 4c36fe03..0407a402 100644 --- a/chopper_generator/CHANGELOG.md +++ b/chopper_generator/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## 4.0.5 + +- Analyzer dependency upgrade +- Fix for warnings + ## 4.0.3 - Interpolation fixes diff --git a/chopper_generator/lib/src/generator.dart b/chopper_generator/lib/src/generator.dart index 19630d2f..ccf42daf 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'; @@ -83,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)}'); } 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 78beea6b..a1bb399f 100644 --- a/chopper_generator/pubspec.yaml +++ b/chopper_generator/pubspec.yaml @@ -1,15 +1,14 @@ name: chopper_generator description: Chopper is an http client generator using source_gen, inspired by Retrofit -version: 4.0.3 +version: 4.0.5 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/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: {}), ] 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 f672b1088dee563016307b8df91941295ccbed0d Mon Sep 17 00:00:00 2001 From: Ivan Terekhin Date: Wed, 22 Jun 2022 17:35:25 +0300 Subject: [PATCH 06/60] Release 4.0.6 (#341) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ivan Terekhin Co-authored-by: István Juhos Co-authored-by: Meysam Karimi Co-authored-by: Youssef Raafat Co-authored-by: luis901101 Co-authored-by: melvspace Co-authored-by: Michal Šrůtek <35694712+michalsrutek@users.noreply.github.com> Co-authored-by: Andre Co-authored-by: John Wimer Co-authored-by: Max Röhrl Co-authored-by: ipcjs Co-authored-by: ibadin Co-authored-by: Meysam Karimi <31154534+meysam1717@users.noreply.github.com> --- README.md | 1 - chopper/CHANGELOG.md | 5 + chopper/README.md | 1 - chopper/lib/src/annotations.dart | 38 ++ chopper/pubspec.yaml | 2 +- chopper_generator/CHANGELOG.md | 4 + chopper_generator/lib/src/generator.dart | 50 ++- chopper_generator/pubspec.yaml | 4 +- example/bin/main_built_value.dart | 17 +- example/bin/main_json_serializable.dart | 12 +- 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 -- 22 files changed, 653 insertions(+), 195 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/CHANGELOG.md b/chopper/CHANGELOG.md index 5b23c6a2..fbe89040 100644 --- a/chopper/CHANGELOG.md +++ b/chopper/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## 4.0.6 + +- FieldMap added +- Example migrated to null safety / Angular removed + ## 4.0.5 - Add additional param for the authenticator 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/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/pubspec.yaml b/chopper/pubspec.yaml index 95a5dc3d..9818fc66 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.5 +version: 4.0.6 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 0407a402..3e27a067 100644 --- a/chopper_generator/CHANGELOG.md +++ b/chopper_generator/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 4.0.6 + +- Analyzer dependency upgrade + ## 4.0.5 - Analyzer dependency upgrade 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' diff --git a/chopper_generator/pubspec.yaml b/chopper_generator/pubspec.yaml index a1bb399f..8c98c3b8 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.5 +version: 4.0.6 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 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..0d09e8f8 100644 --- a/example/bin/main_json_serializable.dart +++ b/example/bin/main_json_serializable.dart @@ -60,14 +60,14 @@ 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) { + 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) { - if (entity is Iterable) return _decodeList(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; } 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 140849cc18bc06bd82f46b0da4b6e4b794f07a49 Mon Sep 17 00:00:00 2001 From: Ivan Terekhin Date: Tue, 13 Sep 2022 15:33:54 +0300 Subject: [PATCH 07/60] Release 5.0.0 (#353) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ivan Terekhin Co-authored-by: István Juhos Co-authored-by: Meysam Karimi Co-authored-by: Youssef Raafat Co-authored-by: luis901101 Co-authored-by: melvspace Co-authored-by: Michal Šrůtek <35694712+michalsrutek@users.noreply.github.com> Co-authored-by: Andre Co-authored-by: John Wimer Co-authored-by: Max Röhrl Co-authored-by: ipcjs Co-authored-by: ibadin Co-authored-by: Meysam Karimi <31154534+meysam1717@users.noreply.github.com> Co-authored-by: Klemen Tusar Co-authored-by: Klemen Tusar --- chopper/CHANGELOG.md | 18 +- chopper/analysis_options.yaml | 35 +- chopper/example/definition.chopper.dart | 4 +- 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 | 113 +++-- chopper/lib/src/request.dart | 83 ++-- chopper/lib/src/response.dart | 2 +- chopper/lib/src/utils.dart | 24 +- chopper/pubspec.yaml | 7 +- chopper/test/base_test.dart | 204 ++++++++- chopper/test/client_test.dart | 7 +- chopper/test/converter_test.dart | 37 +- 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.chopper.dart | 40 +- chopper/test/test_service.dart | 26 +- chopper_built_value/CHANGELOG.md | 4 + chopper_built_value/analysis_options.yaml | 35 +- .../lib/chopper_built_value.dart | 41 +- chopper_built_value/pubspec.yaml | 9 +- chopper_built_value/test/converter_test.dart | 24 +- 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/CHANGELOG.md | 11 +- chopper_generator/analysis_options.yaml | 36 +- chopper_generator/lib/chopper_generator.dart | 1 + chopper_generator/lib/src/generator.dart | 397 ++++++++++-------- chopper_generator/pubspec.yaml | 15 +- example/analysis_options.yaml | 32 ++ example/bin/main_built_value.dart | 44 +- example/bin/main_json_serializable.dart | 35 +- 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 | 95 ++++- example/pubspec.yaml | 6 +- 47 files changed, 1170 insertions(+), 711 deletions(-) create mode 100644 example/analysis_options.yaml diff --git a/chopper/CHANGELOG.md b/chopper/CHANGELOG.md index fbe89040..e78e1909 100644 --- a/chopper/CHANGELOG.md +++ b/chopper/CHANGELOG.md @@ -1,22 +1,8 @@ # Changelog -## 4.0.6 +## 5.0.0 -- FieldMap added -- Example migrated to null safety / Angular removed - -## 4.0.5 - -- Add additional param for the authenticator - -## 4.0.4 - -- Fix for authenticator usage - -## 4.0.3 - -- Fix for authenticator usage -- Null-safety fixes +- API breaking changes (FutureOr) ## 4.0.1 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.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/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 5c89cbc3..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; } - Response decodeJson(Response response) { - final supportedContentTypes = [jsonHeaders, jsonApiHeaders]; + FutureOr decodeJson(Response response) async { + final List supportedContentTypes = [jsonHeaders, jsonApiHeaders]; - final contentType = response.headers[contentTypeKey]; + final String? contentType = response.headers[contentTypeKey]; var body = response.body; if (supportedContentTypes.contains(contentType)) { @@ -264,7 +263,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,32 +274,35 @@ 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) { chopperLogger.warning(e); + return data; } } @override - Response convertError(Response response) => - decodeJson(response); + FutureOr convertError( + Response response, + ) async => + await decodeJson(response); - static Response responseFactory( + static FutureOr> responseFactory( Response response, - ) { - return const JsonConverter().convertResponse(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,22 +326,19 @@ 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; } @override - Response convertResponse(Response response) => + FutureOr> convertResponse( + 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 9818fc66..96817bb9 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: 4.0.6 +version: 5.0.0 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 b591168a..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')); @@ -591,6 +598,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); @@ -682,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 { @@ -699,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); }); @@ -713,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 7bc98b58..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(), @@ -67,39 +68,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'})); }); @@ -111,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 { @@ -128,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.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..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'; @@ -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); @@ -82,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') @@ -121,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/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/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 0468d0a0..7beef449 100644 --- a/chopper_built_value/lib/chopper_built_value.dart +++ b/chopper_built_value/lib/chopper_built_value.dart @@ -1,13 +1,14 @@ -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. 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 @@ -16,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['\$']); } @@ -33,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)); } @@ -42,27 +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 - Response convertResponse(Response response) { - final jsonResponse = jsonConverter.convertResponse(response); - final body = deserialize(jsonResponse.body); - return jsonResponse.copyWith(body: body); + FutureOr> convertResponse( + Response response, + ) async { + final Response jsonResponse = await jsonConverter.convertResponse(response); + + return jsonResponse.copyWith( + body: deserialize(jsonResponse.body), + ); } @override - Response convertError(Response response) { - final jsonResponse = jsonConverter.convertResponse(response); + FutureOr convertError( + 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..0bd63c23 100644 --- a/chopper_built_value/pubspec.yaml +++ b/chopper_built_value/pubspec.yaml @@ -1,16 +1,16 @@ 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 environment: - sdk: ">=2.12.0 <3.0.0" + sdk: ">=2.17.0 <3.0.0" 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: @@ -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/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_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/CHANGELOG.md b/chopper_generator/CHANGELOG.md index 3e27a067..22074e42 100644 --- a/chopper_generator/CHANGELOG.md +++ b/chopper_generator/CHANGELOG.md @@ -1,17 +1,12 @@ # Changelog -## 4.0.6 +## 5.0.0 -- Analyzer dependency upgrade - -## 4.0.5 - -- Analyzer dependency upgrade -- Fix for warnings +- API breaking changes (FutureOr usage) ## 4.0.3 -- Interpolation fixes +- Analyzer dependency upgrade ## 4.0.2 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 a3ae9880..13b3c606 100644 --- a/chopper_generator/lib/src/generator.dart +++ b/chopper_generator/lib/src/generator.dart @@ -1,31 +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 @@ -35,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`.', @@ -61,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) @@ -81,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; @@ -163,7 +173,7 @@ class ChopperGenerator extends GeneratorForAnnotation { m.parameters.where((p) => p.isNamed).map(buildNamedParam), ); - final blocks = [ + final List blocks = [ url.assignFinal(_urlVar).statement, ]; @@ -171,29 +181,48 @@ class ChopperGenerator extends GeneratorForAnnotation { blocks.add(_generateMap(queries).assignFinal(_parametersVar).statement); } - final hasQueryMap = queryMap.isNotEmpty; + // 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), + ].where((el) => el.type.isNullable).map((el) => el.name); + + final bool 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, ); } } - 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( @@ -206,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( @@ -221,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( @@ -241,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( @@ -275,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') @@ -306,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; } @@ -346,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, @@ -378,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) || @@ -408,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}}'); }); @@ -439,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); @@ -468,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); }); @@ -481,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), ]; @@ -505,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,'); @@ -525,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) { @@ -539,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'); } } @@ -567,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 8c98c3b8..a5804be1 100644 --- a/chopper_generator/pubspec.yaml +++ b/chopper_generator/pubspec.yaml @@ -1,26 +1,27 @@ name: chopper_generator description: Chopper is an http client generator using source_gen, inspired by Retrofit -version: 4.0.6 +version: 5.0.0 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 + 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 + chopper: ^5.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 8ebaff5b..9f87becc 100644 --- a/example/bin/main_built_value.dart +++ b/example/bin/main_built_value.dart @@ -1,28 +1,33 @@ +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: [ @@ -33,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(); @@ -44,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); @@ -56,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( @@ -75,19 +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 - Response convertResponse(Response response) { + FutureOr> convertResponse( + Response response, + ) async { // use [JsonConverter] to decode json - final jsonRes = 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 0d09e8f8..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); @@ -91,20 +94,24 @@ 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)); } @override // all objects should implements toJson method + // ignore: unnecessary_overrides Request convertRequest(Request request) => super.convertRequest(request); - Response convertError(Response response) { + @override + 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), 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..f9411798 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: @@ -112,21 +126,14 @@ packages: path: "../chopper" relative: true source: path - version: "4.0.1" + version: "4.1.0" 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" + 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 e26eef464885a93977886cf2f7e6664bb8ef41e7 Mon Sep 17 00:00:00 2001 From: Ivan Terekhin Date: Tue, 13 Sep 2022 17:31:12 +0300 Subject: [PATCH 08/60] Chopper-Gen 5.0.0+1 (#355) --- 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 3186982d7597898d67ed71adafb8c7e5bc7afc67 Mon Sep 17 00:00:00 2001 From: Ivan Terekhin Date: Sat, 8 Oct 2022 20:00:58 +0300 Subject: [PATCH 09/60] Release 5.0.1 (#368) Co-authored-by: Klemen Tusar --- .github/ISSUE_TEMPLATE/bug_report.md | 69 +++ chopper/Makefile | 61 +++ chopper/example/definition.chopper.dart | 102 +++- 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 | 481 ++++++++++++++---- chopper/test/test_service.dart | 20 + chopper/test/utils_test.dart | 277 ++++++++++ chopper_built_value/Makefile | 61 +++ chopper_generator/Makefile | 46 ++ chopper_generator/analysis_options.yaml | 4 +- chopper_generator/lib/src/generator.dart | 112 ++-- chopper_generator/pubspec.yaml | 2 +- example/Makefile | 46 ++ ...son_serializable_squadron_worker_pool.dart | 164 ++++++ example/lib/built_value_resource.chopper.dart | 47 +- .../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/lib/json_serializable.chopper.dart | 62 ++- example/pubspec.lock | 474 ----------------- example/pubspec.yaml | 2 + faq.md | 194 +++++++ tool/makefile_helpers.sh | 27 + 27 files changed, 1882 insertions(+), 713 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 chopper/Makefile create mode 100644 chopper/test/utils_test.dart create mode 100644 chopper_built_value/Makefile create mode 100644 chopper_generator/Makefile create mode 100644 example/Makefile 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 delete mode 100644 example/pubspec.lock create mode 100644 tool/makefile_helpers.sh 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 + + + +``` +``` + + + +``` +``` + + + +``` +``` + +
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/example/definition.chopper.dart b/chopper/example/definition.chopper.dart index f43a754d..884b3470 100644 --- a/chopper/example/definition.chopper.dart +++ b/chopper/example/definition.chopper.dart @@ -18,60 +18,110 @@ class _$MyService extends MyService { @override Future> getResource(String id) { - final $url = '/resources/${id}'; - final $request = Request('GET', $url, client.baseUrl); + final String $url = '/resources/${id}'; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + ); return client.send($request); } @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('GET', $url, client.baseUrl, - parameters: $params, headers: $headers); + final Request $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 String $url = '/resources/resources'; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + ); return client .send>, Map>($request); } @override - Future> postResourceUrlEncoded(String toto, String b) { - final $url = '/resources/'; - final $body = {'a': toto, 'b': b}; - final $request = Request('POST', $url, client.baseUrl, body: $body); + Future> postResourceUrlEncoded( + String toto, + String b, + ) { + final String $url = '/resources/'; + final $body = { + 'a': toto, + 'b': b, + }; + final Request $request = Request( + 'POST', + $url, + client.baseUrl, + body: $body, + ); return client.send($request); } @override Future> postResources( - Map a, Map b, String c) { - final $url = '/resources/multi'; - final $parts = [ - PartValue>('1', a), - PartValue>('2', b), - PartValue('3', c) + Map a, + Map b, + String c, + ) { + final String $url = '/resources/multi'; + final List $parts = [ + PartValue>( + '1', + a, + ), + PartValue>( + '2', + b, + ), + PartValue( + '3', + c, + ), ]; - final $request = - Request('POST', $url, client.baseUrl, parts: $parts, multipart: true); + final Request $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 String $url = '/resources/file'; + final List $parts = [ + PartValue>( + 'file', + bytes, + ) + ]; + final Request $request = Request( + 'POST', + $url, + client.baseUrl, + parts: $parts, + multipart: true, + ); return client.send($request); } } 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 f1fd6769..d0a43b5c 100644 --- a/chopper/test/test_service.chopper.dart +++ b/chopper/test/test_service.chopper.dart @@ -17,276 +17,531 @@ class _$HttpTestService extends HttpTestService { final definitionType = HttpTestService; @override - Future> getTest(String id, {required String dynamicHeader}) { - final $url = '/test/get/${id}'; - final $headers = { + Future> getTest( + String id, { + required String dynamicHeader, + }) { + final String $url = '/test/get/${id}'; + final Map $headers = { 'test': dynamicHeader, }; - - final $request = Request('GET', $url, client.baseUrl, headers: $headers); + final Request $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 String $url = '/test/head'; + final Request $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 String $url = '/test/options'; + final Request $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 String $url = '/test/get'; + final Request $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 String $url = '/test'; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + ); return client.send($request); } @override Future> getAllWithTrailingSlash() { - final $url = '/test/'; - final $request = Request('GET', $url, client.baseUrl); + final String $url = '/test/'; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + ); return client.send($request); } @override - Future> getQueryTest( - {String name = '', int? number, int? def = 42}) { - final $url = '/test/query'; - final $params = { + Future> getQueryTest({ + String name = '', + int? number, + int? def = 42, + }) { + final String $url = '/test/query'; + final Map $params = { 'name': name, 'int': number, - 'default_value': def + 'default_value': def, }; - final $request = Request('GET', $url, client.baseUrl, parameters: $params); + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + ); return client.send($request); } @override Future> getQueryMapTest(Map query) { - final $url = '/test/query_map'; - final $params = query; - final $request = Request('GET', $url, client.baseUrl, parameters: $params); + final String $url = '/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 $url = '/test/query_map'; - final $params = {'test': test}; + Future> getQueryMapTest2( + Map query, { + bool? test, + }) { + final String $url = '/test/query_map'; + final Map $params = {'test': test}; $params.addAll(query); - final $request = Request('GET', $url, client.baseUrl, parameters: $params); + 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 $url = '/test/query_map'; - final $params = {'name': name, 'number': number}; + Future> getQueryMapTest3({ + String name = '', + int? number, + Map filters = const {}, + }) { + final String $url = '/test/query_map'; + final Map $params = { + 'name': name, + 'number': number, + }; $params.addAll(filters); - final $request = Request('GET', $url, client.baseUrl, parameters: $params); + final Request $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); + Future> getQueryMapTest4({ + String name = '', + int? number, + Map? filters, + }) { + final String $url = '/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 $url = '/test/query_map'; - final $params = filters ?? {}; - final $request = Request('GET', $url, client.baseUrl, parameters: $params); + final String $url = '/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 $url = '/test/get_body'; + final String $url = '/test/get_body'; final $body = body; - final $request = Request('GET', $url, client.baseUrl, body: $body); + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + body: $body, + ); return client.send($request); } @override Future> postTest(String data) { - final $url = '/test/post'; + final String $url = '/test/post'; final $body = data; - final $request = Request('POST', $url, client.baseUrl, body: $body); + final Request $request = Request( + 'POST', + $url, + client.baseUrl, + body: $body, + ); return client.send($request); } @override Future> postStreamTest(Stream> byteStream) { - final $url = '/test/post'; + final String $url = '/test/post'; final $body = byteStream; - final $request = Request('POST', $url, client.baseUrl, body: $body); + final Request $request = Request( + 'POST', + $url, + client.baseUrl, + body: $body, + ); return client.send($request); } @override - Future> putTest(String test, String data) { - final $url = '/test/put/${test}'; + Future> putTest( + String test, + String data, + ) { + final String $url = '/test/put/${test}'; final $body = data; - final $request = Request('PUT', $url, client.baseUrl, body: $body); + final Request $request = Request( + 'PUT', + $url, + client.baseUrl, + body: $body, + ); return client.send($request); } @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('DELETE', $url, client.baseUrl, headers: $headers); + final Request $request = Request( + 'DELETE', + $url, + client.baseUrl, + headers: $headers, + ); return client.send($request); } @override - Future> patchTest(String id, String data) { - final $url = '/test/patch/${id}'; + Future> patchTest( + String id, + String data, + ) { + final String $url = '/test/patch/${id}'; final $body = data; - final $request = Request('PATCH', $url, client.baseUrl, body: $body); + final Request $request = Request( + 'PATCH', + $url, + client.baseUrl, + body: $body, + ); return client.send($request); } @override Future> mapTest(Map map) { - final $url = '/test/map'; + final String $url = '/test/map'; final $body = map; - final $request = Request('POST', $url, client.baseUrl, body: $body); + final Request $request = Request( + 'POST', + $url, + client.baseUrl, + body: $body, + ); return client.send($request); } @override Future> postForm(Map fields) { - final $url = '/test/form/body'; + final String $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 = Request( + 'POST', + $url, + client.baseUrl, + body: $body, + ); + return client.send( + $request, + requestConverter: convertForm, + ); } @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('POST', $url, client.baseUrl, body: $body, headers: $headers); + final Request $request = Request( + 'POST', + $url, + client.baseUrl, + body: $body, + headers: $headers, + ); return client.send($request); } @override - 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); + Future> postFormFields( + String foo, + int bar, + ) { + final String $url = '/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 $url = '/test/map/json'; + final String $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 = Request( + 'POST', + $url, + client.baseUrl, + body: $body, + ); + return client.send( + $request, + requestConverter: customConvertRequest, + responseConverter: customConvertResponse, + ); } @override Future> postResources( - Map a, Map b) { - final $url = '/test/multi'; - final $parts = [ - PartValue>('1', a), - PartValue>('2', b) + Map a, + Map b, + ) { + final String $url = '/test/multi'; + final List $parts = [ + PartValue>( + '1', + a, + ), + PartValue>( + '2', + b, + ), ]; - final $request = - Request('POST', $url, client.baseUrl, parts: $parts, multipart: true); + final Request $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 String $url = '/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> postMultipartFile(MultipartFile file, - {String? id}) { - final $url = '/test/file'; - final $parts = [ - PartValue('id', id), - PartValueFile('file', file) + Future> postMultipartFile( + MultipartFile file, { + String? id, + }) { + final String $url = '/test/file'; + final List $parts = [ + PartValue( + 'id', + id, + ), + PartValueFile( + 'file', + file, + ), ]; - final $request = - Request('POST', $url, client.baseUrl, parts: $parts, multipart: true); + final Request $request = Request( + 'POST', + $url, + client.baseUrl, + parts: $parts, + multipart: true, + ); return client.send($request); } @override Future> postListFiles(List files) { - final $url = '/test/files'; - final $parts = [ - PartValueFile>('files', files) + final String $url = '/test/files'; + final List $parts = [ + PartValueFile>( + 'files', + files, + ) ]; - final $request = - Request('POST', $url, client.baseUrl, parts: $parts, multipart: true); + final Request $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 String $url = 'https://test.com'; + final Request $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 String $url = '/test/list/string'; + final Request $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 String $url = '/test/no-body'; + final Request $request = Request( + 'POST', + $url, + client.baseUrl, + ); 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_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/chopper_generator/analysis_options.yaml b/chopper_generator/analysis_options.yaml index 57256269..3a82dc3b 100644 --- a/chopper_generator/analysis_options.yaml +++ b/chopper_generator/analysis_options.yaml @@ -14,8 +14,8 @@ dart_code_metrics: cyclomatic-complexity: 20 number-of-arguments: 4 maximum-nesting-level: 5 - number-of-parameters: 5 - source-lines-of-code: 200 + number-of-parameters: 6 + 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..6dda3c4a 100644 --- a/chopper_generator/lib/src/generator.dart +++ b/chopper_generator/lib/src/generator.dart @@ -174,11 +174,15 @@ class ChopperGenerator extends GeneratorForAnnotation { ); final List blocks = [ - url.assignFinal(_urlVar).statement, + declareFinal(_urlVar, type: refer('String')).assign(url).statement, ]; if (queries.isNotEmpty) { - blocks.add(_generateMap(queries).assignFinal(_parametersVar).statement); + blocks.add( + declareFinal(_parametersVar, type: refer('Map')) + .assign(_generateMap(queries)) + .statement, + ); } // Build an iterable of all the parameters that are nullable @@ -194,21 +198,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, type: refer('Map')) + .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 +229,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 +246,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 +256,25 @@ class ChopperGenerator extends GeneratorForAnnotation { bool hasParts = multipart && (parts.isNotEmpty || fileFields.isNotEmpty); if (hasParts) { blocks.add( - _generateList(parts, fileFields).assignFinal(_partsVar).statement, + declareFinal(_partsVar, type: refer('List')) + .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, type: refer('List')) + .assign(refer(partMap.keys.first)) + .statement, ); } } @@ -273,12 +282,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, type: refer('List')) + .assign(refer(fileFieldMap.keys.first)) + .statement, ); } } @@ -296,13 +309,22 @@ class ChopperGenerator extends GeneratorForAnnotation { ); } - blocks.add(_generateRequest( - method, - hasBody: hasBody, - useQueries: hasQuery, - useHeaders: headers != null, - hasParts: hasParts, - ).assignFinal(_requestVar).statement); + final bool useBrackets = getUseBrackets(method); + + blocks.add( + declareFinal(_requestVar, type: refer('Request')) + .assign( + _generateRequest( + method, + hasBody: hasBody, + useQueries: hasQuery, + useHeaders: headers != null, + hasParts: hasParts, + useBrackets: useBrackets, + ), + ) + .statement, + ); final Map namedArguments = {}; @@ -339,13 +361,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) { @@ -475,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)), @@ -501,6 +520,10 @@ class ChopperGenerator extends GeneratorForAnnotation { namedParams['headers'] = refer(_headersVar); } + if (useBrackets) { + namedParams['useBrackets'] = literalBool(useBrackets); + } + return refer('Request').newInstance(params, namedParams); } @@ -527,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) { @@ -578,10 +603,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; } } @@ -599,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; } diff --git a/chopper_generator/pubspec.yaml b/chopper_generator/pubspec.yaml index 588ed462..96bcf513 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.1.0 + analyzer: ^4.4.0 build: ^2.0.0 built_collection: ^5.0.0 chopper: ^5.0.0 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/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/built_value_resource.chopper.dart b/example/lib/built_value_resource.chopper.dart index c092d4b0..a68d4b5c 100644 --- a/example/lib/built_value_resource.chopper.dart +++ b/example/lib/built_value_resource.chopper.dart @@ -18,39 +18,58 @@ class _$MyService extends MyService { @override Future> getResource(String id) { - final $url = '/resources/${id}/'; - final $request = Request('GET', $url, client.baseUrl); + final String $url = '/resources/${id}/'; + final Request $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 String $url = '/resources/list'; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + ); return client.send, Resource>($request); } @override Future> getTypedResource() { - final $url = '/resources/'; - final $headers = { + final String $url = '/resources/'; + final Map $headers = { 'foo': 'bar', }; - - final $request = Request('GET', $url, client.baseUrl, headers: $headers); + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + headers: $headers, + ); return client.send($request); } @override - Future> newResource(Resource resource, {String? name}) { - final $url = '/resources'; - final $headers = { + Future> newResource( + Resource resource, { + String? name, + }) { + final String $url = '/resources'; + final Map $headers = { if (name != null) 'name': name, }; - final $body = resource; - final $request = - Request('POST', $url, client.baseUrl, body: $body, headers: $headers); + final Request $request = Request( + 'POST', + $url, + client.baseUrl, + body: $body, + headers: $headers, + ); return client.send($request); } } 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/lib/json_serializable.chopper.dart b/example/lib/json_serializable.chopper.dart index bd44d2f3..e9892bfc 100644 --- a/example/lib/json_serializable.chopper.dart +++ b/example/lib/json_serializable.chopper.dart @@ -18,51 +18,75 @@ class _$MyService extends MyService { @override Future> getResource(String id) { - final $url = '/resources/${id}/'; - final $request = Request('GET', $url, client.baseUrl); + final String $url = '/resources/${id}/'; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + ); return client.send($request); } @override Future>> getResources() { - final $url = '/resources/all'; - final $headers = { + final String $url = '/resources/all'; + final Map $headers = { 'test': 'list', }; - - final $request = Request('GET', $url, client.baseUrl, headers: $headers); + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + headers: $headers, + ); return client.send, Resource>($request); } @override Future>> getMapResource(String id) { - final $url = '/resources/'; - final $params = {'id': id}; - final $request = Request('GET', $url, client.baseUrl, parameters: $params); + final String $url = '/resources/'; + final Map $params = {'id': id}; + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + parameters: $params, + ); return client.send, Map>($request); } @override Future> getTypedResource() { - final $url = '/resources/'; - final $headers = { + final String $url = '/resources/'; + final Map $headers = { 'foo': 'bar', }; - - final $request = Request('GET', $url, client.baseUrl, headers: $headers); + final Request $request = Request( + 'GET', + $url, + client.baseUrl, + headers: $headers, + ); return client.send($request); } @override - Future> newResource(Resource resource, {String? name}) { - final $url = '/resources'; - final $headers = { + Future> newResource( + Resource resource, { + String? name, + }) { + final String $url = '/resources'; + final Map $headers = { if (name != null) 'name': name, }; - final $body = resource; - final $request = - Request('POST', $url, client.baseUrl, body: $body, headers: $headers); + final Request $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" 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 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 28da5efade3c609390cde3391f1f7654a5e871ec Mon Sep 17 00:00:00 2001 From: Ivan Terekhin Date: Sat, 8 Oct 2022 20:14:04 +0300 Subject: [PATCH 10/60] Release 5.0.1 (#369) --- 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 96bcf513..2e8c2bd4 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 0e5f98669e5fa9b12a55f5b3480ac92adc5517a2 Mon Sep 17 00:00:00 2001 From: Ivan Terekhin Date: Sat, 15 Oct 2022 09:50:30 +0300 Subject: [PATCH 11/60] Release 5.1.0 (master) (#374) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix Header Option Casting (#260) Co-authored-by: Ivan Terekhin * Fix for #259 (#263) * 4.0.1 fixes (#264) * analyzer dependency upgraded (#296) * fix(generator): fix PartValueFile value not nullable if arg is (#288) (#293) * Chopper generator release 4.0.2 (#297) * fix: fix this.body cast of null value when response body is null (#291) (#292) * Interpolation fixes (#275) * encodeQueryComponent now encodeComponent (#278) * Prevent double call on token refreshment (#276) * Fixes for #309 #308 (#310) * Remove new keyword from interceptors.md (#312) * Analyzer upgrade (#320) Co-authored-by: István Juhos * Add unnecessary_brace_in_string_interps to lint ignores (#317) * Extend pragma to quiet the linter (#318) Co-authored-by: Ivan Terekhin * Fix converter getting called twice if using an authenticator with a JsonConverter on the request (#324) * migrate example to nullsafety (#331) * Resolve problem in main_json_serializable example (#328) * Add @FiledMap @PartMap @PartFileMap (#335) Co-authored-by: Meysam Karimi * Upgrade of analyzer (#340) * Fix nullable QueryMap fails to compile (#344) * Change return type of decodeJson to FutureOr in order to be able to support compute() (#345) * Migrate from pedantic to lints ^2.0.0 with lints/recommended.yaml (#349) * Version bumped for release (#352) * Revert analyzer to ^4.1.0 and silence linters for Element.enclosingElement (#354) * [chopper_generator] Update analyzer to ^4.4.0 and code_builde to ^4.3.0 and migrate deprecated code (#358) * Add Makefiles to streamline development (#357) * Add Bug Report Github issue template (#359) * [chopper_generator] Add types to the generated variables (#360) * Provide an example using an Isolate Worker Pool with Squadron (#361) * mapToQuery changes (#364) * Version bumped / changelog update (#367) * Request extends http.BaseRequest (#370) * Exclude null query vars by default and add new @Method annotation includeNullQueryVars (#372) * 5.1.0 (dev) (#373) Co-authored-by: Ivan Terekhin <231950+JEuler@users.noreply.github.com> Co-authored-by: Youssef Raafat Co-authored-by: luis901101 Co-authored-by: melvspace Co-authored-by: Michal Šrůtek <35694712+michalsrutek@users.noreply.github.com> Co-authored-by: István Juhos Co-authored-by: Andre Co-authored-by: John Wimer Co-authored-by: Max Röhrl Co-authored-by: ipcjs Co-authored-by: ibadin Co-authored-by: Meysam Karimi <31154534+meysam1717@users.noreply.github.com> Co-authored-by: Meysam Karimi Co-authored-by: Klemen Tusar Co-authored-by: Klemen Tusar Co-authored-by: Ivan Terekhin <231950+JEuler@users.noreply.github.com> --- chopper/CHANGELOG.md | 5 + chopper/lib/chopper.dart | 1 + chopper/lib/src/annotations.dart | 41 ++- 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 | 319 ++++++++++--------- chopper/lib/src/utils.dart | 24 +- chopper/pubspec.yaml | 9 +- chopper/test/base_test.dart | 230 ++++++++++--- 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/test/test_service.chopper.dart | 37 +++ chopper/test/test_service.dart | 15 + chopper/test/utils_test.dart | 305 +++++++++++++++++- chopper_built_value/test/converter_test.dart | 12 +- chopper_generator/CHANGELOG.md | 4 + chopper_generator/analysis_options.yaml | 2 +- chopper_generator/lib/src/generator.dart | 11 + chopper_generator/pubspec.yaml | 4 +- 23 files changed, 1117 insertions(+), 261 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/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/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..5ddda966 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. @@ -170,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, }); } @@ -187,6 +213,7 @@ class Get extends Method { super.path, super.headers, super.useBrackets, + super.includeNullQueryVars, }) : super(HttpMethod.Get); } @@ -200,6 +227,7 @@ class Post extends Method { super.path, super.headers, super.useBrackets, + super.includeNullQueryVars, }) : super(HttpMethod.Post); } @@ -211,6 +239,7 @@ class Delete extends Method { super.path, super.headers, super.useBrackets, + super.includeNullQueryVars, }) : super(HttpMethod.Delete); } @@ -224,6 +253,7 @@ class Put extends Method { super.path, super.headers, super.useBrackets, + super.includeNullQueryVars, }) : super(HttpMethod.Put); } @@ -236,6 +266,7 @@ class Patch extends Method { super.path, super.headers, super.useBrackets, + super.includeNullQueryVars, }) : super(HttpMethod.Patch); } @@ -247,6 +278,7 @@ class Head extends Method { super.path, super.headers, super.useBrackets, + super.includeNullQueryVars, }) : super(HttpMethod.Head); } @@ -257,6 +289,7 @@ class Options extends Method { super.path, super.headers, super.useBrackets, + super.includeNullQueryVars, }) : super(HttpMethod.Options); } 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..f4189cc9 100644 --- a/chopper/lib/src/request.dart +++ b/chopper/lib/src/request.dart @@ -1,64 +1,127 @@ 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; + final bool includeNullQueryVars; - 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, + 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.body, + Map? parameters, + Map headers = const {}, this.multipart = false, this.parts = const [], this.useBrackets = false, - }); + this.includeNullQueryVars = false, + }) : origin = url.origin, + path = url.path, + parameters = {...url.queryParametersAll, ...?parameters}, + super( + method, + buildUri( + url.origin, + url.path, + {...url.queryParametersAll, ...?parameters}, + useBrackets: useBrackets, + includeNullQueryVars: includeNullQueryVars, + ), + ) { + 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, + bool? includeNullQueryVars, }) => 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, + includeNullQueryVars: includeNullQueryVars ?? this.includeNullQueryVars, ); - 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, + 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 String query = mapToQuery( + parameters, + useBrackets: useBrackets, + includeNullQueryVars: includeNullQueryVars, + ); + + return query.isNotEmpty + ? uri.replace(query: uri.hasQuery ? '${uri.query}&$query' : query) + : uri; + } /// Converts this Chopper Request into a [http.BaseRequest]. /// @@ -69,18 +132,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 +240,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/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/pubspec.yaml b/chopper/pubspec.yaml index 9033bc29..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 @@ -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..42b2a39c 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( @@ -107,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')); @@ -129,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')); @@ -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,13 +909,44 @@ 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); - } + expect( + () async { + try { + await service + .getTest('1234', dynamicHeader: '') + .timeout(const Duration(seconds: 3)); + } finally { + httpClient.close(); + } + }, + throwsA(isA()), + ); + }); + + 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(); }); @@ -1033,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/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/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_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')); 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/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; } diff --git a/chopper_generator/pubspec.yaml b/chopper_generator/pubspec.yaml index 2e8c2bd4..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 @@ -12,7 +12,7 @@ dependencies: 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 From 1267147cb51bca5a6a949d5fa14e757157803dc2 Mon Sep 17 00:00:00 2001 From: Ivan Terekhin Date: Mon, 17 Oct 2022 09:53:02 +0300 Subject: [PATCH 12/60] 5.1.0+1 (#379) --- .github/workflows/dart.yml | 85 +++++++++++++++++++--------------- .github/workflows/publish.yml | 12 ++--- chopper_generator/CHANGELOG.md | 4 ++ chopper_generator/pubspec.yaml | 4 +- mono_repo.yaml | 4 +- tool/ci.sh | 2 +- 6 files changed, 63 insertions(+), 48 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/chopper_generator/CHANGELOG.md b/chopper_generator/CHANGELOG.md index 185ca6c8..011b042e 100644 --- a/chopper_generator/CHANGELOG.md +++ b/chopper_generator/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 5.1.0+1 + +- Analyzer upgrade + ## 5.1.0 - Annotation to include null vars in query diff --git a/chopper_generator/pubspec.yaml b/chopper_generator/pubspec.yaml index 6937cc90..f92dc5e4 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.1.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.4.0 + analyzer: '>=4.4.0 <6.0.0' build: ^2.0.0 built_collection: ^5.0.0 chopper: ^5.0.0 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 7221844df9a1a2327c139cc94fe3cd8dd80d88ec Mon Sep 17 00:00:00 2001 From: Ivan Terekhin Date: Tue, 18 Oct 2022 10:18:50 +0300 Subject: [PATCH 13/60] Update mono_repo to 6.4.2 (#380) (#381) Co-authored-by: Klemen Tusar --- .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 bf73cf2a84848db005251e90ad7c7224f65e0763 Mon Sep 17 00:00:00 2001 From: Erlang Parasu Date: Sat, 10 Dec 2022 21:03:03 +0800 Subject: [PATCH 14/60] Update getting-started.md (#389) add nullable example static create --- 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 81cf57655bd85ba5878624a9c7391a04cc626c3b Mon Sep 17 00:00:00 2001 From: Ivan Terekhin Date: Tue, 13 Dec 2022 16:02:02 +0300 Subject: [PATCH 15/60] 5.2.0.release (#393) --- .gitignore | 3 +- README.md | 3 +- chopper/CHANGELOG.md | 4 + chopper/example/definition.chopper.dart | 12 +- chopper/example/main.dart | 2 +- chopper/lib/src/base.dart | 59 +-- chopper/lib/src/request.dart | 92 ++-- chopper/pubspec.yaml | 3 +- chopper/test/authenticator_test.dart | 392 ++++++++++++++++++ chopper/test/base_test.dart | 168 +++++++- chopper/test/client_test.dart | 38 +- chopper/test/converter_test.dart | 9 +- chopper/test/fake_authenticator.dart | 23 + chopper/test/interceptors_test.dart | 14 +- chopper/test/multipart_test.dart | 39 +- chopper/test/request_test.dart | 145 ++++++- chopper/test/test_service.chopper.dart | 91 ++-- chopper/test/test_service.dart | 6 + chopper_built_value/test/converter_test.dart | 6 +- chopper_generator/CHANGELOG.md | 5 +- chopper_generator/lib/src/generator.dart | 19 +- chopper_generator/pubspec.yaml | 2 +- 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 +- example/pubspec.yaml | 2 +- faq.md | 59 ++- 29 files changed, 1003 insertions(+), 217 deletions(-) create mode 100644 chopper/test/authenticator_test.dart create mode 100644 chopper/test/fake_authenticator.dart 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/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! 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/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..4fa556c2 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() @@ -287,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()); @@ -301,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( @@ -341,10 +352,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 +371,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 +387,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 +410,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 +433,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 +457,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 +474,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/pubspec.yaml b/chopper/pubspec.yaml index 51d7abcd..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 @@ -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/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/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/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; + } +} 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..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 { @@ -203,9 +227,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 +244,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 +283,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 +303,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 +342,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..f93398a5 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', @@ -411,12 +411,31 @@ 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, { String? id, }) { - final String $url = '/test/file'; + final Uri $url = Uri.parse('/test/file'); final List $parts = [ PartValue( 'id', @@ -439,7 +458,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 +477,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 +488,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 +499,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 +514,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 +532,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 +546,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 +560,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 +574,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 +589,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/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( 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/CHANGELOG.md b/chopper_generator/CHANGELOG.md index 011b042e..4bdfebed 100644 --- a/chopper_generator/CHANGELOG.md +++ b/chopper_generator/CHANGELOG.md @@ -1,8 +1,7 @@ # Changelog +## 5.2.0 -## 5.1.0+1 - -- Analyzer upgrade +- Replaced the String based path with Uri ## 5.1.0 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/chopper_generator/pubspec.yaml b/chopper_generator/pubspec.yaml index f92dc5e4..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+1 +version: 5.2.0 documentation: https://hadrien-lejard.gitbook.io/chopper repository: https://github.com/lejard-h/chopper 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/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 diff --git a/faq.md b/faq.md index fca603c6..a2f31633 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(), ], @@ -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 @@ -332,7 +383,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 6b731a7d07c35d6a830a51226479d8c0ebf7432d Mon Sep 17 00:00:00 2001 From: Ivan Terekhin Date: Wed, 14 Dec 2022 10:31:43 +0300 Subject: [PATCH 16/60] 6.0.0 release (#396) --- 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 4600c9356a0a51a6996d3d7e222ff4aba20b308c Mon Sep 17 00:00:00 2001 From: Ivan Terekhin Date: Thu, 9 Feb 2023 13:49:38 +0700 Subject: [PATCH 17/60] Upgrade of chopper_built_value and chopper version --- chopper/CHANGELOG.md | 6 + chopper/lib/chopper.dart | 1 + chopper/lib/src/annotations.dart | 6 +- chopper/lib/src/http_logging_interceptor.dart | 173 ++++++++++ chopper/lib/src/interceptor.dart | 52 --- chopper/lib/src/utils.dart | 22 +- chopper/pubspec.yaml | 2 +- chopper/test/converter_test.dart | 14 + .../test/http_logging_interceptor_test.dart | 302 ++++++++++++++++++ chopper/test/interceptors_test.dart | 51 --- chopper/test/utils_test.dart | 164 ++++++++++ chopper_built_value/CHANGELOG.md | 4 + chopper_built_value/pubspec.yaml | 4 +- faq.md | 3 +- interceptors.md | 11 + 15 files changed, 694 insertions(+), 121 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/CHANGELOG.md b/chopper/CHANGELOG.md index 32f0af43..14081397 100644 --- a/chopper/CHANGELOG.md +++ b/chopper/CHANGELOG.md @@ -1,4 +1,10 @@ # Changelog + +## 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/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/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/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/lib/src/utils.dart b/chopper/lib/src/utils.dart index 299ddead..656c637c 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'; @@ -30,8 +32,8 @@ Request applyHeader( /// /// ```dart /// final newRequest = applyHeaders(request, { -/// 'Authorization': 'Bearer ', -/// 'Content-Type': 'application/json', +/// 'Authorization': 'Bearer ', +/// 'Content-Type': 'application/json', /// }); /// ``` Request applyHeaders( @@ -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/pubspec.yaml b/chopper/pubspec.yaml index ad1ac78f..89123059 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.0.0 +version: 6.1.0 documentation: https://hadrien-lejard.gitbook.io/chopper repository: https://github.com/lejard-h/chopper 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/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/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); + }, + ); + }); } diff --git a/chopper_built_value/CHANGELOG.md b/chopper_built_value/CHANGELOG.md index e3a476bc..6eab3bae 100644 --- a/chopper_built_value/CHANGELOG.md +++ b/chopper_built_value/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 1.2.0 + +- Chopper upgraded + ## 1.1.0 - Chopper upgraded diff --git a/chopper_built_value/pubspec.yaml b/chopper_built_value/pubspec.yaml index 0bd63c23..c3a1885c 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.1.0 +version: 1.2.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: ^5.0.0 + chopper: ^6.0.0 http: ^0.13.0 dev_dependencies: 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): 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 4efc2a9cbfd486088cd9f815e66c10ad95786697 Mon Sep 17 00:00:00 2001 From: Ivan Terekhin Date: Thu, 23 Feb 2023 18:02:30 +0700 Subject: [PATCH 18/60] Release 6.1.1 (#412) --- chopper/CHANGELOG.md | 3 + chopper/lib/src/request.dart | 25 +- chopper/lib/src/response.dart | 10 +- chopper/lib/src/utils.dart | 9 +- chopper/pubspec.yaml | 7 +- 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 ++ 12 files changed, 479 insertions(+), 6 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/CHANGELOG.md b/chopper/CHANGELOG.md index 14081397..fafc09e0 100644 --- a/chopper/CHANGELOG.md +++ b/chopper/CHANGELOG.md @@ -1,5 +1,8 @@ # Changelog +## 6.1.1 +- EquatableMixin for Request, Response and PartValue + ## 6.1.0 - HttpLogging interceptor more configurable 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 89123059..d4d936b7 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.0 +version: 6.1.1 documentation: https://hadrien-lejard.gitbook.io/chopper repository: https://github.com/lejard-h/chopper @@ -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 bea6840b2160bbab5ef6082f97af33419cac89ac Mon Sep 17 00:00:00 2001 From: Ivan Terekhin Date: Sat, 13 May 2023 11:19:52 +0400 Subject: [PATCH 19/60] Release 6.1.2 (#418) --- .github/CODEOWNERS | 5 + .github/ISSUE_TEMPLATE/feature_request.md | 20 ++++ .github/workflows/dart.yml | 98 ++++++++++--------- chopper/CHANGELOG.md | 3 + chopper/pubspec.yaml | 4 +- chopper_built_value/CHANGELOG.md | 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/CHANGELOG.md | 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 +- 20 files changed, 138 insertions(+), 85 deletions(-) create mode 100644 .github/CODEOWNERS create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md 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 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 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/CHANGELOG.md b/chopper/CHANGELOG.md index fafc09e0..8efd695f 100644 --- a/chopper/CHANGELOG.md +++ b/chopper/CHANGELOG.md @@ -1,5 +1,8 @@ # Changelog +## 6.1.2 +- Packages upgrade, constraints upgrade + ## 6.1.1 - EquatableMixin for Request, Response and PartValue diff --git a/chopper/pubspec.yaml b/chopper/pubspec.yaml index d4d936b7..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.1.1 +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/CHANGELOG.md b/chopper_built_value/CHANGELOG.md index 6eab3bae..c78c9e07 100644 --- a/chopper_built_value/CHANGELOG.md +++ b/chopper_built_value/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 1.2.1 + +- Packages upgrade, constraints upgrade + ## 1.2.0 - Chopper upgraded diff --git a/chopper_built_value/pubspec.yaml b/chopper_built_value/pubspec.yaml index c3a1885c..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.2.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/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/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 030e833d8ef81fe5376665b2d7a8efd769841eb2 Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Sun, 14 May 2023 05:28:34 +0100 Subject: [PATCH 20/60] :bookmark: release 6.1.2 (2nd attempt) (#420) --- .github/workflows/publish.yml | 63 ++++++++++++++++---------- .github/workflows/publish_dry_run.yml | 47 +++++++++++++++++++ chopper/.DS_Store | Bin 6148 -> 0 bytes tool/publish.sh | 11 +++-- 4 files changed, 91 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/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 aae04918a394b64f26ca15593032893fb34ae142 Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Sun, 14 May 2023 07:52:32 +0100 Subject: [PATCH 21/60] :bookmark: release v6.1.2 (3rd attempt) (#422) # 6.1.2 * [FIX] https://github.com/lejard-h/chopper/pull/419 * [FIX] https://github.com/lejard-h/chopper/pull/421 * [CHORE] https://github.com/lejard-h/chopper/pull/417 --- .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 636dbc3ea46164dad2831e4687589884294f0099 Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Mon, 15 May 2023 07:05:11 +0100 Subject: [PATCH 22/60] :twisted_rightwards_arrows: merge updated workflows to master (#426) --- .github/workflows/dart.yml | 59 ++++++++++----------- .github/workflows/publish.yml | 75 +++++++++++++++++---------- .github/workflows/publish_dry_run.yml | 64 ++++++++++++++++------- chopper/mono_pkg.yaml | 2 +- chopper_built_value/mono_pkg.yaml | 2 +- mono_repo.yaml | 30 ++++------- tool/ci.sh | 10 ++-- tool/compare_versions.dart | 38 ++++++++++++++ tool/coverage.sh | 11 ---- tool/pubspec.yaml | 12 +++++ 10 files changed, 187 insertions(+), 116 deletions(-) create mode 100644 tool/compare_versions.dart delete mode 100755 tool/coverage.sh create mode 100644 tool/pubspec.yaml 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/.github/workflows/publish.yml b/.github/workflows/publish.yml index 1ed7dbc0..8591699a 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -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/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/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/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 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 6fc206218f3dde9ddf5a765ab38f0572ba5c3f63 Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Sat, 20 May 2023 13:08:08 +0100 Subject: [PATCH 23/60] :twisted_rightwards_arrows: merge publish workflow fix to master (#428) --- .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 87fb6c50bb6435e3d86c29d3b934603fb7f23359 Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Sun, 28 May 2023 09:57:26 +0100 Subject: [PATCH 24/60] :bookmark: release v6.1.3 (#436) # chopper ## 6.1.3 * Add follow redirects to toHttpRequest (#430) * Update http constraint to ">=0.13.0 <2.0.0" (#431) * Add MultipartRequest log to CurlInterceptor (#435) --- # chopper_built_value ## 1.2.2 * Update http constraint to ">=0.13.0 <2.0.0" (#431) --- # example * Update squadron example (#432) --- # Github actions * Add cleanup step to publish workflow (#434) --------- Co-authored-by: Joran Dob Co-authored-by: Joseph, NamKung Co-authored-by: Klemen Tusar --- .github/workflows/publish.yml | 7 +- chopper/CHANGELOG.md | 57 +++-- chopper/lib/src/interceptor.dart | 10 + chopper/lib/src/request.dart | 1 + chopper/pubspec.yaml | 4 +- chopper/test/interceptors_test.dart | 29 +++ chopper_built_value/CHANGELOG.md | 4 + chopper_built_value/pubspec.yaml | 4 +- chopper_generator/CHANGELOG.md | 49 ++-- 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 | 6 +- 13 files changed, 345 insertions(+), 60 deletions(-) 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 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/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/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) { diff --git a/chopper/pubspec.yaml b/chopper/pubspec.yaml index f6cfea6a..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 @@ -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/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/\"", + ), + ); + }); }); } 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 1f536048..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 @@ -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 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/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..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 @@ -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 ae73e23651e3e47d9145ba869edad6f1b19eef3a Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Sun, 4 Jun 2023 07:46:33 +0100 Subject: [PATCH 25/60] :bookmark: release v6.1.4 (#440) # chopper ## 6.1.4 - [FIX] https://github.com/lejard-h/chopper/pull/439 --- chopper/CHANGELOG.md | 4 + chopper/lib/src/request.dart | 17 +- chopper/pubspec.yaml | 2 +- chopper/test/multipart_test.dart | 558 ++++++++++++++++--------- chopper/test/test_service.chopper.dart | 36 ++ chopper/test/test_service.dart | 9 + 6 files changed, 414 insertions(+), 212 deletions(-) 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/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/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 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 b19f402815676c20122bf79cdabb10f72b4a42f3 Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Mon, 19 Jun 2023 17:00:15 +0100 Subject: [PATCH 26/60] :bookmark: release chopper_generator v6.0.2 (#446) --- chopper_generator/CHANGELOG.md | 4 ++++ chopper_generator/lib/chopper_generator.dart | 2 +- chopper_generator/lib/src/generator.dart | 13 +++++++++++-- chopper_generator/pubspec.yaml | 2 +- 4 files changed, 17 insertions(+), 4 deletions(-) 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/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) => 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 f1c929ceaaab2cb855cc102effadd0ddbefe7447 Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Wed, 5 Jul 2023 08:59:31 +0100 Subject: [PATCH 27/60] :bookmark: release chopper generator v6.0.3 (#449) # chopper_generator ## 6.0.3 * [CHORE] #448 --- .github/workflows/dart.yml | 21 +- chopper/pubspec.yaml | 1 + chopper/test/ensure_build_test.dart | 16 + chopper_generator/CHANGELOG.md | 13 + 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 | 7 +- chopper_generator/test/ensure_build_test.dart | 16 + .../test/test_service.chopper.dart | 639 ++++++++++++++++++ chopper_generator/test/test_service.dart | 218 ++++++ 17 files changed, 1205 insertions(+), 235 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/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/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..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 @@ -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 e1786408b7d9e67881eb4a343f4a21bde6053432 Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Sat, 29 Jul 2023 22:42:08 +0100 Subject: [PATCH 28/60] :bookmark: release v7.0.0 (#454) --- .github/workflows/publish.yml | 2 + .github/workflows/publish_dry_run.yml | 2 + chopper/CHANGELOG.md | 5 + chopper/analysis_options.yaml | 21 -- 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 | 27 ++- .../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/CHANGELOG.md | 4 + chopper_built_value/analysis_options.yaml | 21 -- chopper_built_value/pubspec.yaml | 23 +- chopper_generator/CHANGELOG.md | 6 + chopper_generator/analysis_options.yaml | 22 -- chopper_generator/lib/src/generator.dart | 9 +- chopper_generator/lib/src/utils.dart | 2 +- chopper_generator/pubspec.yaml | 31 ++- 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 | 23 +- 31 files changed, 235 insertions(+), 286 deletions(-) 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/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/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 2af5cfbf..5a6d5859 100644 --- a/chopper/pubspec.yaml +++ b/chopper/pubspec.yaml @@ -1,30 +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 - dart_code_metrics: '>=4.8.1 <6.0.0' + 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/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_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..a59baadb 100644 --- a/chopper_built_value/pubspec.yaml +++ b/chopper_built_value/pubspec.yaml @@ -1,25 +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 - dart_code_metrics: '>=4.8.1 <6.0.0' - 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/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 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/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 fc438eca..e342ecc7 100644 --- a/chopper_generator/pubspec.yaml +++ b/chopper_generator/pubspec.yaml @@ -1,30 +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 - dart_code_metrics: '>=4.8.1 <6.0.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/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..735d046c 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -5,25 +5,24 @@ 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: - 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 85079b29bfd61d5fd6316e285bd923b34b5f88ef Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Sun, 30 Jul 2023 13:13:04 +0100 Subject: [PATCH 29/60] :twisted_rightwards_arrows: sync CODEOWNERS from origin/development (#458) --- .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 203e64dd2574a02204365cd204fd97db69379607 Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Thu, 3 Aug 2023 17:53:18 +0200 Subject: [PATCH 30/60] :bookmark: release v7.0.1 (#465) # chopper ## 7.0.1 - Refactor ChopperClient constructor - Refactor ChopperClient.getService - Refactor CurlInterceptor # chopper_generator ## 7.0.1 - Add final class modifier to generated Chopper API implementation --- .github/workflows/dart.yml | 40 ++--------- chopper/CHANGELOG.md | 6 ++ chopper/example/definition.chopper.dart | 2 +- chopper/lib/src/base.dart | 71 ++++++++++--------- chopper/lib/src/interceptor.dart | 31 ++++---- chopper/lib/src/utils.dart | 4 +- chopper/pubspec.yaml | 2 +- chopper/test/base_test.dart | 54 ++++++++++++-- chopper/test/test_service.chopper.dart | 2 +- chopper_built_value/mono_pkg.yaml | 2 +- chopper_generator/CHANGELOG.md | 4 ++ chopper_generator/lib/src/generator.dart | 1 + chopper_generator/mono_pkg.yaml | 2 +- chopper_generator/pubspec.yaml | 2 +- .../test/test_service.chopper.dart | 2 +- example/lib/built_value_resource.chopper.dart | 2 +- example/lib/json_serializable.chopper.dart | 2 +- mono_repo.yaml | 2 +- tool/ci.sh | 2 +- 19 files changed, 127 insertions(+), 106 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/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/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/lib/src/base.dart b/chopper/lib/src/base.dart index 27ab57fc..e791c13b 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, @@ -47,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; @@ -114,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] @@ -168,15 +170,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; @@ -185,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/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/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/test/base_test.dart b/chopper/test/base_test.dart index d6486c2e..3e6a6f01 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."), ); } @@ -635,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 { 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_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/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/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/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/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 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; 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 ee165199efb255aa24e9b4ce3a35f508a47aab22 Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Mon, 7 Aug 2023 11:11:53 +0200 Subject: [PATCH 31/60] :bookmark: release v7.0.2 (#472) --- .github/dependabot.yml | 17 +++++++++ .github/workflows/dart.yml | 2 -- .github/workflows/publish.yml | 11 ++++-- .github/workflows/publish_dry_run.yml | 9 +++-- chopper/CHANGELOG.md | 4 +++ chopper/lib/src/http_logging_interceptor.dart | 35 ++++++++++--------- chopper/pubspec.yaml | 5 ++- chopper_built_value/pubspec.yaml | 1 - chopper_generator/pubspec.yaml | 1 - mono_repo.yaml | 2 +- tool/publish.sh | 19 ---------- 11 files changed, 60 insertions(+), 46 deletions(-) create mode 100644 .github/dependabot.yml delete mode 100644 tool/publish.sh 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 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/.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/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/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; diff --git a/chopper/pubspec.yaml b/chopper/pubspec.yaml index 0e86235c..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 @@ -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/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: 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 b521f7ca65b20fa1a43a91ff500a3aad86e4ddfa Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Thu, 10 Aug 2023 19:51:40 +0200 Subject: [PATCH 32/60] :bookmark: release v7.0.3 (#476) --- chopper/CHANGELOG.md | 4 ++ chopper/lib/src/chopper_log_record.dart | 13 ++++++ chopper/lib/src/http_logging_interceptor.dart | 45 ++++++++++++------- chopper/pubspec.yaml | 2 +- 4 files changed, 48 insertions(+), 16 deletions(-) create mode 100644 chopper/lib/src/chopper_log_record.dart 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/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; 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 b5ef81eaa85c232342a69fe130ca88c80e4e58df Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Mon, 14 Aug 2023 08:45:23 +0100 Subject: [PATCH 33/60] :bookmark: release v7.0.4 (#482) --- chopper/CHANGELOG.md | 5 +++++ chopper/lib/chopper.dart | 1 + chopper/lib/src/chopper_log_record.dart | 2 +- chopper/pubspec.yaml | 2 +- 4 files changed, 8 insertions(+), 2 deletions(-) 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/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; 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; 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 2bdf9476726181d265f61bb014c6f07a5663db31 Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Thu, 17 Aug 2023 08:42:05 +0100 Subject: [PATCH 34/60] :bookmark: release chopper_generator v7.0.2 (#485) --- 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 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 4d541b87..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 @@ -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 c0133d73bb6740e1acb0070a0e3f60aa635a0c2d Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Tue, 22 Aug 2023 08:35:35 +0100 Subject: [PATCH 35/60] :bookmark: release chopper v7.0.5 (#490) --- chopper/CHANGELOG.md | 4 ++++ chopper/README.md | 6 +++--- chopper/pubspec.yaml | 2 +- 3 files changed, 8 insertions(+), 4 deletions(-) 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/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 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 7833e70d460a4ebf9c902d8057c0f5cb6504db40 Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Sat, 2 Sep 2023 08:28:56 +0100 Subject: [PATCH 36/60] :bookmark: release v7.0.6 (#496) # chopper ## 7.0.6 - #493 - #497 - #495 # chopper_built_value ## 2.0.1 - #495 # chopper_generator ## 7.0.3 - #493 - #495 --------- Co-authored-by: Klemen Tusar Co-authored-by: Martin Alejandro Escobar Espinel <56127727+martinale14@users.noreply.github.com> --- chopper/CHANGELOG.md | 6 + chopper/lib/src/annotations.dart | 2 + chopper/lib/src/base.dart | 7 +- chopper/lib/src/interceptor.dart | 19 +- chopper/pubspec.yaml | 8 +- chopper/test/base_test.dart | 141 ++++ chopper/test/converter_test.dart | 19 + chopper/test/ensure_build_test.dart | 20 +- .../test/test_service_variable.chopper.dart | 641 ++++++++++++++++++ chopper/test/test_service_variable.dart | 220 ++++++ chopper_built_value/CHANGELOG.md | 4 + chopper_built_value/pubspec.yaml | 7 +- chopper_generator/CHANGELOG.md | 5 + chopper_generator/lib/src/generator.dart | 75 +- chopper_generator/pubspec.yaml | 8 +- chopper_generator/test/ensure_build_test.dart | 21 +- .../test/test_service_variable.chopper.dart | 641 ++++++++++++++++++ .../test/test_service_variable.dart | 220 ++++++ 18 files changed, 2028 insertions(+), 36 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/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/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/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/pubspec.yaml b/chopper/pubspec.yaml index 0ddea437..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 @@ -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/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/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', () { 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_built_value/CHANGELOG.md b/chopper_built_value/CHANGELOG.md index 7e536f4d..186e3f62 100644 --- a/chopper_built_value/CHANGELOG.md +++ b/chopper_built_value/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 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 b23d0a66..8b8ec181 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 documentation: https://hadrien-lejard.gitbook.io/chopper/converters/built-value-converter repository: https://github.com/lejard-h/chopper @@ -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/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/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/pubspec.yaml b/chopper_generator/pubspec.yaml index 6e2d7d4c..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 @@ -28,3 +28,9 @@ dev_dependencies: dependency_overrides: chopper: path: ../chopper + +topics: + - api + - codegen + - http + - rest 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 b969ea08546516ea8b95dc3288f70c54d4e2a363 Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Sat, 2 Sep 2023 12:25:09 +0100 Subject: [PATCH 37/60] :bug: Fix chopper_built_value pub.dev topics (#498) --- .github/workflows/publish.yml | 13 +++++++++++++ .github/workflows/publish_dry_run.yml | 13 +++++++++++++ chopper_built_value/CHANGELOG.md | 4 ++++ chopper_built_value/pubspec.yaml | 4 ++-- 4 files changed, 32 insertions(+), 2 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_built_value/CHANGELOG.md b/chopper_built_value/CHANGELOG.md index 186e3f62..16e4dacf 100644 --- a/chopper_built_value/CHANGELOG.md +++ b/chopper_built_value/CHANGELOG.md @@ -1,5 +1,9 @@ # 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)) diff --git a/chopper_built_value/pubspec.yaml b/chopper_built_value/pubspec.yaml index 8b8ec181..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.1 +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 From 9a3b540d227fe4e1135d9836f7329d948b75e30a Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Tue, 5 Sep 2023 20:09:15 +0100 Subject: [PATCH 38/60] :bookmark: release chopper generator v7.0.4 (#502) # chopper_generator ## 7.0.4 - #501 --- chopper/example/definition.chopper.dart | 2 +- chopper/test/test_service.chopper.dart | 2 +- chopper/test/test_service_variable.chopper.dart | 2 +- chopper_generator/CHANGELOG.md | 4 ++++ chopper_generator/lib/src/generator.dart | 1 + chopper_generator/pubspec.yaml | 2 +- chopper_generator/test/test_service.chopper.dart | 2 +- chopper_generator/test/test_service_variable.chopper.dart | 2 +- 8 files changed, 11 insertions(+), 6 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/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/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/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 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 9b6fc0d2a34d62babe895d855b4077280eee9d5d Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Sat, 7 Oct 2023 10:12:51 +0100 Subject: [PATCH 39/60] :bookmark: release chopper v7.0.7 and chopper_generator v7.0.5 (#511) --- .github/workflows/dart.yml | 10 +++--- .github/workflows/publish.yml | 34 ++++++++++++++----- .github/workflows/publish_dry_run.yml | 30 +++++++++++----- chopper/CHANGELOG.md | 4 +++ chopper/analysis_options.yaml | 1 - chopper/example/definition.chopper.dart | 2 +- chopper/lib/src/request.dart | 19 +++++++---- chopper/pubspec.yaml | 2 +- chopper/test/base_test.dart | 20 +++++++++++ chopper/test/test_service.chopper.dart | 2 +- .../test/test_service_variable.chopper.dart | 2 +- chopper_built_value/analysis_options.yaml | 1 - chopper_generator/CHANGELOG.md | 4 +++ chopper_generator/Makefile | 4 +++ chopper_generator/analysis_options.yaml | 1 - chopper_generator/lib/src/generator.dart | 7 +--- chopper_generator/pubspec.yaml | 2 +- .../test/test_service.chopper.dart | 2 +- .../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 +- tool/ci.sh | 2 +- 23 files changed, 109 insertions(+), 47 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/.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 diff --git a/chopper/CHANGELOG.md b/chopper/CHANGELOG.md index a2753721..528eb8dd 100644 --- a/chopper/CHANGELOG.md +++ b/chopper/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 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/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/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/pubspec.yaml b/chopper/pubspec.yaml index 6ec0d139..f125ad3e 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 documentation: https://hadrien-lejard.gitbook.io/chopper repository: https://github.com/lejard-h/chopper 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, 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/CHANGELOG.md b/chopper_generator/CHANGELOG.md index 0b7c9f39..0b373162 100644 --- a/chopper_generator/CHANGELOG.md +++ b/chopper_generator/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 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/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/pubspec.yaml b/chopper_generator/pubspec.yaml index 5cfb749d..6048de23 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 documentation: https://hadrien-lejard.gitbook.io/chopper repository: https://github.com/lejard-h/chopper 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; 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 741fb998bf1a377d58c361c14be7f1b4c25fe75f Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Sat, 7 Oct 2023 10:20:01 +0100 Subject: [PATCH 40/60] :green_heart: fix release workflow permissions (#512) --- .github/workflows/publish.yml | 2 ++ 1 file changed, 2 insertions(+) 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 ] From 294e27ad58b6157eefe33b387365d6214fbbcb43 Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Sat, 7 Oct 2023 10:33:21 +0100 Subject: [PATCH 41/60] :bookmark: release v7.0.7+1 (#513) # chopper ## 7.0.7+1 - #512 # chopper_built_value - #512 # chopper_generator ## 7.0.5+1 - #512 --- chopper/CHANGELOG.md | 4 ++++ chopper/pubspec.yaml | 2 +- chopper_built_value/CHANGELOG.md | 4 ++++ chopper_built_value/pubspec.yaml | 2 +- chopper_generator/CHANGELOG.md | 4 ++++ chopper_generator/pubspec.yaml | 2 +- 6 files changed, 15 insertions(+), 3 deletions(-) diff --git a/chopper/CHANGELOG.md b/chopper/CHANGELOG.md index 528eb8dd..b46c66c5 100644 --- a/chopper/CHANGELOG.md +++ b/chopper/CHANGELOG.md @@ -1,5 +1,9 @@ # 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)) diff --git a/chopper/pubspec.yaml b/chopper/pubspec.yaml index f125ad3e..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.7 +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 0b373162..945efac1 100644 --- a/chopper_generator/CHANGELOG.md +++ b/chopper_generator/CHANGELOG.md @@ -1,5 +1,9 @@ # 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)) diff --git a/chopper_generator/pubspec.yaml b/chopper_generator/pubspec.yaml index 6048de23..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.5 +version: 7.0.5+1 documentation: https://hadrien-lejard.gitbook.io/chopper repository: https://github.com/lejard-h/chopper From 961bdce4e8984e2df2bd4786ae847fa9cd7392db Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Wed, 11 Oct 2023 08:33:43 +0100 Subject: [PATCH 42/60] :bookmark: release chopper v7.0.8 (#517) --- chopper/CHANGELOG.md | 4 ++ chopper/lib/src/utils.dart | 6 +- chopper/pubspec.yaml | 2 +- chopper/test/base_test.dart | 78 ++++++++++++++++++++++++-- chopper/test/test_service.chopper.dart | 13 +++++ chopper/test/test_service.dart | 5 ++ 6 files changed, 102 insertions(+), 6 deletions(-) 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/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/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 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 fcbe6bb8229c631542d3a393584c0d0392086f6c Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Fri, 13 Oct 2023 14:37:59 +0100 Subject: [PATCH 43/60] :bookmark: release chopper_generator v7.0.6 (#521) --- 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/CHANGELOG.md | 4 + chopper_generator/lib/src/generator.dart | 17 +- chopper_generator/pubspec.yaml | 2 +- 7 files changed, 291 insertions(+), 9 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/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/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) => 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 9f3b2e7d795b2579f459a5b7fe7ecf70efb65b1b Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Mon, 30 Oct 2023 12:29:17 +0000 Subject: [PATCH 44/60] :bookmark: release chopper v7.0.9 (#531) # chopper ## 7.0.9 - #527 - #529 --------- Co-authored-by: Diego Tori --- chopper/CHANGELOG.md | 5 + chopper/Makefile | 2 +- chopper/lib/src/authenticator.dart | 54 ++++- chopper/lib/src/base.dart | 16 +- chopper/lib/src/chopper_log_record.dart | 12 ++ chopper/lib/src/request.dart | 33 +++ chopper/lib/src/response.dart | 11 + chopper/pubspec.yaml | 4 +- chopper/test/authenticator_test.dart | 255 ++++++++++++++++++++++++ chopper/test/fake_authenticator.dart | 45 ++++- chopper_built_value/Makefile | 2 +- chopper_built_value/pubspec.yaml | 2 +- chopper_generator/pubspec.yaml | 2 +- 13 files changed, 433 insertions(+), 10 deletions(-) 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/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/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 504590e2..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); } } @@ -515,6 +518,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 {} diff --git a/chopper/pubspec.yaml b/chopper/pubspec.yaml index 79716f73..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 @@ -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 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; + }; } 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" 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: 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 aaaf482839ef2147881614913bb6fdcfc17e41fd Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Sat, 30 Dec 2023 10:04:30 +0100 Subject: [PATCH 45/60] :bookmark: release chopper v7.0.10, chopper_generator v7.0.7 (#541) --- .github/workflows/dart.yml | 4 +- .github/workflows/publish.yml | 4 +- .github/workflows/publish_dry_run.yml | 4 +- chopper/CHANGELOG.md | 4 + chopper/example/definition.chopper.dart | 3 +- chopper/pubspec.yaml | 2 +- chopper/test/base_test.dart | 33 +++ chopper/test/fixtures/example_enum.dart | 8 + chopper/test/test_service.chopper.dart | 28 ++- chopper/test/test_service.dart | 11 + .../test/test_service_base_url.chopper.dart | 3 +- .../test/test_service_variable.chopper.dart | 3 +- chopper_generator/CHANGELOG.md | 4 + chopper_generator/lib/src/generator.dart | 188 ++++++++++-------- chopper_generator/pubspec.yaml | 2 +- .../test/test_service.chopper.dart | 28 ++- chopper_generator/test/test_service.dart | 18 ++ .../test/test_service_variable.chopper.dart | 3 +- tool/ci.sh | 18 +- 19 files changed, 261 insertions(+), 107 deletions(-) create mode 100644 chopper/test/fixtures/example_enum.dart 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/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/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/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/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 028a7310..0db60af9 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( @@ -649,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/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/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/lib/src/generator.dart b/chopper_generator/lib/src/generator.dart index 007649bf..915b49f8 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()), @@ -658,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); } }); @@ -682,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/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 diff --git a/chopper_generator/test/test_service.chopper.dart b/chopper_generator/test/test_service.chopper.dart index f070c8a8..b94c2e9b 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( @@ -636,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; +} 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( 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 d793affe1762d5b93719312babc2fcb06278be21 Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Fri, 12 Jan 2024 08:54:10 +0000 Subject: [PATCH 46/60] :bookmark: release v7.1.0 (#553) --- chopper/CHANGELOG.md | 6 + chopper/lib/src/annotations.dart | 127 ++++ chopper/lib/src/chopper_http_exception.dart | 13 + 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 | 27 + chopper/pubspec.yaml | 2 +- chopper/test/chopper_http_exception_test.dart | 21 + chopper/test/ensure_build_test.dart | 1 + chopper/test/fixtures/error_fixtures.dart | 3 + chopper/test/response_test.dart | 114 +++ chopper/test/test_service.chopper.dart | 30 +- chopper/test/test_service.dart | 11 +- .../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/CHANGELOG.md | 5 + chopper_generator/lib/src/generator.dart | 83 ++- chopper_generator/lib/src/vars.dart | 1 + chopper_generator/pubspec.yaml | 4 +- chopper_generator/test/ensure_build_test.dart | 11 +- .../test/test_service.chopper.dart | 30 +- chopper_generator/test/test_service.dart | 11 +- .../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 ++++++ faq.md | 44 +- getting-started.md | 14 +- requests.md | 203 +++-- 32 files changed, 2570 insertions(+), 100 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/fixtures/error_fixtures.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/CHANGELOG.md b/chopper/CHANGELOG.md index b15c91b9..82da3393 100644 --- a/chopper/CHANGELOG.md +++ b/chopper/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 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/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/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/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 92d1a8bb..b9f8e06d 100644 --- a/chopper/lib/src/response.dart +++ b/chopper/lib/src/response.dart @@ -1,9 +1,11 @@ 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'; +/// {@template response} /// A [http.BaseResponse] wrapper representing a response of a Chopper network call. /// /// ```dart @@ -15,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. @@ -30,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. @@ -67,6 +71,29 @@ 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; + } + } + + /// 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/pubspec.yaml b/chopper/pubspec.yaml index 9bd3a192..94589d43 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 documentation: https://hadrien-lejard.gitbook.io/chopper repository: https://github.com/lejard-h/chopper 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/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(); +} 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..c6335821 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 @@ -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 1ed74368..6ae6cb4e 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(); @@ -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/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/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/lib/src/generator.dart b/chopper_generator/lib/src/generator.dart index 915b49f8..f35a341a 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); }); } @@ -430,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( @@ -508,8 +546,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/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 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..262dfb11 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 @@ -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 30d6a451..1fae94fc 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(); @@ -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) { 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; +} 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 165d3618f661193cfee171cef41a301b4c8b7f9d Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Fri, 12 Jan 2024 09:50:37 +0000 Subject: [PATCH 47/60] :bookmark: release chopper v7.1.0+1 (#555) # chopper ## 7.1.0+1 - Bump `chopper_generator` version requirement to 7.1.0 --- chopper/CHANGELOG.md | 4 ++++ chopper/pubspec.yaml | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/chopper/CHANGELOG.md b/chopper/CHANGELOG.md index 82da3393..6c82fa4d 100644 --- a/chopper/CHANGELOG.md +++ b/chopper/CHANGELOG.md @@ -1,5 +1,9 @@ # 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)) diff --git a/chopper/pubspec.yaml b/chopper/pubspec.yaml index 94589d43..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.1.0 +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: From 56c1ce025923b50d01029201f2c525b41ffd5db8 Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Mon, 22 Jan 2024 18:46:27 +0000 Subject: [PATCH 48/60] :bookmark: release chopper_generator v7.1.1 (#563) --- .github/dependabot.yml | 5 ++ chopper_generator/CHANGELOG.md | 4 ++ .../lib/src/builder_factory.dart | 64 +++++++++++++++---- chopper_generator/pubspec.yaml | 3 +- 4 files changed, 62 insertions(+), 14 deletions(-) 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: 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/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..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 @@ -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 From 911b4c7f2694a13a2adc82e8aaa69e4b7f69994b Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Sun, 28 Jan 2024 09:33:37 +0000 Subject: [PATCH 49/60] :bookmark: release chopper v7.1.1 (#568) * :bookmark: release chopper v7.1.1 # chopper ## 7.1.1 - #567 --------- Signed-off-by: dependabot[bot] Co-authored-by: Job Guldemeester --- chopper/CHANGELOG.md | 4 ++++ chopper/lib/src/annotations.dart | 23 +++++++++++++++++++++++ chopper/pubspec.yaml | 2 +- faq.md | 16 ++++++++++++++++ 4 files changed, 44 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/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(); 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 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 a8cab3f57a534cb1db6b1d924ebd66d65860bd59 Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Mon, 26 Feb 2024 09:52:18 +0000 Subject: [PATCH 50/60] :bookmark: release chopper v7.1.1+1 (#576) * :bookmark: release chopper v7.1.1+1 # chopper ## 7.1.1+1 - #570 --------- Signed-off-by: dependabot[bot] Co-authored-by: Job Guldemeester --- chopper/CHANGELOG.md | 4 ++++ chopper/lib/chopper.dart | 5 +++-- chopper/pubspec.yaml | 2 +- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/chopper/CHANGELOG.md b/chopper/CHANGELOG.md index 60a8bc9f..a1621713 100644 --- a/chopper/CHANGELOG.md +++ b/chopper/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 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/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; diff --git a/chopper/pubspec.yaml b/chopper/pubspec.yaml index b4a729d0..9ca02f9d 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.1.1+1 documentation: https://hadrien-lejard.gitbook.io/chopper repository: https://github.com/lejard-h/chopper From 47084ccfa2af290081ae0716d52d5172056cb5b9 Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Fri, 8 Mar 2024 08:55:52 +0000 Subject: [PATCH 51/60] :bookmark: release v7.2.0 (#581) * :bookmark: release v7.2.0 # chopper ## 7.2.0 - #579 # chopper_generator ## 7.2.0 - #579 --------- Signed-off-by: dependabot[bot] Co-authored-by: wenchieh --- .github/workflows/dart.yml | 8 +- chopper/CHANGELOG.md | 4 + chopper/lib/src/annotations.dart | 29 +++++- chopper/pubspec.yaml | 2 +- chopper_generator/CHANGELOG.md | 4 + chopper_generator/lib/src/generator.dart | 94 +++++++++++++++--- chopper_generator/pubspec.yaml | 4 +- .../test/test_service.chopper.dart | 95 +++++++++++++++++++ chopper_generator/test/test_service.dart | 27 ++++++ requests.md | 63 +++++++----- 10 files changed, 284 insertions(+), 46 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" diff --git a/chopper/CHANGELOG.md b/chopper/CHANGELOG.md index a1621713..dea66c34 100644 --- a/chopper/CHANGELOG.md +++ b/chopper/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+1 - Export `ChopperHttpException` in library exports ([#570](https://github.com/lejard-h/chopper/pull/570)) 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/pubspec.yaml b/chopper/pubspec.yaml index 9ca02f9d..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+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/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/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 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 d2a4c46922e72f0813f6cad41bb8ef595d8f6674 Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Wed, 3 Apr 2024 09:08:05 +0200 Subject: [PATCH 52/60] :bookmark: release v7.3.0 (#589) * :bookmark: release v7.3.0 # chopper ## 7.3.0 - Add support for `@Tag` annotation ([#586](https://github.com/lejard-h/chopper/pull/586)) # chopper_generator ## 7.3.0 - Add support for `@Tag` annotation ([#586](https://github.com/lejard-h/chopper/pull/586)) --------- Signed-off-by: dependabot[bot] Co-authored-by: wenchieh --- .github/workflows/dart.yml | 8 +- .github/workflows/publish.yml | 6 +- .github/workflows/publish_dry_run.yml | 4 +- chopper/CHANGELOG.md | 4 + chopper/example/tag.chopper.dart | 44 ++++++++++ chopper/example/tag.dart | 87 +++++++++++++++++++ chopper/lib/src/annotations.dart | 22 +++++ chopper/lib/src/request.dart | 4 + chopper/pubspec.yaml | 6 +- chopper_generator/CHANGELOG.md | 4 + chopper_generator/lib/src/generator.dart | 6 ++ chopper_generator/pubspec.yaml | 4 +- .../test/test_service.chopper.dart | 17 ++++ chopper_generator/test/test_service.dart | 6 ++ requests.md | 46 +++++++++- 15 files changed, 253 insertions(+), 15 deletions(-) create mode 100644 chopper/example/tag.chopper.dart create mode 100644 chopper/example/tag.dart 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 f7835d30..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 }} @@ -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) }} 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 }} 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/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..a9e7e9b7 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( @@ -62,6 +64,7 @@ base class Request extends http.BaseRequest with EquatableMixin { List? parts, bool? useBrackets, bool? includeNullQueryVars, + Object? tag, }) => Request( method ?? this.method, @@ -74,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]. 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/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/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 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 3b3952e7766a8d3070518335b66c6ed89453d95a Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Fri, 5 Apr 2024 09:16:50 +0200 Subject: [PATCH 53/60] :bookmark: release v7.4.0 (#595) * :bookmark: release v7.4.0 # chopper ## 7.4.0 - #592 # chopper_generator ## 7.4.0 - #592 --------- Signed-off-by: dependabot[bot] --- .github/workflows/dart.yml | 24 +- .github/workflows/publish.yml | 4 +- .github/workflows/publish_dry_run.yml | 4 +- chopper/CHANGELOG.md | 4 + 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 | 5 +- 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/CHANGELOG.md | 4 + chopper_generator/analysis_options.yaml | 1 + chopper_generator/lib/src/generator.dart | 20 +- chopper_generator/lib/src/utils.dart | 22 +- chopper_generator/pubspec.yaml | 6 +- .../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 + 33 files changed, 2948 insertions(+), 233 deletions(-) create mode 100644 chopper/lib/src/list_format.dart 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 }} 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/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..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 @@ -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 @@ -25,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/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/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/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..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,13 +11,15 @@ 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 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 ed2cee4808ce244539c704f1047871f9119007d7 Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Sat, 6 Apr 2024 09:00:40 +0200 Subject: [PATCH 54/60] :bookmark: pre-release v8.0.0-rc.1 (#598) # chopper ## 8.0.0-rc.1 - Restructure interceptors ([#547](https://github.com/lejard-h/chopper/pull/547)) # chopper_generator ## 8.0.0-rc.1 - Restructure interceptors ([#547](https://github.com/lejard-h/chopper/pull/547)) --------- Signed-off-by: dependabot[bot] Co-authored-by: Job Guldemeester --- chopper/CHANGELOG.md | 4 + 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/pubspec.yaml | 18 +- 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 +- chopper_generator/CHANGELOG.md | 4 + chopper_generator/pubspec.yaml | 22 +- example/bin/main_json_serializable.dart | 20 +- ...son_serializable_squadron_worker_pool.dart | 6 +- faq.md | 66 +++-- interceptors.md | 88 +++++-- tool/pubspec.yaml | 6 +- 44 files changed, 1609 insertions(+), 625 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/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/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/pubspec.yaml b/chopper/pubspec.yaml index c64405ab..5d2c2e47 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 # commented out for publishing rc to pub.dev dependency_overrides: chopper_generator: 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/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..c8f708a0 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: ">=7.4.0 <9.0.0" # will be replaced by ^8.0.0 in next rc + 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: 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. 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 ae505af2f6a33b84fa37410870c5e6987e7cd249 Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Fri, 3 May 2024 07:26:32 +0100 Subject: [PATCH 55/60] :bookmark: pre-release v8.0.0-rc.2 (#606) # chopper ## 8.0.0-rc.2 - #604 # chopper_generator ## 8.0.0-rc.2 - #604 # chopper_built_value ## 3.0.0-rc.1 - Require Chopper ^8.0.0 --------- Signed-off-by: dependabot[bot] --- .github/workflows/dart.yml | 7 +-- chopper/CHANGELOG.md | 4 ++ chopper/lib/src/annotations.dart | 11 +++++ chopper/pubspec.yaml | 8 ++-- .../test/chain/interceptor_chain_test.dart | 46 +++++++++++++++++++ chopper/test/test_service.chopper.dart | 39 ++++++++++++++++ chopper/test/test_service.dart | 9 ++++ chopper_built_value/CHANGELOG.md | 4 ++ chopper_built_value/pubspec.yaml | 20 ++++---- chopper_generator/CHANGELOG.md | 4 ++ chopper_generator/lib/src/generator.dart | 11 ++++- chopper_generator/lib/src/utils.dart | 15 ++++++ chopper_generator/pubspec.yaml | 8 ++-- .../test/test_service.chopper.dart | 39 ++++++++++++++++ chopper_generator/test/test_service.dart | 9 ++++ mono_repo.yaml | 4 +- 16 files changed, 215 insertions(+), 23 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/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/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/pubspec.yaml b/chopper/pubspec.yaml index 5d2c2e47..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 # commented out for publishing rc to pub.dev + 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/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 { 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_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/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/pubspec.yaml b/chopper_generator/pubspec.yaml index c8f708a0..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: ">=7.4.0 <9.0.0" # will be replaced by ^8.0.0 in next rc + 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: 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) { 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 df9801ada11d6a95159b43f250d3a2f630a1dfcc Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Thu, 9 May 2024 08:59:09 +0100 Subject: [PATCH 56/60] :bookmark: release v8.0.0 (#609) # chopper ## 8.0.0 - Restructure interceptors ([#547](https://github.com/lejard-h/chopper/pull/547)) - Add per-request timeout ([#604](https://github.com/lejard-h/chopper/pull/604)) # chopper_generator ## 8.0.0 - Restructure interceptors ([#547](https://github.com/lejard-h/chopper/pull/547)) - Add per-request timeout ([#604](https://github.com/lejard-h/chopper/pull/604)) ## 3.0.0 - Require Chopper ^8.0.0 --------- Signed-off-by: dependabot[bot] Co-authored-by: Job Guldemeester --- chopper/CHANGELOG.md | 12 +- .../http_logging_interceptor.dart | 121 ++- chopper/pubspec.yaml | 4 +- .../test/http_logging_interceptor_test.dart | 910 +++++++++++++----- 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 +- 14 files changed, 764 insertions(+), 343 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/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/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/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', + ], + ), + ); + }); + }); }); }); } 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 3d17471d29c457b967ea3c150dc28721c57f53cc Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Sun, 30 Jun 2024 11:19:55 +0100 Subject: [PATCH 57/60] :bookmark: release v8.0.1 (#625) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * :arrow_up: update dependencies and linters (#615) * :memo: update build_runner reference in readme (#616) * :page_facing_up: create symlink to chopper/LICENSE (#617) * :busts_in_silhouette: update/fix contributors (#618) * :lock: add Security Policy (#619) * 🐛 Fix null body converter (#623) * :package: directly export `qs.ListFormat` instead of internal wrapper (#624) --------- Signed-off-by: dependabot[bot] Co-authored-by: Job Guldemeester --- .all-contributorsrc | 57 +++++--- CODE-OF-CONDUCT.md | 133 ++++++++++++++++++ CONTRIBUTING.md | 111 +++++++++++++++ LICENSE | 1 + README.md | 13 +- SECURITY.md | 46 ++++++ chopper/CHANGELOG.md | 6 + chopper/README.md | 4 +- chopper/lib/chopper.dart | 2 +- chopper/lib/src/annotations.dart | 2 +- .../response_converter_interceptor.dart | 10 +- chopper/lib/src/request.dart | 2 +- chopper/lib/src/utils.dart | 10 +- chopper/pubspec.yaml | 12 +- .../response_converter_interceptor_test.dart | 58 ++++++++ chopper/test/ensure_build_test.dart | 2 + chopper/test/utils_test.dart | 2 +- chopper_built_value/CHANGELOG.md | 4 + chopper_built_value/pubspec.yaml | 6 +- chopper_generator/CHANGELOG.md | 5 + chopper_generator/lib/src/generator.dart | 10 +- chopper_generator/lib/src/utils.dart | 4 +- chopper_generator/pubspec.yaml | 9 +- chopper_generator/test/ensure_build_test.dart | 2 + ...son_serializable_squadron_worker_pool.dart | 8 +- example/lib/json_decode_service.dart | 2 +- example/pubspec.yaml | 2 +- 27 files changed, 454 insertions(+), 69 deletions(-) create mode 100644 CODE-OF-CONDUCT.md create mode 100644 CONTRIBUTING.md create mode 120000 LICENSE create mode 100644 SECURITY.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/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 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

💻
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. diff --git a/chopper/CHANGELOG.md b/chopper/CHANGELOG.md index 84af67b0..11f5b754 100644 --- a/chopper/CHANGELOG.md +++ b/chopper/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 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/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: ^ ``` 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/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/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 f71cfdbb..dcedd18c 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 documentation: https://hadrien-lejard.gitbook.io/chopper repository: https://github.com/lejard-h/chopper @@ -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.2.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/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 { 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/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_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 d1303271..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 @@ -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/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/lib/src/generator.dart b/chopper_generator/lib/src/generator.dart index 774d8f13..ce3efadf 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'; @@ -12,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. @@ -404,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); @@ -518,9 +519,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 +639,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) || @@ -717,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 f04ba058..cb26cfc6 100644 --- a/chopper_generator/lib/src/utils.dart +++ b/chopper_generator/lib/src/utils.dart @@ -1,10 +1,12 @@ +// ignore_for_file: deprecated_member_use + 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 92e14816..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,22 +11,21 @@ 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 meta: ^1.9.1 source_gen: ^1.5.0 yaml: ^3.1.2 - qs_dart: ^1.0.10 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 2888a256f5e3c8722bf280cf371327a3eae55687 Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Sun, 30 Jun 2024 11:50:49 +0100 Subject: [PATCH 58/60] :bookmark: release chopper v8.0.1+1 (#627) --- chopper/CHANGELOG.md | 4 ++++ chopper/lib/src/list_format.dart | 29 ----------------------------- 2 files changed, 4 insertions(+), 29 deletions(-) delete mode 100644 chopper/lib/src/list_format.dart diff --git a/chopper/CHANGELOG.md b/chopper/CHANGELOG.md index 11f5b754..fd922ce4 100644 --- a/chopper/CHANGELOG.md +++ b/chopper/CHANGELOG.md @@ -1,5 +1,9 @@ # 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)) 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; -} From d3d468e95ce03f8b7d97b3a2e33631224950e0ae Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Sun, 30 Jun 2024 12:29:24 +0100 Subject: [PATCH 59/60] :bookmark: release chopper v8.0.1+1 (again) (#628) --- chopper/pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chopper/pubspec.yaml b/chopper/pubspec.yaml index dcedd18c..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.1 +version: 8.0.1+1 documentation: https://hadrien-lejard.gitbook.io/chopper repository: https://github.com/lejard-h/chopper From b58a090fbbfb0d5a422dbe6076c276e8f4a18ad4 Mon Sep 17 00:00:00 2001 From: Klemen Tusar Date: Thu, 5 Sep 2024 06:08:16 +0100 Subject: [PATCH 60/60] :bookmark: release v8.0.2 (#637) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 🐛 properly escape single quote in cURL interceptor output #635 - 🎨 remove duplicate null-coalescing responseTypeReference #634 --- chopper/CHANGELOG.md | 4 ++ .../src/interceptors/curl_interceptor.dart | 4 +- chopper/pubspec.yaml | 6 +- chopper/test/interceptors_test.dart | 67 ++++++++++++------- chopper_generator/CHANGELOG.md | 4 ++ chopper_generator/lib/src/generator.dart | 5 +- chopper_generator/pubspec.yaml | 2 +- 7 files changed, 60 insertions(+), 32 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/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/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/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/'", ), ); }); 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/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)) 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