From d707d3f819b12b50f8a8870ca532671836d15d05 Mon Sep 17 00:00:00 2001 From: Ivan Terekhin Date: Tue, 1 Jun 2021 18:03:29 +0300 Subject: [PATCH 01/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] 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/30] :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/30] :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/30] :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/30] :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/30] :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/30] :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/30] :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/30] :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/30] :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/30] :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/30] :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")