├── .all-contributorsrc
├── .github
├── CODEOWNERS
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ └── feature_request.md
├── dependabot.yml
└── workflows
│ ├── dart.yml
│ ├── publish.yml
│ └── publish_dry_run.yml
├── .gitignore
├── CODE-OF-CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── SECURITY.md
├── SUMMARY.md
├── chopper
├── CHANGELOG.md
├── LICENSE
├── Makefile
├── README.md
├── analysis_options.yaml
├── example
│ ├── definition.chopper.dart
│ ├── definition.dart
│ ├── main.dart
│ ├── tag.chopper.dart
│ └── tag.dart
├── lib
│ ├── chopper.dart
│ └── src
│ │ ├── annotations.dart
│ │ ├── authenticator.dart
│ │ ├── base.dart
│ │ ├── chain
│ │ ├── call.dart
│ │ ├── chain.dart
│ │ └── interceptor_chain.dart
│ │ ├── chopper_exception.dart
│ │ ├── chopper_http_exception.dart
│ │ ├── chopper_log_record.dart
│ │ ├── constants.dart
│ │ ├── converters.dart
│ │ ├── extensions.dart
│ │ ├── interceptors
│ │ ├── authenticator_interceptor.dart
│ │ ├── curl_interceptor.dart
│ │ ├── headers_interceptor.dart
│ │ ├── http_call_interceptor.dart
│ │ ├── http_logging_interceptor.dart
│ │ ├── interceptor.dart
│ │ ├── internal_interceptor.dart
│ │ ├── request_converter_interceptor.dart
│ │ ├── request_stream_interceptor.dart
│ │ └── response_converter_interceptor.dart
│ │ ├── request.dart
│ │ ├── response.dart
│ │ └── utils.dart
├── mono_pkg.yaml
├── pubspec.yaml
├── pubspec_overrides.yaml
└── test
│ ├── annotations_test.chopper.dart
│ ├── annotations_test.dart
│ ├── authenticator_test.dart
│ ├── base_test.dart
│ ├── chain
│ ├── authenticator_interceptor_test.dart
│ ├── interceptor_chain_test.dart
│ ├── request_converter_interceptor_test.dart
│ └── response_converter_interceptor_test.dart
│ ├── chopper_client_extended_test.dart
│ ├── chopper_exception_test.dart
│ ├── chopper_http_exception_test.dart
│ ├── client_test.dart
│ ├── converter_test.dart
│ ├── curl_interceptor_test.dart
│ ├── ensure_build_test.dart
│ ├── equatable_test.dart
│ ├── extensions_test.dart
│ ├── fake_authenticator.dart
│ ├── fixtures
│ ├── error_fixtures.dart
│ ├── example_enum.dart
│ ├── http_response_fixture.dart
│ ├── payload_fixture.dart
│ ├── request_fixture.dart
│ └── response_fixture.dart
│ ├── form_test.dart
│ ├── helpers
│ ├── fake_chain.dart
│ ├── http_response_extension.dart
│ └── payload.dart
│ ├── http_call_interceptor_test.dart
│ ├── http_logging_interceptor_test.dart
│ ├── interceptors_test.dart
│ ├── json_test.dart
│ ├── multipart_test.dart
│ ├── request_stream_interceptor_test.dart
│ ├── request_test.dart
│ ├── response_test.dart
│ ├── test_service.chopper.dart
│ ├── test_service.dart
│ ├── test_service_base_url.chopper.dart
│ ├── test_service_base_url.dart
│ ├── test_service_variable.chopper.dart
│ ├── test_service_variable.dart
│ ├── test_without_response_service.chopper.dart
│ ├── test_without_response_service.dart
│ └── utils_test.dart
├── chopper_built_value
├── CHANGELOG.md
├── LICENSE
├── Makefile
├── README.md
├── analysis_options.yaml
├── build.yaml
├── lib
│ └── chopper_built_value.dart
├── mono_pkg.yaml
├── pubspec.yaml
├── pubspec_overrides.yaml
└── test
│ ├── converter_test.dart
│ ├── data.dart
│ ├── data.g.dart
│ ├── serializers.dart
│ └── serializers.g.dart
├── chopper_generator
├── CHANGELOG.md
├── LICENSE
├── Makefile
├── README.md
├── analysis_options.yaml
├── build.yaml
├── lib
│ ├── chopper_generator.dart
│ └── src
│ │ ├── builder_factory.dart
│ │ ├── extensions.dart
│ │ ├── generator.dart
│ │ ├── utils.dart
│ │ └── vars.dart
├── mono_pkg.yaml
├── pubspec.yaml
├── pubspec_overrides.yaml
└── test
│ ├── ensure_build_test.dart
│ ├── test_service.chopper.dart
│ ├── test_service.dart
│ ├── test_service_variable.chopper.dart
│ ├── test_service_variable.dart
│ ├── test_without_response_service.chopper.dart
│ └── test_without_response_service.dart
├── codecov.yml
├── converters
├── built-value-converter.md
└── converters.md
├── docs
└── ci
│ └── ci_setup.md
├── example
├── Makefile
├── analysis_options.yaml
├── bin
│ ├── main_built_value.dart
│ ├── main_json_serializable.dart
│ └── main_json_serializable_squadron_worker_pool.dart
├── build.yaml
├── build_serializers.yaml
├── lib
│ ├── built_value_resource.chopper.dart
│ ├── built_value_resource.dart
│ ├── built_value_resource.g.dart
│ ├── built_value_serializers.dart
│ ├── built_value_serializers.g.dart
│ ├── json_decode_service.activator.g.dart
│ ├── json_decode_service.dart
│ ├── json_decode_service.vm.g.dart
│ ├── json_decode_service.worker.g.dart
│ ├── json_serializable.chopper.dart
│ ├── json_serializable.dart
│ └── json_serializable.g.dart
└── pubspec.yaml
├── faq.md
├── flutter_favorite.png
├── getting-started.md
├── interceptors.md
├── mono_repo.yaml
├── requests.md
└── tool
├── ci.sh
├── compare_versions.dart
├── makefile_helpers.sh
└── pubspec.yaml
/.all-contributorsrc:
--------------------------------------------------------------------------------
1 | {
2 | "projectName": "chopper",
3 | "projectOwner": "lejard-h",
4 | "files": [
5 | "README.md"
6 | ],
7 | "imageSize": 100,
8 | "commit": false,
9 | "contributors": [
10 | {
11 | "login": "Vovanella95",
12 | "name": "Uladzimir Paliukhovich",
13 | "avatar_url": "https://avatars.githubusercontent.com/u/11267533?v=4",
14 | "profile": "https://github.com/Vovanella95",
15 | "contributions": [
16 | "code"
17 | ]
18 | },
19 | {
20 | "login": "fryette",
21 | "name": "Eugeny Sampir",
22 | "avatar_url": "https://avatars.githubusercontent.com/u/3999503?v=4",
23 | "profile": "http://ysampir@gmail.com",
24 | "contributions": [
25 | "code"
26 | ]
27 | },
28 | {
29 | "login": "Guldem",
30 | "name": "Job Guldemeester",
31 | "avatar_url": "https://avatars.githubusercontent.com/u/11982796?v=4",
32 | "profile": "https://github.com/Guldem",
33 | "contributions": [
34 | "code",
35 | "review",
36 | "test",
37 | "doc"
38 | ]
39 | },
40 | {
41 | "login": "JEuler",
42 | "name": "Ivan Terekhin",
43 | "avatar_url": "https://avatars.githubusercontent.com/u/231950?v=4",
44 | "profile": "https://www.upwork.com/freelancers/~01192eefd8a1c267f7",
45 | "contributions": [
46 | "code",
47 | "review",
48 | "test",
49 | "doc"
50 | ]
51 | },
52 | {
53 | "login": "techouse",
54 | "name": "Klemen Tusar",
55 | "avatar_url": "https://avatars.githubusercontent.com/u/1174328?v=4",
56 | "profile": "https://github.com/techouse",
57 | "contributions": [
58 | "code",
59 | "review",
60 | "test",
61 | "doc"
62 | ]
63 | },
64 | {
65 | "login": "stewemetal",
66 | "name": "István Juhos",
67 | "avatar_url": "https://avatars.githubusercontent.com/u/5860632?v=4",
68 | "profile": "https://github.com/stewemetal",
69 | "contributions": [
70 | "code",
71 | "review",
72 | "test",
73 | "doc"
74 | ]
75 | },
76 | {
77 | "login": "lejard-h",
78 | "name": "Hadrien Lejard",
79 | "avatar_url": "https://avatars.githubusercontent.com/u/7336262?v=4",
80 | "profile": "https://github.com/lejard-h",
81 | "contributions": [
82 | "code",
83 | "review",
84 | "test",
85 | "doc"
86 | ]
87 | }
88 | ],
89 | "repoType": "github",
90 | "contributorsPerLine": 7,
91 | "repoHost": "https://github.com",
92 | "skipCi": true
93 | }
94 |
--------------------------------------------------------------------------------
/.github/CODEOWNERS:
--------------------------------------------------------------------------------
1 | # These owners will be the default owners for everything in
2 | # the repo. Unless a later match takes precedence,
3 | # @global-owner1 and @global-owner2 will be requested for
4 | # review when someone opens a pull request.
5 | * @JEuler @lejard-h @meysam1717 @stewemetal @techouse
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: The application is crashing or throws an exception or something else looks wrong.
4 | title: ''
5 | labels: bug
6 | assignees: ''
7 |
8 | ---
9 |
10 | ## Steps to Reproduce
11 |
12 |
13 |
14 | 1. Execute `dart run` on the code sample
15 | 2. ...
16 | 3. ...
17 |
18 | **Expected results:**
19 |
20 | **Actual results:**
21 |
22 |
23 | Code sample
24 |
25 |
37 |
38 | ```dart
39 | ```
40 |
41 |
42 |
43 |
44 | Logs
45 |
46 |
52 |
53 | ```
54 | ```
55 |
56 |
60 |
61 | ```
62 | ```
63 |
64 |
65 |
66 | ```
67 | ```
68 |
69 |
70 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: ''
5 | labels: enhancement
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Is your feature request related to a problem? Please describe.**
11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
12 |
13 | **Describe the solution you'd like**
14 | A clear and concise description of what you want to happen.
15 |
16 | **Describe alternatives you've considered**
17 | A clear and concise description of any alternative solutions or features you've considered.
18 |
19 | **Additional context**
20 | Add any other context or screenshots about the feature request here.
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: "github-actions"
4 | directory: "/"
5 | schedule:
6 | interval: "weekly"
7 | target-branch: "develop"
8 | - package-ecosystem: "pub"
9 | directory: "/chopper"
10 | schedule:
11 | interval: "weekly"
12 | target-branch: "develop"
13 | - package-ecosystem: "pub"
14 | directory: "/chopper_built_value"
15 | schedule:
16 | interval: "weekly"
17 | target-branch: "develop"
18 | - package-ecosystem: "pub"
19 | directory: "/chopper_generator"
20 | schedule:
21 | interval: "weekly"
22 | target-branch: "develop"
--------------------------------------------------------------------------------
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------
1 | name: Publish packages
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 | defaults:
8 | run:
9 | shell: bash
10 | env:
11 | PUB_ENVIRONMENT: bot.github
12 | permissions: read-all
13 |
14 | jobs:
15 | get_base_version:
16 | name: "Get base version"
17 | runs-on: ubuntu-latest
18 | strategy:
19 | matrix:
20 | package: [ chopper, chopper_generator, chopper_built_value ]
21 | outputs:
22 | BASE_VERSION_chopper: ${{ steps.load_base_version.outputs.BASE_VERSION_chopper }}
23 | BASE_VERSION_chopper_generator: ${{ steps.load_base_version.outputs.BASE_VERSION_chopper_generator }}
24 | BASE_VERSION_chopper_built_value: ${{ steps.load_base_version.outputs.BASE_VERSION_chopper_built_value }}
25 | steps:
26 | - uses: dart-lang/setup-dart@v1
27 | with:
28 | sdk: stable
29 | - id: checkout
30 | uses: actions/checkout@v4
31 | with:
32 | fetch-depth: 2
33 | - run: git checkout HEAD^
34 | - name: Load base version
35 | id: load_base_version
36 | working-directory: ${{ matrix.package }}
37 | run: |
38 | set -e
39 | echo "BASE_VERSION_${{ matrix.package }}=$(yq -r '.version' pubspec.yaml)" >> $GITHUB_OUTPUT
40 | publish:
41 | name: "Publish"
42 | needs: get_base_version
43 | runs-on: ubuntu-latest
44 | permissions:
45 | contents: write
46 | strategy:
47 | matrix:
48 | package: [ chopper, chopper_generator, chopper_built_value ]
49 | fail-fast: true
50 | max-parallel: 1
51 | steps:
52 | - uses: dart-lang/setup-dart@v1
53 | with:
54 | sdk: stable
55 | - id: checkout
56 | uses: actions/checkout@v4
57 | - name: Load this version
58 | id: load_this_version
59 | working-directory: ${{ matrix.package }}
60 | run: |
61 | set -e
62 | echo "THIS_VERSION=$(yq -r '.version' pubspec.yaml)" >> $GITHUB_ENV
63 | - name: Compare versions
64 | id: compare_versions
65 | env:
66 | BASE_VERSION_chopper: ${{ needs.get_base_version.outputs.BASE_VERSION_chopper }}
67 | BASE_VERSION_chopper_generator: ${{ needs.get_base_version.outputs.BASE_VERSION_chopper_generator }}
68 | BASE_VERSION_chopper_built_value: ${{ needs.get_base_version.outputs.BASE_VERSION_chopper_built_value }}
69 | working-directory: tool
70 | run: |
71 | set -e
72 | dart pub get
73 | echo "IS_VERSION_GREATER=$(dart run compare_versions.dart $THIS_VERSION $BASE_VERSION_${{ matrix.package }})" >> $GITHUB_ENV
74 | - name: Validate pub.dev topics
75 | id: validate_pub_dev_topics
76 | working-directory: ${{ matrix.package }}
77 | run: |
78 | set -e
79 | pattern="^[a-z][a-z0-9-]*[a-z0-9]$"
80 | for topic in $(yq -r '.topics[]' pubspec.yaml); do
81 | if [[ ! $topic =~ $pattern ]]; then
82 | echo "Invalid topic: $topic"
83 | exit 1
84 | fi
85 | done
86 | - name: Create release-specific CHANGELOG
87 | id: create_changelog
88 | if: ${{ env.IS_VERSION_GREATER == 1 }}
89 | working-directory: ${{ matrix.package }}
90 | run: |
91 | set -e
92 | CHANGELOG_PATH=$RUNNER_TEMP/CHANGELOG.md
93 | awk '/^##[[:space:]].*/ { if (count == 1) exit; count++; print } count == 1 && !/^##[[:space:]].*/ { print }' CHANGELOG.md | sed -e :a -e '/^\n*$/{$d;N;ba' -e '}' > $CHANGELOG_PATH
94 | echo -en "\n[https://pub.dev/packages/${{ matrix.package }}/versions/$THIS_VERSION](https://pub.dev/packages/${{ matrix.package }}/versions/$THIS_VERSION)" >> $CHANGELOG_PATH
95 | echo "CHANGELOG_PATH=$CHANGELOG_PATH" >> $GITHUB_ENV
96 | - name: Set up pub credentials
97 | id: credentials
98 | if: ${{ env.IS_VERSION_GREATER == 1 }}
99 | run: |
100 | set -e
101 | mkdir -p $XDG_CONFIG_HOME/dart
102 | echo -n '${{ secrets.CREDENTIAL_JSON }}' > $XDG_CONFIG_HOME/dart/pub-credentials.json
103 | - name: Publish
104 | id: publish
105 | if: ${{ env.IS_VERSION_GREATER == 1 }}
106 | working-directory: ${{ matrix.package }}
107 | run: dart pub publish --force
108 | - name: Github release
109 | id: github_release
110 | if: ${{ env.IS_VERSION_GREATER == 1 }}
111 | uses: softprops/action-gh-release@v2
112 | with:
113 | name: ${{ format('{0}-v{1}', matrix.package, env.THIS_VERSION) }}
114 | tag_name: ${{ format('{0}-v{1}', matrix.package, env.THIS_VERSION) }}
115 | body_path: ${{ env.CHANGELOG_PATH }}
116 | - name: Skip publish
117 | id: skip_publish
118 | if: ${{ env.IS_VERSION_GREATER == 0 }}
119 | run: echo "Skipping publish for ${{ matrix.package }} because the version is not greater than the one on pub.dev"
120 | - name: Cleanup
121 | id: cleanup
122 | if: ${{ always() }}
123 | run: |
124 | rm -rf $XDG_CONFIG_HOME/dart/pub-credentials.json
125 | rm -rf $CHANGELOG_PATH
126 |
--------------------------------------------------------------------------------
/.github/workflows/publish_dry_run.yml:
--------------------------------------------------------------------------------
1 | name: Publish packages (dry run)
2 |
3 | on:
4 | pull_request:
5 | branches:
6 | - master
7 | defaults:
8 | run:
9 | shell: bash
10 | env:
11 | PUB_ENVIRONMENT: bot.github
12 | permissions: read-all
13 |
14 | jobs:
15 | get_base_version:
16 | name: "Get base version"
17 | runs-on: ubuntu-latest
18 | strategy:
19 | matrix:
20 | package: [ chopper, chopper_generator, chopper_built_value ]
21 | outputs:
22 | BASE_VERSION_chopper: ${{ steps.load_base_version.outputs.BASE_VERSION_chopper }}
23 | BASE_VERSION_chopper_generator: ${{ steps.load_base_version.outputs.BASE_VERSION_chopper_generator }}
24 | BASE_VERSION_chopper_built_value: ${{ steps.load_base_version.outputs.BASE_VERSION_chopper_built_value }}
25 | steps:
26 | - uses: dart-lang/setup-dart@v1
27 | with:
28 | sdk: stable
29 | - id: checkout
30 | uses: actions/checkout@v4
31 | with:
32 | ref: ${{ github.event.pull_request.base.ref }}
33 | - name: Load base version
34 | id: load_base_version
35 | working-directory: ${{ matrix.package }}
36 | run: |
37 | set -e
38 | echo "BASE_VERSION_${{ matrix.package }}=$(yq -r '.version' pubspec.yaml)" >> $GITHUB_OUTPUT
39 | publish_dry_run:
40 | name: "Publish DRY RUN"
41 | needs: get_base_version
42 | runs-on: ubuntu-latest
43 | strategy:
44 | matrix:
45 | package: [ chopper, chopper_generator, chopper_built_value ]
46 | fail-fast: true
47 | max-parallel: 1
48 | steps:
49 | - uses: dart-lang/setup-dart@v1
50 | with:
51 | sdk: stable
52 | - id: checkout
53 | uses: actions/checkout@v4
54 | - name: Load this version
55 | id: load_this_version
56 | working-directory: ${{ matrix.package }}
57 | run: |
58 | set -e
59 | echo "THIS_VERSION=$(yq -r '.version' pubspec.yaml)" >> $GITHUB_ENV
60 | - name: Compare versions
61 | id: compare_versions
62 | env:
63 | BASE_VERSION_chopper: ${{ needs.get_base_version.outputs.BASE_VERSION_chopper }}
64 | BASE_VERSION_chopper_generator: ${{ needs.get_base_version.outputs.BASE_VERSION_chopper_generator }}
65 | BASE_VERSION_chopper_built_value: ${{ needs.get_base_version.outputs.BASE_VERSION_chopper_built_value }}
66 | working-directory: tool
67 | run: |
68 | set -e
69 | dart pub get
70 | echo "IS_VERSION_GREATER=$(dart run compare_versions.dart $THIS_VERSION $BASE_VERSION_${{ matrix.package }})" >> $GITHUB_ENV
71 | - name: Validate pub.dev topics
72 | id: validate_pub_dev_topics
73 | working-directory: ${{ matrix.package }}
74 | run: |
75 | set -e
76 | pattern="^[a-z][a-z0-9-]*[a-z0-9]$"
77 | for topic in $(yq -r '.topics[]' pubspec.yaml); do
78 | if [[ ! $topic =~ $pattern ]]; then
79 | echo "Invalid topic: $topic"
80 | exit 1
81 | fi
82 | done
83 | - name: Create release-specific CHANGELOG
84 | id: create_changelog
85 | if: ${{ env.IS_VERSION_GREATER == 1 }}
86 | working-directory: ${{ matrix.package }}
87 | run: |
88 | set -e
89 | CHANGELOG_PATH=$RUNNER_TEMP/CHANGELOG.md
90 | awk '/^##[[:space:]].*/ { if (count == 1) exit; count++; print } count == 1 && !/^##[[:space:]].*/ { print }' CHANGELOG.md | sed -e :a -e '/^\n*$/{$d;N;ba' -e '}' > $CHANGELOG_PATH
91 | echo -en "\n[https://pub.dev/packages/${{ matrix.package }}/versions/$THIS_VERSION](https://pub.dev/packages/${{ matrix.package }}/versions/$THIS_VERSION)" >> $CHANGELOG_PATH
92 | echo "CHANGELOG_PATH=$CHANGELOG_PATH" >> $GITHUB_ENV
93 | - name: Publish (dry run)
94 | id: publish_dry_run
95 | if: ${{ env.IS_VERSION_GREATER == 1 }}
96 | working-directory: ${{ matrix.package }}
97 | run: dart pub publish --dry-run
98 | - name: Skip publish (dry run)
99 | id: skip_publish_dry_run
100 | if: ${{ env.IS_VERSION_GREATER == 0 }}
101 | run: echo "Skipping publish (dry run) for ${{ matrix.package }} because the version is not greater than the one on pub.dev"
102 | - name: Cleanup
103 | id: cleanup
104 | if: ${{ always() }}
105 | run: |
106 | rm -rf $CHANGELOG_PATH
107 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Files and directories created by pub
2 | .packages
3 | .pub/
4 | build/
5 | # Remove the following pattern if you wish to check in your lock file
6 | pubspec.lock
7 |
8 | # Directory created by dartdoc
9 | doc/api/
10 | .dart_tool
11 | .idea/
12 | .test_coverage.dart
13 | coverage
14 | chopper/doc/
15 | .vscode/
16 | pubspec.temp.yaml
17 | .DS_Store
18 |
--------------------------------------------------------------------------------
/CODE-OF-CONDUCT.md:
--------------------------------------------------------------------------------
1 |
2 | # Contributor Covenant Code of Conduct
3 |
4 | ## Our Pledge
5 |
6 | We as members, contributors, and leaders pledge to make participation in our
7 | community a harassment-free experience for everyone, regardless of age, body
8 | size, visible or invisible disability, ethnicity, sex characteristics, gender
9 | identity and expression, level of experience, education, socio-economic status,
10 | nationality, personal appearance, race, caste, color, religion, or sexual
11 | identity and orientation.
12 |
13 | We pledge to act and interact in ways that contribute to an open, welcoming,
14 | diverse, inclusive, and healthy community.
15 |
16 | ## Our Standards
17 |
18 | Examples of behavior that contributes to a positive environment for our
19 | community include:
20 |
21 | * Demonstrating empathy and kindness toward other people
22 | * Being respectful of differing opinions, viewpoints, and experiences
23 | * Giving and gracefully accepting constructive feedback
24 | * Accepting responsibility and apologizing to those affected by our mistakes,
25 | and learning from the experience
26 | * Focusing on what is best not just for us as individuals, but for the overall
27 | community
28 |
29 | Examples of unacceptable behavior include:
30 |
31 | * The use of sexualized language or imagery, and sexual attention or advances of
32 | any kind
33 | * Trolling, insulting or derogatory comments, and personal or political attacks
34 | * Public or private harassment
35 | * Publishing others' private information, such as a physical or email address,
36 | without their explicit permission
37 | * Other conduct which could reasonably be considered inappropriate in a
38 | professional setting
39 |
40 | ## Enforcement Responsibilities
41 |
42 | Community leaders are responsible for clarifying and enforcing our standards of
43 | acceptable behavior and will take appropriate and fair corrective action in
44 | response to any behavior that they deem inappropriate, threatening, offensive,
45 | or harmful.
46 |
47 | Community leaders have the right and responsibility to remove, edit, or reject
48 | comments, commits, code, wiki edits, issues, and other contributions that are
49 | not aligned to this Code of Conduct, and will communicate reasons for moderation
50 | decisions when appropriate.
51 |
52 | ## Scope
53 |
54 | This Code of Conduct applies within all community spaces, and also applies when
55 | an individual is officially representing the community in public spaces.
56 | Examples of representing our community include using an official email address,
57 | posting via an official social media account, or acting as an appointed
58 | representative at an online or offline event.
59 |
60 | ## Enforcement
61 |
62 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
63 | reported to the community leaders responsible for enforcement at
64 | [techouse@gmail.com](mailto:techouse@gmail.com).
65 | All complaints will be reviewed and investigated promptly and fairly.
66 |
67 | All community leaders are obligated to respect the privacy and security of the
68 | reporter of any incident.
69 |
70 | ## Enforcement Guidelines
71 |
72 | Community leaders will follow these Community Impact Guidelines in determining
73 | the consequences for any action they deem in violation of this Code of Conduct:
74 |
75 | ### 1. Correction
76 |
77 | **Community Impact**: Use of inappropriate language or other behavior deemed
78 | unprofessional or unwelcome in the community.
79 |
80 | **Consequence**: A private, written warning from community leaders, providing
81 | clarity around the nature of the violation and an explanation of why the
82 | behavior was inappropriate. A public apology may be requested.
83 |
84 | ### 2. Warning
85 |
86 | **Community Impact**: A violation through a single incident or series of
87 | actions.
88 |
89 | **Consequence**: A warning with consequences for continued behavior. No
90 | interaction with the people involved, including unsolicited interaction with
91 | those enforcing the Code of Conduct, for a specified period of time. This
92 | includes avoiding interactions in community spaces as well as external channels
93 | like social media. Violating these terms may lead to a temporary or permanent
94 | ban.
95 |
96 | ### 3. Temporary Ban
97 |
98 | **Community Impact**: A serious violation of community standards, including
99 | sustained inappropriate behavior.
100 |
101 | **Consequence**: A temporary ban from any sort of interaction or public
102 | communication with the community for a specified period of time. No public or
103 | private interaction with the people involved, including unsolicited interaction
104 | with those enforcing the Code of Conduct, is allowed during this period.
105 | Violating these terms may lead to a permanent ban.
106 |
107 | ### 4. Permanent Ban
108 |
109 | **Community Impact**: Demonstrating a pattern of violation of community
110 | standards, including sustained inappropriate behavior, harassment of an
111 | individual, or aggression toward or disparagement of classes of individuals.
112 |
113 | **Consequence**: A permanent ban from any sort of public interaction within the
114 | community.
115 |
116 | ## Attribution
117 |
118 | This Code of Conduct is adapted from the [Contributor Covenant][homepage],
119 | version 2.1, available at
120 | [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
121 |
122 | Community Impact Guidelines were inspired by
123 | [Mozilla's code of conduct enforcement ladder][Mozilla CoC].
124 |
125 | For answers to common questions about this code of conduct, see the FAQ at
126 | [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
127 | [https://www.contributor-covenant.org/translations][translations].
128 |
129 | [homepage]: https://www.contributor-covenant.org
130 | [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
131 | [Mozilla CoC]: https://github.com/mozilla/diversity
132 | [FAQ]: https://www.contributor-covenant.org/faq
133 | [translations]: https://www.contributor-covenant.org/translations
134 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | Thank you for your interest in contributing to this project. This project relies on the help of volunteer developers for
4 | its development and maintenance.
5 |
6 | Before making any changes to this repository, please first discuss the proposed changes with the repository owners
7 | through an issue, email, or any other appropriate method of communication.
8 |
9 | Please note that a [code of conduct](CODE-OF-CONDUCT.md) is in place and should be adhered to during all interactions
10 | related to the project.
11 |
12 | ## Dart version support
13 |
14 | Currently, the package supports Dart versions 3.0 and above. Once a new Dart version is released, we will aim to support
15 | it as soon as possible. If you encounter any issues with a new Dart version, please create an issue in the repository.
16 |
17 | ## Flutter support
18 |
19 | This package is designed to work with Flutter 3.10 and above. We prioritize and are dedicated to maintaining
20 | compatibility with these versions for a smooth user experience.
21 |
22 | ## Testing
23 |
24 | Given the critical nature of correctly generating HTTP requests and handling API responses in the Chopper package, and
25 | the potential for security vulnerabilities if this is not done correctly or consistently across platforms and versions
26 | of Dart and Flutter, thorough testing is of utmost importance. Please remember to write tests for any new code you
27 | create, using the [test](https://pub.dev/packages/test) package for all test cases.
28 |
29 | ### Running the test suite
30 |
31 | To run the test suite, follow these commands:
32 |
33 | ```bash
34 | git clone https://github.com/lejard-h/chopper.git
35 |
36 | pushd chopper
37 | dart pub get
38 | dart test --platform vm
39 | dart test --platform chrome
40 | popd
41 |
42 | pushd chopper_generator
43 | dart pub get
44 | dart test --platform vm
45 | dart test --platform chrome
46 | popd
47 |
48 | pushd chopper_built_value
49 | dart pub get
50 | dart test --platform vm
51 | dart test --platform chrome
52 | popd
53 | ```
54 |
55 | ### Running the test suite with coverage
56 |
57 | ```bash
58 | pushd chopper
59 | make show_test_coverage
60 | popd
61 |
62 | pushd chopper_generator
63 | make show_test_coverage
64 | popd
65 |
66 | pushd chopper_built_value
67 | make show_test_coverage
68 | popd
69 | ```
70 |
71 | ## Submitting changes
72 |
73 | To contribute to this project, please submit a new pull request and provide a clear list of your changes. For guidance
74 | on creating pull requests, you can refer to this resource. When sending a pull request, we highly appreciate the
75 | inclusion of tests, as we strive to enhance our test coverage.
76 | Following our coding conventions is essential, and it would be ideal if you ensure that each commit focuses on a single
77 | feature. For commits, please write clear log messages. While concise one-line messages are suitable for small changes,
78 | more substantial modifications should follow a format similar to the example below:
79 |
80 | ```bash
81 | git commit -m "A brief summary of the commit
82 | >
83 | > A paragraph describing what changed and its impact."
84 | ```
85 |
86 | ## Coding standards
87 |
88 | Prioritizing code readability and conciseness is essential. To achieve this, we recommend using `dart format` for code
89 | formatting. Once your work is deemed complete, it is advisable to run the following command:
90 |
91 | ```bash
92 | pushd chopper
93 | dart format lib test --output=none --set-exit-if-changed .
94 | dart analyze lib test --fatal-infos
95 | popd
96 |
97 | pushd chopper_generator
98 | dart format lib test --output=none --set-exit-if-changed .
99 | dart analyze lib test --fatal-infos
100 | popd
101 |
102 | pushd chopper_built_value
103 | dart format lib test --output=none --set-exit-if-changed .
104 | dart analyze lib test --fatal-infos
105 | popd
106 | ```
107 |
108 | This command runs the Dart analyzer to identify any potential issues or inconsistencies in your code. By following these
109 | guidelines, you can ensure a high-quality codebase.
110 |
111 | Thanks!
112 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | chopper/LICENSE
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Chopper
2 |
3 |
4 | [](#contributors-)
5 |
6 | [](https://pub.dartlang.org/packages/chopper)
7 | [](https://github.com/lejard-h/chopper/actions?query=workflow%3A%22Dart+CI%22)
8 | [](https://codecov.io/gh/lejard-h/chopper)
9 |
10 | [ ](https://flutter.dev/docs/development/packages-and-plugins/favorites)
11 |
12 | Chopper is an http client generator for Dart and Flutter using source_gen and inspired by Retrofit.
13 |
14 | [Documentation](https://hadrien-lejard.gitbook.io/chopper)
15 |
16 | ## Installation
17 |
18 | Please refer to the installation guide at [pub.dev](https://pub.dev/packages/chopper/install) or in the [Getting started](getting-started.md) document.
19 |
20 | * [Requests](requests.md)
21 | * [Converters](converters/converters.md)
22 | * [Interceptors](interceptors.md)
23 |
24 | ## Examples
25 |
26 | * [json serializable Converter](https://github.com/lejard-h/chopper/blob/master/example/bin/main_json_serializable.dart)
27 | * [built value Converter](https://github.com/lejard-h/chopper/blob/master/example/bin/main_built_value.dart)
28 |
29 | ## [Issue Tracker](https://github.com/lejard-h/chopper/issues)
30 |
31 | ## Contributors ✨
32 |
33 | Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
34 |
35 |
36 |
37 |
38 |
49 |
50 |
51 |
52 |
53 |
54 |
55 | This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!
56 |
--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------
1 | # Security Policy
2 |
3 | ## Supported Versions
4 |
5 | | Version | Supported |
6 | |---------|--------------------|
7 | | 8.x.x | :white_check_mark: |
8 | | 7.x.x | :x: |
9 | | 6.x.x | :x: |
10 | | 5.x.x | :x: |
11 | | 4.x.x | :x: |
12 | | 3.x.x | :x: |
13 | | 2.x.x | :x: |
14 | | 1.x.x | :x: |
15 | | 0.x.x | :x: |
16 |
17 |
18 | ## Reporting a Vulnerability
19 |
20 | We take the security of our software seriously. If you believe you have found a security vulnerability, please report it
21 | to us as described below.
22 |
23 | **DO NOT CREATE A GITHUB ISSUE** reporting the vulnerability.
24 |
25 | Instead, send an email to either [techouse@gmail.com](mailto:techouse@gmail.com) or
26 | [i.terhin@gmail.com](mailto:i.terhin@gmail.com).
27 |
28 | In the report, please include the following:
29 |
30 | - Your name and affiliation (if any).
31 | - A description of the technical details of the vulnerabilities. It is very important to let us know how we can
32 | reproduce your findings.
33 | - An explanation who can exploit this vulnerability, and what they gain when doing so -- write an attack scenario. This
34 | will help us evaluate your submission quickly, especially if it is a complex or creative vulnerability.
35 | - Whether this vulnerability is public or known to third parties. If it is, please provide details.
36 |
37 | If you don’t get an acknowledgment from us or have heard nothing from us in a week, please contact us again.
38 |
39 | We will send a response indicating the next steps in handling your report. We will keep you informed about the progress
40 | towards a fix and full announcement.
41 |
42 | We will not disclose your identity to the public without your permission. We strive to credit researchers in our
43 | advisories when we release a fix, but only after getting your permission.
44 |
45 | We appreciate your efforts to responsibly disclose your findings, and will make every effort to acknowledge your
46 | contributions.
47 |
--------------------------------------------------------------------------------
/SUMMARY.md:
--------------------------------------------------------------------------------
1 | # Table of contents
2 |
3 | * [Chopper](README.md)
4 | * [Getting started](getting-started.md)
5 | * [Requests](requests.md)
6 | * [Interceptors](interceptors.md)
7 | * [FAQ](faq.md)
8 | * [API Reference](https://pub.dev/documentation/chopper/latest/)
9 |
10 | ## Converters
11 |
12 | * [Converters](converters/converters.md)
13 | * [Built Value Converter](converters/built-value-converter.md)
14 |
15 |
--------------------------------------------------------------------------------
/chopper/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Hadrien Lejard
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/chopper/Makefile:
--------------------------------------------------------------------------------
1 | # Makefile
2 |
3 | help:
4 | @printf "%-20s %s\n" "Target" "Description"
5 | @printf "%-20s %s\n" "------" "-----------"
6 | @make -pqR : 2>/dev/null \
7 | | awk -v RS= -F: '/^# File/,/^# Finished Make data base/ {if ($$1 !~ "^[#.]") {print $$1}}' \
8 | | sort \
9 | | egrep -v -e '^[^[:alnum:]]' -e '^$@$$' \
10 | | xargs -I _ sh -c 'printf "%-20s " _; make _ -nB | (grep -i "^# Help:" || echo "") | tail -1 | sed "s/^# Help: //g"'
11 |
12 | analyze:
13 | @# Help: Analyze the project's Dart code.
14 | dart analyze --fatal-infos
15 |
16 | check_format:
17 | @# Help: Check the formatting of one or more Dart files.
18 | dart format --output=none --set-exit-if-changed .
19 |
20 | check_outdated:
21 | @# Help: Check which of the project's packages are outdated.
22 | dart pub outdated
23 |
24 | check_style:
25 | @# Help: Analyze the project's Dart code and check the formatting one or more Dart files.
26 | make analyze && make check_format
27 |
28 | code_gen:
29 | @# Help: Run the build system for Dart code generation and modular compilation.
30 | dart run build_runner build --delete-conflicting-outputs
31 |
32 | code_gen_watcher:
33 | @# Help: Run the build system for Dart code generation and modular compilation as a watcher.
34 | dart run build_runner watch --delete-conflicting-outputs
35 |
36 | format:
37 | @# Help: Format one or more Dart files.
38 | dart format .
39 |
40 | install:
41 | @# Help: Install all the project's packages
42 | dart pub get
43 |
44 | sure:
45 | @# Help: Analyze the project's Dart code, check the formatting one or more Dart files and run unit tests for the current project.
46 | make check_style && make tests
47 |
48 | show_test_coverage:
49 | @# Help: Run Dart unit tests for the current project and show the coverage.
50 | dart pub global activate coverage && dart pub global run coverage:test_with_coverage
51 | lcov --remove coverage/lcov.info '**.g.dart' '**.mock.dart' '**.chopper.dart' -o coverage/lcov_without_generated_code.info --ignore-errors unused
52 | genhtml coverage/lcov_without_generated_code.info -o coverage/html
53 | source ../tool/makefile_helpers.sh && open_link "coverage/html/index.html"
54 |
55 | tests:
56 | @# Help: Run Dart unit and widget tests for the current project.
57 | dart test
58 |
59 | upgrade:
60 | @# Help: Upgrade all the project's packages.
61 | dart pub upgrade
--------------------------------------------------------------------------------
/chopper/README.md:
--------------------------------------------------------------------------------
1 | # Chopper
2 |
3 | [](https://pub.dartlang.org/packages/chopper)
4 | [](https://github.com/lejard-h/chopper/actions?query=workflow%3A%22Dart+CI%22)
5 | [](https://codecov.io/gh/lejard-h/chopper)
6 |
7 | [ ](https://flutter.dev/docs/development/packages-and-plugins/favorites)
8 |
9 | Chopper is an http client generator for Dart and Flutter using source_gen and inspired by Retrofit.
10 |
11 | [**Documentation**](https://hadrien-lejard.gitbook.io/chopper)
12 |
13 | ## Adding Chopper to your project
14 |
15 | In your project's `pubspec.yaml` file,
16 |
17 | * Add *chopper*'s latest version to your *dependencies*.
18 | * Add `build_runner: ^2.4.9` to your *dev_dependencies*.
19 | * *build_runner* may already be in your *dev_dependencies* depending on your project setup and other dependencies.
20 | * Add *chopper_generator*'s latest version to your *dev_dependencies*.
21 |
22 | ```yaml
23 | # pubspec.yaml
24 |
25 | dependencies:
26 | chopper: ^
27 |
28 | dev_dependencies:
29 | build_runner: ^2.4.9
30 | chopper_generator: ^
31 | ```
32 |
33 | Latest versions:
34 |
35 | * *chopper* 
36 | * *chopper_generator* 
37 |
38 | ## Documentation
39 |
40 | * [Getting started](../getting-started.md)
41 | * [Converters](../converters/converters.md)
42 | * [Interceptors](../interceptors.md)
43 |
44 | ## Examples
45 |
46 | * [json_serializable Converter](https://github.com/lejard-h/chopper/blob/master/example/bin/main_json_serializable.dart)
47 | * [built_value Converter](https://github.com/lejard-h/chopper/blob/master/example/bin/main_built_value.dart)
48 |
49 | ## 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).
50 |
--------------------------------------------------------------------------------
/chopper/analysis_options.yaml:
--------------------------------------------------------------------------------
1 | include: package:lints/recommended.yaml
2 |
3 | analyzer:
4 | exclude:
5 | - "**.g.dart"
6 | - "**.chopper.dart"
7 | - "**.mocks.dart"
8 | - "example/**"
9 |
10 | linter:
11 | rules:
12 | avoid_print: true
13 | prefer_single_quotes: true
14 |
--------------------------------------------------------------------------------
/chopper/example/definition.chopper.dart:
--------------------------------------------------------------------------------
1 | // dart format width=80
2 | // GENERATED CODE - DO NOT MODIFY BY HAND
3 |
4 | part of 'definition.dart';
5 |
6 | // **************************************************************************
7 | // ChopperGenerator
8 | // **************************************************************************
9 |
10 | // coverage:ignore-file
11 | // ignore_for_file: type=lint
12 | final class _$MyService extends MyService {
13 | _$MyService([ChopperClient? client]) {
14 | if (client == null) return;
15 | this.client = client;
16 | }
17 |
18 | @override
19 | final Type definitionType = MyService;
20 |
21 | @override
22 | Future> getResource(String id) {
23 | final Uri $url = Uri.parse('/resources/${id}');
24 | final Request $request = Request(
25 | 'GET',
26 | $url,
27 | client.baseUrl,
28 | );
29 | return client.send($request);
30 | }
31 |
32 | @override
33 | Future>> getMapResource(String id) {
34 | final Uri $url = Uri.parse('/resources/');
35 | final Map $params = {'id': id};
36 | final Map $headers = {
37 | 'foo': 'bar',
38 | };
39 | final Request $request = Request(
40 | 'GET',
41 | $url,
42 | client.baseUrl,
43 | parameters: $params,
44 | headers: $headers,
45 | );
46 | return client.send, Map>($request);
47 | }
48 |
49 | @override
50 | Future>>> getListResources() {
51 | final Uri $url = Uri.parse('/resources/resources');
52 | final Request $request = Request(
53 | 'GET',
54 | $url,
55 | client.baseUrl,
56 | );
57 | return client
58 | .send>, Map>($request);
59 | }
60 |
61 | @override
62 | Future> postResourceUrlEncoded(
63 | String toto,
64 | String b,
65 | ) {
66 | final Uri $url = Uri.parse('/resources/');
67 | final $body = {
68 | 'a': toto,
69 | 'b': b,
70 | };
71 | final Request $request = Request(
72 | 'POST',
73 | $url,
74 | client.baseUrl,
75 | body: $body,
76 | );
77 | return client.send($request);
78 | }
79 |
80 | @override
81 | Future> postResources(
82 | Map a,
83 | Map b,
84 | String c,
85 | ) {
86 | final Uri $url = Uri.parse('/resources/multi');
87 | final List $parts = [
88 | PartValue>(
89 | '1',
90 | a,
91 | ),
92 | PartValue>(
93 | '2',
94 | b,
95 | ),
96 | PartValue(
97 | '3',
98 | c,
99 | ),
100 | ];
101 | final Request $request = Request(
102 | 'POST',
103 | $url,
104 | client.baseUrl,
105 | parts: $parts,
106 | multipart: true,
107 | );
108 | return client.send($request);
109 | }
110 |
111 | @override
112 | Future> postFile(List bytes) {
113 | final Uri $url = Uri.parse('/resources/file');
114 | final List $parts = [
115 | PartValue>(
116 | 'file',
117 | bytes,
118 | )
119 | ];
120 | final Request $request = Request(
121 | 'POST',
122 | $url,
123 | client.baseUrl,
124 | parts: $parts,
125 | multipart: true,
126 | );
127 | return client.send($request);
128 | }
129 | }
130 |
--------------------------------------------------------------------------------
/chopper/example/definition.dart:
--------------------------------------------------------------------------------
1 | import 'dart:async';
2 |
3 | import 'package:chopper/chopper.dart';
4 |
5 | part 'definition.chopper.dart';
6 |
7 | @ChopperApi(baseUrl: '/resources')
8 | abstract class MyService extends ChopperService {
9 | static MyService create(ChopperClient client) => _$MyService(client);
10 |
11 | @Get(path: '/{id}')
12 | Future getResource(
13 | @Path() String id,
14 | );
15 |
16 | @Get(path: '/', headers: {'foo': 'bar'})
17 | Future> getMapResource(
18 | @Query() String id,
19 | );
20 |
21 | @Get(path: '/resources')
22 | Future>> getListResources();
23 |
24 | @Post(path: '/')
25 | Future postResourceUrlEncoded(
26 | @Field('a') String toto,
27 | @Field() String b,
28 | );
29 |
30 | @Post(path: '/multi')
31 | @multipart
32 | Future postResources(
33 | @Part('1') Map a,
34 | @Part('2') Map b,
35 | @Part('3') String c,
36 | );
37 |
38 | @Post(path: '/file')
39 | @multipart
40 | Future postFile(
41 | @Part('file') List bytes,
42 | );
43 | }
44 |
--------------------------------------------------------------------------------
/chopper/example/main.dart:
--------------------------------------------------------------------------------
1 | import 'package:chopper/chopper.dart';
2 |
3 | import 'definition.dart';
4 |
5 | Future main() async {
6 | final chopper = ChopperClient(
7 | baseUrl: Uri.parse('http://localhost:8000'),
8 | services: [
9 | // the generated service
10 | MyService.create(ChopperClient()),
11 | ],
12 | converter: JsonConverter(),
13 | );
14 |
15 | final myService = chopper.getService();
16 |
17 | final response = await myService.getMapResource('1');
18 | print(response.body);
19 |
20 | final list = await myService.getListResources();
21 | print(list.body);
22 |
23 | chopper.dispose();
24 | }
25 |
--------------------------------------------------------------------------------
/chopper/example/tag.chopper.dart:
--------------------------------------------------------------------------------
1 | // dart format width=80
2 | // GENERATED CODE - DO NOT MODIFY BY HAND
3 |
4 | part of 'tag.dart';
5 |
6 | // **************************************************************************
7 | // ChopperGenerator
8 | // **************************************************************************
9 |
10 | // coverage:ignore-file
11 | // ignore_for_file: type=lint
12 | final class _$TagService extends TagService {
13 | _$TagService([ChopperClient? client]) {
14 | if (client == null) return;
15 | this.client = client;
16 | }
17 |
18 | @override
19 | final Type definitionType = TagService;
20 |
21 | @override
22 | Future> requestWithTag({BizTag tag = const BizTag()}) {
23 | final Uri $url = Uri.parse('/tag');
24 | final Request $request = Request(
25 | 'GET',
26 | $url,
27 | client.baseUrl,
28 | tag: tag,
29 | );
30 | return client.send($request);
31 | }
32 |
33 | @override
34 | Future> includeBodyNullOrEmptyTag(
35 | {IncludeBodyNullOrEmptyTag tag = const IncludeBodyNullOrEmptyTag()}) {
36 | final Uri $url = Uri.parse('/tag');
37 | final Request $request = Request(
38 | 'GET',
39 | $url,
40 | client.baseUrl,
41 | tag: tag,
42 | );
43 | return client.send($request);
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/chopper/example/tag.dart:
--------------------------------------------------------------------------------
1 | /// @author luwenjie on 2024/3/20 11:38:11
2 | ///
3 | ///
4 | ///
5 | import "package:chopper/chopper.dart";
6 |
7 | import 'definition.dart';
8 |
9 | part 'tag.chopper.dart';
10 |
11 | Future main() async {
12 | final chopper = ChopperClient(
13 | baseUrl: Uri.parse('http://localhost:8000'),
14 | services: [
15 | // the generated service
16 | TagService.create(ChopperClient()),
17 | ],
18 | interceptors: [
19 | TagInterceptor(),
20 | ],
21 | converter: JsonConverter(),
22 | );
23 |
24 | final myService = chopper.getService();
25 |
26 | final response = await myService.getMapResource('1');
27 | print(response.body);
28 |
29 | final list = await myService.getListResources();
30 | print(list.body);
31 | chopper.dispose();
32 | }
33 |
34 | // add a uniform appId header for some path
35 | class BizTag {
36 | final int appId;
37 |
38 | BizTag({this.appId = 0});
39 | }
40 |
41 | class IncludeBodyNullOrEmptyTag {
42 | bool includeNull = false;
43 | bool includeEmpty = false;
44 |
45 | IncludeBodyNullOrEmptyTag(this.includeNull, this.includeEmpty);
46 | }
47 |
48 | class TagConverter extends JsonConverter {
49 | FutureOr convertRequest(Request request) {
50 | final tag = request.tag;
51 | if (tag is IncludeBodyNullOrEmptyTag) {
52 | if (request.body is Map) {
53 | final Map body = request.body as Map;
54 | final Map bodyCopy = {};
55 | for (final MapEntry entry in body.entries) {
56 | if (!tag.includeNull && entry.value == null) continue;
57 | if (!tag.includeEmpty && entry.value == "") continue;
58 | bodyCopy[entry.key] = entry.value;
59 | }
60 | request = request.copyWith(body: bodyCopy);
61 | }
62 | }
63 | }
64 | }
65 |
66 | class TagInterceptor implements RequestInterceptor {
67 | FutureOr onRequest(Request request) {
68 | final tag = request.tag;
69 | if (tag is BizTag) {
70 | request.headers["x-appId"] = tag.appId;
71 | }
72 | return request;
73 | }
74 | }
75 |
76 | @ChopperApi(baseUrl: '/tag')
77 | abstract class TagService extends ChopperService {
78 | static TagService create(ChopperClient client) => _$TagService(client);
79 |
80 | @get(path: '/bizRequest')
81 | Future requestWithTag({@Tag() BizTag tag = const BizTag()});
82 |
83 | @get(path: '/include')
84 | Future includeBodyNullOrEmptyTag(
85 | {@Tag()
86 | IncludeBodyNullOrEmptyTag tag = const IncludeBodyNullOrEmptyTag()});
87 | }
88 |
--------------------------------------------------------------------------------
/chopper/lib/chopper.dart:
--------------------------------------------------------------------------------
1 | /// Chopper is an http client generator using source_gen and inspired by Retrofit.
2 | ///
3 | /// [Getting Started](https://hadrien-lejard.gitbook.io/chopper)
4 | library;
5 |
6 | export 'package:qs_dart/qs_dart.dart' show ListFormat;
7 | export 'src/annotations.dart';
8 | export 'src/authenticator.dart';
9 | export 'src/base.dart';
10 | export 'src/chopper_http_exception.dart';
11 | export 'src/chopper_exception.dart';
12 | export 'src/chopper_log_record.dart';
13 | export 'src/constants.dart';
14 | export 'src/extensions.dart';
15 | export 'src/chain/chain.dart';
16 | export 'src/interceptors/interceptor.dart';
17 | export 'src/converters.dart';
18 | export 'src/request.dart';
19 | export 'src/response.dart';
20 | export 'src/utils.dart' hide mapToQuery;
21 |
--------------------------------------------------------------------------------
/chopper/lib/src/authenticator.dart:
--------------------------------------------------------------------------------
1 | import 'dart:async';
2 |
3 | import 'package:chopper/chopper.dart';
4 |
5 | ///
6 | /// Callback that is called when an authentication challenge is received
7 | /// based on the given [request], [response], and optionally the
8 | /// [originalRequest].
9 | ///
10 | typedef AuthenticationCallback = FutureOr Function(
11 | Request request,
12 | Response response, [
13 | Request? originalRequest,
14 | ]);
15 |
16 | ///
17 | /// Handles authentication challenges raised by the [ChopperClient].
18 | ///
19 | /// Optionally, you can override either [onAuthenticationSuccessful] or
20 | /// [onAuthenticationFailed] in order to listen to when a particular
21 | /// authentication request succeeds or fails.
22 | ///
23 | /// For example, you can use these in order to reset or mutate your
24 | /// instance's internal state for the purposes of keeping track of
25 | /// the number of retries made to authenticate a request.
26 | ///
27 | /// Furthermore, you can use these callbacks to determine whether
28 | /// your authentication [Request] from [authenticate] actually succeeded
29 | /// or failed.
30 | ///
31 | abstract class Authenticator {
32 | ///
33 | /// Returns a [Request] that includes credentials to satisfy
34 | /// an authentication challenge received in [response], based on
35 | /// the incoming [request] or optionally, the [originalRequest]
36 | /// (which was not modified with any previous [Interceptor]s).
37 | ///
38 | /// Otherwise, return `null` if the challenge cannot be satisfied.
39 | ///
40 | FutureOr authenticate(
41 | Request request,
42 | Response response, [
43 | Request? originalRequest,
44 | ]);
45 |
46 | ///
47 | /// Optional callback called by [ChopperClient] when the outgoing
48 | /// request from [authenticate] was successful.
49 | ///
50 | /// You can use this to determine whether that request actually succeeded
51 | /// in authenticating the user.
52 | ///
53 | AuthenticationCallback? get onAuthenticationSuccessful => null;
54 |
55 | ///
56 | /// Optional callback called by [ChopperClient] when the outgoing
57 | /// request from [authenticate] failed to authenticate.
58 | ///
59 | /// You can use this to determine whether that request failed to recover
60 | /// the user's session.
61 | ///
62 | AuthenticationCallback? get onAuthenticationFailed => null;
63 | }
64 |
--------------------------------------------------------------------------------
/chopper/lib/src/chain/call.dart:
--------------------------------------------------------------------------------
1 | import 'package:chopper/src/annotations.dart';
2 | import 'package:chopper/src/base.dart';
3 | import 'package:chopper/src/chain/interceptor_chain.dart';
4 | import 'package:chopper/src/interceptors/authenticator_interceptor.dart';
5 | import 'package:chopper/src/interceptors/http_call_interceptor.dart';
6 | import 'package:chopper/src/interceptors/interceptor.dart';
7 | import 'package:chopper/src/interceptors/request_converter_interceptor.dart';
8 | import 'package:chopper/src/interceptors/request_stream_interceptor.dart';
9 | import 'package:chopper/src/interceptors/response_converter_interceptor.dart';
10 | import 'package:chopper/src/request.dart';
11 | import 'package:chopper/src/response.dart';
12 |
13 | /// {@template Call}
14 | /// A single call to a HTTP endpoint. It holds the [request] and the [client].
15 | /// {@endtemplate}
16 | class Call {
17 | /// {@macro Call}
18 | Call({
19 | required this.request,
20 | required this.client,
21 | required this.requestCallback,
22 | });
23 |
24 | /// Request to be executed.
25 | final Request request;
26 |
27 | /// Chopper client that created this call.
28 | final ChopperClient client;
29 |
30 | /// Callback to send intercepted and converted request to the stream controller.
31 | final void Function(Request event) requestCallback;
32 |
33 | Future> execute(
34 | ConvertRequest? requestConverter,
35 | ConvertResponse? responseConverter,
36 | ) async {
37 | final interceptors = [
38 | RequestConverterInterceptor(client.converter, requestConverter),
39 | ...client.interceptors,
40 | RequestStreamInterceptor(requestCallback),
41 | if (client.authenticator != null)
42 | AuthenticatorInterceptor(client.authenticator!),
43 | ResponseConverterInterceptor(
44 | converter: client.converter,
45 | errorConverter: client.errorConverter,
46 | responseConverter: responseConverter,
47 | ),
48 | HttpCallInterceptor(client.httpClient),
49 | ];
50 |
51 | final interceptorChain = InterceptorChain(
52 | request: request,
53 | interceptors: interceptors,
54 | );
55 |
56 | return await interceptorChain.proceed(request);
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/chopper/lib/src/chain/chain.dart:
--------------------------------------------------------------------------------
1 | import 'dart:async';
2 |
3 | import 'package:chopper/src/request.dart';
4 | import 'package:chopper/src/response.dart';
5 |
6 | /// A single chain instance in the chain of interceptors that is called in order to process requests and responses.
7 | ///
8 | /// The chain is used to proceed to the next interceptor in the chain.
9 | /// Call [proceed] to proceed to the next interceptor in the chain.
10 | /// ```dart
11 | /// await chain.proceed(request);
12 | /// ```
13 | abstract interface class Chain {
14 | /// Proceed to the next interceptor in the chain.
15 | /// Provide the [request] to be processed by the next interceptor.
16 | FutureOr> proceed(Request request);
17 |
18 | /// The request to be processed by the chain up to this point.
19 | /// The request is provide by the previous interceptor in the chain.
20 | Request get request;
21 | }
22 |
--------------------------------------------------------------------------------
/chopper/lib/src/chain/interceptor_chain.dart:
--------------------------------------------------------------------------------
1 | import 'dart:async';
2 |
3 | import 'package:chopper/src/chain/chain.dart';
4 | import 'package:chopper/src/chopper_exception.dart';
5 | import 'package:chopper/src/interceptors/interceptor.dart';
6 | import 'package:chopper/src/interceptors/internal_interceptor.dart';
7 | import 'package:chopper/src/request.dart';
8 | import 'package:chopper/src/response.dart';
9 |
10 | /// {@template InterceptorChain}
11 | /// A chain of interceptors that are called in order to process requests and responses.
12 | /// {@endtemplate}
13 | class InterceptorChain implements Chain {
14 | /// {@macro InterceptorChain}
15 | InterceptorChain({
16 | required this.interceptors,
17 | required this.request,
18 | this.index = 0,
19 | }) : assert(interceptors.isNotEmpty, 'Interceptors list must not be empty');
20 |
21 | @override
22 | final Request request;
23 |
24 | /// Response received from the next interceptor in the chain.
25 | Response? response;
26 |
27 | /// List of interceptors to be called in order.
28 | final List interceptors;
29 |
30 | /// Index of the current interceptor in the chain.
31 | final int index;
32 |
33 | @override
34 | FutureOr> proceed(Request request) async {
35 | assert(index < interceptors.length, 'Interceptor index out of bounds');
36 | if (index - 1 >= 0 && interceptors[index - 1] is! InternalInterceptor) {
37 | assert(
38 | this.request.body == request.body,
39 | 'Interceptor [${interceptors[index - 1].runtimeType}] should not transform the body of the request, '
40 | 'Use Request converter instead',
41 | );
42 | }
43 |
44 | final interceptor = interceptors[index];
45 | final next = copyWith(request: request, index: index + 1);
46 | response = await interceptor.intercept(next);
47 |
48 | if (index + 1 < interceptors.length &&
49 | interceptor is! InternalInterceptor) {
50 | if (response == null) {
51 | throw ChopperException('Response is null', request: request);
52 | }
53 |
54 | assert(
55 | response?.body == next.response?.body,
56 | 'Interceptor [${interceptor.runtimeType}] should not transform the body of the response, '
57 | 'Use Response converter instead',
58 | );
59 | }
60 |
61 | return response!;
62 | }
63 |
64 | /// Copy the current [InterceptorChain]. With updated [request] or [index].
65 | InterceptorChain copyWith({
66 | Request? request,
67 | int? index,
68 | }) =>
69 | InterceptorChain(
70 | request: request ?? this.request,
71 | index: index ?? this.index,
72 | interceptors: interceptors,
73 | );
74 | }
75 |
--------------------------------------------------------------------------------
/chopper/lib/src/chopper_exception.dart:
--------------------------------------------------------------------------------
1 | import 'package:chopper/src/request.dart';
2 | import 'package:chopper/src/response.dart';
3 |
4 | /// {@template ChopperException}
5 | /// An exception thrown when something goes wrong with Chopper.
6 | /// {@endtemplate}
7 | class ChopperException implements Exception {
8 | /// {@macro ChopperException}
9 | ChopperException(this.message, {this.response, this.request});
10 |
11 | /// The response that caused the exception.
12 | final Response? response;
13 |
14 | /// The request that caused the exception.
15 | final Request? request;
16 |
17 | /// The message of the exception.
18 | final String message;
19 |
20 | @override
21 | String toString() {
22 | return 'ChopperException: $message ${response != null ? ', \nResponse: $response' : ''}${request != null ? ', \nRequest: $request' : ''}';
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/chopper/lib/src/chopper_http_exception.dart:
--------------------------------------------------------------------------------
1 | import 'package:chopper/src/response.dart';
2 |
3 | /// {@template ChopperHttpException}
4 | /// An exception thrown when a [Response] is unsuccessful < 200 or > 300.
5 | /// {@endtemplate}
6 | class ChopperHttpException implements Exception {
7 | /// {@macro ChopperHttpException}
8 | ChopperHttpException(this.response);
9 |
10 | /// The response that caused the exception.
11 | final Response response;
12 |
13 | @override
14 | String toString() {
15 | return 'Could not fetch the response for ${response.base.request}. Status code: ${response.statusCode}, error: ${response.error}';
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/chopper/lib/src/chopper_log_record.dart:
--------------------------------------------------------------------------------
1 | import 'package:chopper/src/request.dart';
2 | import 'package:chopper/src/response.dart';
3 | import 'package:meta/meta.dart';
4 |
5 | final class ChopperLogRecord {
6 | const ChopperLogRecord(this.message, {this.request, this.response});
7 |
8 | final String message;
9 | final Request? request;
10 | final Response? response;
11 |
12 | @override
13 | String toString() => message;
14 | }
15 |
16 | ///
17 | /// [ChopperLogRecord] mixin for the purposes of creating mocks
18 | /// using a mocking framework such as Mockito or Mocktail.
19 | ///
20 | /// ```dart
21 | /// base class MockChopperLogRecord extends Mock with MockChopperLogRecordMixin {}
22 | /// ```
23 | ///
24 | @visibleForTesting
25 | base mixin MockChopperLogRecordMixin implements ChopperLogRecord {}
26 |
--------------------------------------------------------------------------------
/chopper/lib/src/constants.dart:
--------------------------------------------------------------------------------
1 | // ignore_for_file: constant_identifier_names
2 |
3 | const String contentTypeKey = 'content-type';
4 | const String jsonHeaders = 'application/json';
5 | const String formEncodedHeaders = 'application/x-www-form-urlencoded';
6 |
7 | // Represent the header for a json api response https://jsonapi.org/#mime-types
8 | const String jsonApiHeaders = 'application/vnd.api+json';
9 |
10 | abstract final class HttpMethod {
11 | static const String Get = 'GET';
12 | static const String Post = 'POST';
13 | static const String Put = 'PUT';
14 | static const String Delete = 'DELETE';
15 | static const String Patch = 'PATCH';
16 | static const String Head = 'HEAD';
17 | static const String Options = 'OPTIONS';
18 | }
19 |
--------------------------------------------------------------------------------
/chopper/lib/src/extensions.dart:
--------------------------------------------------------------------------------
1 | extension StripStringExtension on String {
2 | /// The string without any leading whitespace and optional [character]
3 | String leftStrip([String? character]) {
4 | final String trimmed = trimLeft();
5 |
6 | if (character != null && trimmed.startsWith(character)) {
7 | return trimmed.substring(1);
8 | }
9 |
10 | return trimmed;
11 | }
12 |
13 | /// The string without any trailing whitespace and optional [character]
14 | String rightStrip([String? character]) {
15 | final String trimmed = trimRight();
16 |
17 | if (character != null && trimmed.endsWith(character)) {
18 | return trimmed.substring(0, trimmed.length - 1);
19 | }
20 |
21 | return trimmed;
22 | }
23 |
24 | /// The string without any leading and trailing whitespace and optional [character]
25 | String strip([String? character]) =>
26 | character != null ? leftStrip(character).rightStrip(character) : trim();
27 | }
28 |
29 | extension StatusCodeIntExtension on int {
30 | bool get isSuccessfulStatusCode => this >= 200 && this < 300;
31 | }
32 |
--------------------------------------------------------------------------------
/chopper/lib/src/interceptors/authenticator_interceptor.dart:
--------------------------------------------------------------------------------
1 | import 'dart:async';
2 |
3 | import 'package:chopper/src/authenticator.dart';
4 | import 'package:chopper/src/chain/chain.dart';
5 | import 'package:chopper/src/extensions.dart';
6 | import 'package:chopper/src/interceptors/internal_interceptor.dart';
7 | import 'package:chopper/src/request.dart';
8 | import 'package:chopper/src/response.dart';
9 |
10 | /// {@template AuthenticatorInterceptor}
11 | /// Internal interceptor that handles authentication provided by [authenticator].
12 | /// {@endtemplate}
13 | class AuthenticatorInterceptor implements InternalInterceptor {
14 | /// {@macro AuthenticatorInterceptor}
15 | AuthenticatorInterceptor(this._authenticator);
16 |
17 | /// Authenticator to be used for authentication.
18 | final Authenticator _authenticator;
19 |
20 | @override
21 | FutureOr> intercept(
22 | Chain chain) async {
23 | final originalRequest = chain.request;
24 |
25 | Response response = await chain.proceed(originalRequest);
26 |
27 | final Request? updatedRequest = await _authenticator.authenticate(
28 | originalRequest,
29 | response,
30 | originalRequest,
31 | );
32 |
33 | if (updatedRequest != null) {
34 | response = await chain.proceed(updatedRequest);
35 | if (response.statusCode.isSuccessfulStatusCode) {
36 | await _authenticator.onAuthenticationSuccessful
37 | ?.call(updatedRequest, response, originalRequest);
38 | } else {
39 | await _authenticator.onAuthenticationFailed
40 | ?.call(updatedRequest, response, originalRequest);
41 | }
42 | }
43 |
44 | return response;
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/chopper/lib/src/interceptors/curl_interceptor.dart:
--------------------------------------------------------------------------------
1 | import 'dart:async';
2 |
3 | import 'package:chopper/src/chain/chain.dart';
4 | import 'package:chopper/src/interceptors/interceptor.dart';
5 | import 'package:chopper/src/response.dart';
6 | import 'package:chopper/src/utils.dart';
7 | import 'package:http/http.dart' as http;
8 | import 'package:meta/meta.dart';
9 |
10 | /// A [Interceptor] implementation that prints a curl request equivalent
11 | /// to the network call channeled through it for debugging purposes.
12 | ///
13 | /// Thanks, @edwardaux
14 | @immutable
15 | class CurlInterceptor implements Interceptor {
16 | @override
17 | FutureOr> intercept(
18 | Chain chain) async {
19 | final http.BaseRequest baseRequest = await chain.request.toBaseRequest();
20 | final List curlParts = ['curl -v -X ${baseRequest.method}'];
21 | for (final MapEntry header in baseRequest.headers.entries) {
22 | curlParts.add("-H '${header.key}: ${header.value}'");
23 | }
24 | // this is fairly naive, but it should cover most cases
25 | if (baseRequest is http.Request) {
26 | final String body = baseRequest.body;
27 | if (body.isNotEmpty) {
28 | curlParts.add("-d '${body.replaceAll("'", r"'\''")}'");
29 | }
30 | }
31 | if (baseRequest is http.MultipartRequest) {
32 | for (final MapEntry field in baseRequest.fields.entries) {
33 | curlParts.add("-f '${field.key}: ${field.value}'");
34 | }
35 | for (final http.MultipartFile file in baseRequest.files) {
36 | curlParts.add("-f '${file.field}: ${file.filename ?? ''}'");
37 | }
38 | }
39 | curlParts.add("'${baseRequest.url}'");
40 | chopperLogger.info(curlParts.join(' '));
41 |
42 | return chain.proceed(chain.request);
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/chopper/lib/src/interceptors/headers_interceptor.dart:
--------------------------------------------------------------------------------
1 | import 'dart:async';
2 |
3 | import 'package:chopper/src/chain/chain.dart';
4 | import 'package:chopper/src/interceptors/interceptor.dart';
5 | import 'package:chopper/src/response.dart';
6 | import 'package:chopper/src/utils.dart';
7 | import 'package:meta/meta.dart';
8 |
9 | /// {@template HeadersInterceptor}
10 | /// A [Interceptor] that adds [headers] to every request.
11 | ///
12 | /// Note that this interceptor will overwrite existing headers having the same
13 | /// keys as [headers].
14 | /// {@endtemplate}
15 | @immutable
16 | class HeadersInterceptor implements Interceptor {
17 | final Map headers;
18 |
19 | /// {@macro HeadersInterceptor}
20 | const HeadersInterceptor(this.headers);
21 |
22 | @override
23 | FutureOr> intercept(
24 | Chain chain) async =>
25 | chain.proceed(
26 | applyHeaders(
27 | chain.request,
28 | headers,
29 | ),
30 | );
31 | }
32 |
--------------------------------------------------------------------------------
/chopper/lib/src/interceptors/http_call_interceptor.dart:
--------------------------------------------------------------------------------
1 | import 'dart:async';
2 |
3 | import 'package:chopper/src/chain/chain.dart';
4 | import 'package:chopper/src/chopper_exception.dart';
5 | import 'package:chopper/src/interceptors/internal_interceptor.dart';
6 | import 'package:chopper/src/response.dart';
7 | import 'package:http/http.dart' as http;
8 |
9 | import '../utils.dart';
10 |
11 | /// {@template HttpCallInterceptor}
12 | /// Internal interceptor that handles the actual HTTP calls. HTTP calls are handled by [_httpClient] for http package.
13 | /// {@endtemplate}
14 | class HttpCallInterceptor implements InternalInterceptor {
15 | /// {@macro HttpCallInterceptor}
16 | const HttpCallInterceptor(this._httpClient);
17 |
18 | /// HTTP client to be used for making the actual HTTP calls.
19 | final http.Client _httpClient;
20 |
21 | @override
22 | FutureOr> intercept(
23 | Chain chain) async {
24 | final finalRequest = await chain.request.toBaseRequest();
25 | final streamRes = await _httpClient.send(finalRequest);
26 |
27 | if (isTypeOf>>()) {
28 | return Response(streamRes, (streamRes.stream) as BodyType);
29 | } else if (isTypeOf()) {
30 | final response = await http.Response.fromStream(streamRes);
31 | return Response(response, response.body as BodyType);
32 | } else {
33 | throw ChopperException('Unsupported type', request: chain.request);
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/chopper/lib/src/interceptors/interceptor.dart:
--------------------------------------------------------------------------------
1 | import 'dart:async';
2 |
3 | import 'package:chopper/chopper.dart';
4 | import 'package:meta/meta.dart';
5 |
6 | export 'package:chopper/src/interceptors/curl_interceptor.dart';
7 | export 'package:chopper/src/interceptors/headers_interceptor.dart';
8 | export 'package:chopper/src/interceptors/http_logging_interceptor.dart';
9 |
10 | /// The interface for implementing interceptors.
11 | /// Interceptors are used for intercepting request, responses and preforming operations on them.
12 | ///
13 | /// Interceptor are called in a Chain order.
14 | /// The first interceptor in the chain calls the next interceptor in the chain and so on.
15 | /// The last interceptor in the chain return the response back to the previous interceptor in the chain and so on.
16 | /// This means the request are processed in the order defined by the chain.
17 | /// The responses are process in the reverse order defined by the chain.
18 | ///
19 | /// Chopper has a few built-in interceptors which can be inspected as fully working examples:
20 | /// [HttpLoggingInterceptor], [CurlInterceptor] and [HeaderInterceptor].
21 | ///
22 | /// A short example for adding an authentication token to every request:
23 | ///
24 | /// ```dart
25 | /// class MyRequestInterceptor implements Interceptor {
26 | /// final String token;
27 | ///
28 | /// @override
29 | /// FutureOr> intercept(Chain chain) async {
30 | /// final request = applyHeader(chain.request, 'auth_token', 'Bearer $token');
31 | /// return chain.proceed(request);
32 | /// }
33 | /// }
34 | /// ```
35 | /// A short example for extracting a header value from a response:
36 | ///
37 | /// ```dart
38 | /// class MyResponseInterceptor implements Interceptor {
39 | /// String _token;
40 | ///
41 | /// @override
42 | /// FutureOr> intercept(Chain chain) async {
43 | /// final response = await chain.proceed(chain.request);
44 | ///
45 | /// _token = response.headers['auth_token'];
46 | /// return response;
47 | /// }
48 | /// }
49 | /// ```
50 | ///
51 | /// **While [Interceptor]s *can* modify the body of requests and responses,
52 | /// converting (encoding) the request/response body should be handled by [Converter]s.**
53 | @immutable
54 | abstract interface class Interceptor {
55 | FutureOr> intercept(Chain chain);
56 | }
57 |
--------------------------------------------------------------------------------
/chopper/lib/src/interceptors/internal_interceptor.dart:
--------------------------------------------------------------------------------
1 | import 'package:chopper/src/interceptors/interceptor.dart';
2 |
3 | /// An interface for implementing Internal interceptors only used by Chopper itself.
4 | abstract interface class InternalInterceptor implements Interceptor {}
5 |
--------------------------------------------------------------------------------
/chopper/lib/src/interceptors/request_converter_interceptor.dart:
--------------------------------------------------------------------------------
1 | import 'dart:async';
2 |
3 | import 'package:chopper/src/annotations.dart';
4 | import 'package:chopper/src/chain/chain.dart';
5 | import 'package:chopper/src/converters.dart';
6 | import 'package:chopper/src/interceptors/internal_interceptor.dart';
7 | import 'package:chopper/src/request.dart';
8 | import 'package:chopper/src/response.dart';
9 |
10 | /// {@template RequestConverterInterceptor}
11 | /// Internal interceptor that handles request conversion provided by [_requestConverter] or [_converter].
12 | /// {@endtemplate}
13 | class RequestConverterInterceptor implements InternalInterceptor {
14 | /// {@macro RequestConverterInterceptor}
15 | RequestConverterInterceptor(this._converter, this._requestConverter);
16 |
17 | /// Converter to be used for request conversion.
18 | final Converter? _converter;
19 |
20 | /// Request converter to be used for request conversion.
21 | final ConvertRequest? _requestConverter;
22 |
23 | @override
24 | FutureOr> intercept(
25 | Chain chain) async =>
26 | await chain.proceed(
27 | await _handleRequestConverter(
28 | chain.request,
29 | _requestConverter,
30 | ),
31 | );
32 |
33 | /// Converts the [request] using [_requestConverter] if it is not null, otherwise uses [_converter].
34 | Future _handleRequestConverter(
35 | Request request,
36 | ConvertRequest? requestConverter,
37 | ) async =>
38 | request.body != null || request.parts.isNotEmpty
39 | ? requestConverter != null
40 | ? await requestConverter(request)
41 | : await _encodeRequest(request)
42 | : request;
43 |
44 | /// Encodes the [request] using [_converter] if not null.
45 | Future _encodeRequest(Request request) async =>
46 | _converter?.convertRequest(request) ?? request;
47 | }
48 |
--------------------------------------------------------------------------------
/chopper/lib/src/interceptors/request_stream_interceptor.dart:
--------------------------------------------------------------------------------
1 | import 'dart:async';
2 |
3 | import 'package:chopper/src/chain/chain.dart';
4 | import 'package:chopper/src/interceptors/internal_interceptor.dart';
5 | import 'package:chopper/src/request.dart';
6 | import 'package:chopper/src/response.dart';
7 |
8 | class RequestStreamInterceptor implements InternalInterceptor {
9 | const RequestStreamInterceptor(this.callback);
10 |
11 | final FutureOr Function(Request event) callback;
12 |
13 | @override
14 | FutureOr> intercept(
15 | Chain chain) async {
16 | await callback(chain.request);
17 |
18 | return chain.proceed(chain.request);
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/chopper/lib/src/interceptors/response_converter_interceptor.dart:
--------------------------------------------------------------------------------
1 | import 'dart:async';
2 |
3 | import 'package:chopper/src/annotations.dart';
4 | import 'package:chopper/src/chain/chain.dart';
5 | import 'package:chopper/src/chain/interceptor_chain.dart';
6 | import 'package:chopper/src/converters.dart';
7 | import 'package:chopper/src/extensions.dart';
8 | import 'package:chopper/src/interceptors/internal_interceptor.dart';
9 | import 'package:chopper/src/response.dart';
10 | import 'package:chopper/src/utils.dart';
11 |
12 | /// {@template ResponseConverterInterceptor}
13 | /// Internal interceptor that handles response conversion provided by [_converter], [_responseConverter] or converts error instead with provided [_errorConverter].
14 | /// {@endtemplate}
15 | class ResponseConverterInterceptor implements InternalInterceptor {
16 | /// {@macro ResponseConverterInterceptor}
17 | ResponseConverterInterceptor({
18 | Converter? converter,
19 | ErrorConverter? errorConverter,
20 | FutureOr> Function(Response)? responseConverter,
21 | }) : _responseConverter = responseConverter,
22 | _errorConverter = errorConverter,
23 | _converter = converter;
24 |
25 | /// Converter to be used for response conversion.
26 | final Converter? _converter;
27 |
28 | /// Error converter to be used for error conversion.
29 | final ErrorConverter? _errorConverter;
30 |
31 | /// Response converter to be used for response conversion.
32 | final ConvertResponse? _responseConverter;
33 |
34 | @override
35 | FutureOr> intercept(
36 | Chain chain) async {
37 | final realChain = chain as InterceptorChain;
38 | final typedChain = switch (isTypeOf>>()) {
39 | true => realChain,
40 | false => realChain.copyWith(),
41 | };
42 |
43 | final response = await typedChain.proceed(chain.request);
44 |
45 | return response.statusCode.isSuccessfulStatusCode
46 | ? _handleSuccessResponse(response, _responseConverter)
47 | : _handleErrorResponse(response);
48 | }
49 |
50 | /// Handles the successful response by converting it using [_responseConverter] or [_converter].
51 | Future> _handleSuccessResponse(
52 | Response response,
53 | ConvertResponse? responseConverter,
54 | ) async {
55 | if (responseConverter != null) {
56 | response = await responseConverter(response);
57 | } else if (_converter != null) {
58 | response = await _decodeResponse(response, _converter!);
59 | }
60 |
61 | return Response(response.base, response.body);
62 | }
63 |
64 | /// Converts the [response] using [_converter].
65 | Future> _decodeResponse(
66 | Response response,
67 | Converter withConverter,
68 | ) async =>
69 | await withConverter.convertResponse(response);
70 |
71 | /// Handles the error response by converting it using [_errorConverter].
72 | Future> _handleErrorResponse(
73 | Response response,
74 | ) async {
75 | var error = response.body;
76 | if (_errorConverter != null) {
77 | final errorRes = await _errorConverter?.convertError(
78 | response,
79 | );
80 | error = errorRes?.error ?? errorRes?.body;
81 | }
82 |
83 | return Response(response.base, null, error: error);
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/chopper/lib/src/response.dart:
--------------------------------------------------------------------------------
1 | import 'dart:typed_data';
2 |
3 | import 'package:chopper/src/chopper_http_exception.dart';
4 | import 'package:equatable/equatable.dart' show EquatableMixin;
5 | import 'package:http/http.dart' as http;
6 | import 'package:meta/meta.dart';
7 |
8 | /// {@template response}
9 | /// A [http.BaseResponse] wrapper representing a response of a Chopper network call.
10 | ///
11 | /// ```dart
12 | /// @GET(path: '/something')
13 | /// Future fetchSomething();
14 | /// ```
15 | ///
16 | /// ```dart
17 | /// @GET(path: '/items/{id}')
18 | /// Future> fetchItem();
19 | /// ```
20 | /// {@endtemplate}
21 | @immutable
22 | base class Response with EquatableMixin {
23 | /// The [http.BaseResponse] from `package:http` that this [Response] wraps.
24 | final http.BaseResponse base;
25 |
26 | /// The body of the response after conversion by Chopper
27 | /// See [Converter] for more on body conversion.
28 | ///
29 | /// Can be null if [isSuccessful] is not true.
30 | /// Use [error] to get error body.
31 | final BodyType? body;
32 |
33 | /// The body of the response if [isSuccessful] is false.
34 | final Object? error;
35 |
36 | /// {@macro response}
37 | const Response(this.base, this.body, {this.error});
38 |
39 | /// Makes a copy of this Response, replacing original values with the given ones.
40 | /// This method can also alter the type of the response body.
41 | Response copyWith({
42 | http.BaseResponse? base,
43 | NewBodyType? body,
44 | Object? bodyError,
45 | }) =>
46 | Response(
47 | base ?? this.base,
48 | body ?? (this.body as NewBodyType?),
49 | error: bodyError ?? error,
50 | );
51 |
52 | /// The HTTP status code of the response.
53 | int get statusCode => base.statusCode;
54 |
55 | /// Whether the network call was successful or not.
56 | ///
57 | /// `true` if the result code of the network call is >= 200 && <300
58 | /// If false, [error] will contain the converted error response body.
59 | bool get isSuccessful => statusCode >= 200 && statusCode < 300;
60 |
61 | /// HTTP headers of the response.
62 | Map get headers => base.headers;
63 |
64 | /// Returns the response body as bytes ([Uint8List]) provided the network
65 | /// call was successful, else this will be `null`.
66 | Uint8List get bodyBytes =>
67 | base is http.Response ? (base as http.Response).bodyBytes : Uint8List(0);
68 |
69 | /// Returns the response body as a String provided the network
70 | /// call was successful, else this will be `null`.
71 | String get bodyString =>
72 | base is http.Response ? (base as http.Response).body : '';
73 |
74 | /// Check if the response is an error and if the error is of type [ErrorType] and casts the error to [ErrorType]. Otherwise it returns null.
75 | ErrorType? errorWhereType() {
76 | if (error != null && error is ErrorType) {
77 | return error as ErrorType;
78 | } else {
79 | return null;
80 | }
81 | }
82 |
83 | /// Returns the response body if [Response] [isSuccessful] and [body] is not null.
84 | /// Otherwise it throws an [HttpException] with the response status code and error object.
85 | /// If the error object is an [Exception], it will be thrown instead.
86 | BodyType get bodyOrThrow {
87 | if (isSuccessful && body != null) {
88 | return body!;
89 | } else {
90 | if (error is Exception) {
91 | throw error!;
92 | }
93 | throw ChopperHttpException(this);
94 | }
95 | }
96 |
97 | @override
98 | List get props => [
99 | base,
100 | body,
101 | error,
102 | ];
103 | }
104 |
105 | ///
106 | /// [Response] mixin for the purposes of creating mocks
107 | /// using a mocking framework such as Mockito or Mocktail.
108 | ///
109 | /// ```dart
110 | /// base class MockResponse extends Mock with MockResponseMixin {}
111 | /// ```
112 | ///
113 | @visibleForTesting
114 | base mixin MockResponseMixin implements Response {}
115 |
--------------------------------------------------------------------------------
/chopper/lib/src/utils.dart:
--------------------------------------------------------------------------------
1 | import 'dart:collection';
2 |
3 | import 'package:chopper/src/request.dart';
4 | import 'package:logging/logging.dart';
5 | import 'package:qs_dart/qs_dart.dart' show encode, EncodeOptions, ListFormat;
6 |
7 | /// Creates a new [Request] by copying [request] and adding a header with the
8 | /// provided key [name] and value [value] to the result.
9 | ///
10 | /// If [request] already has a header with the key [name] and [override] is true
11 | /// (default), the existing header value will be replaced with [value] in the resulting [Request].
12 | ///
13 | /// ```dart
14 | /// final newRequest = applyHeader(request, 'Authorization', 'Bearer ');
15 | /// ```
16 | Request applyHeader(
17 | Request request,
18 | String name,
19 | String value, {
20 | bool override = true,
21 | }) =>
22 | applyHeaders(
23 | request,
24 | {name: value},
25 | override: override,
26 | );
27 |
28 | /// Creates a new [Request] by copying [request] and adding the provided [headers]
29 | /// to the result.
30 | ///
31 | /// If [request] already has headers with keys provided in [headers] and [override]
32 | /// is true (default), the conflicting headers will be replaced.
33 | ///
34 | /// ```dart
35 | /// final newRequest = applyHeaders(request, {
36 | /// 'Authorization': 'Bearer ',
37 | /// 'Content-Type': 'application/json',
38 | /// });
39 | /// ```
40 | Request applyHeaders(
41 | Request request,
42 | Map headers, {
43 | bool override = true,
44 | }) {
45 | final LinkedHashMap headersCopy = LinkedHashMap(
46 | equals: (a, b) => a.toLowerCase() == b.toLowerCase(),
47 | hashCode: (e) => e.toLowerCase().hashCode,
48 | );
49 | headersCopy.addAll(request.headers);
50 |
51 | for (final entry in headers.entries) {
52 | if (!override && headersCopy.containsKey(entry.key)) continue;
53 | headersCopy[entry.key] = entry.value;
54 | }
55 |
56 | return request.copyWith(headers: headersCopy);
57 | }
58 |
59 | final chopperLogger = Logger('Chopper');
60 |
61 | /// Creates a valid URI query string from [map].
62 | ///
63 | /// E.g., `{'foo': 'bar', 'ints': [ 1337, 42 ] }` will become 'foo=bar&ints=1337&ints=42'.
64 | String mapToQuery(
65 | Map map, {
66 | ListFormat? listFormat,
67 | @Deprecated('Use listFormat instead') bool? useBrackets,
68 | bool? includeNullQueryVars,
69 | }) {
70 | listFormat ??= useBrackets == true ? ListFormat.brackets : ListFormat.repeat;
71 |
72 | return encode(
73 | map,
74 | EncodeOptions(
75 | listFormat: listFormat,
76 | allowDots: listFormat == ListFormat.repeat,
77 | encodeDotInKeys: listFormat == ListFormat.repeat,
78 | encodeValuesOnly: listFormat == ListFormat.repeat,
79 | skipNulls: includeNullQueryVars != true,
80 | strictNullHandling: false,
81 | serializeDate: (DateTime date) => date.toUtc().toIso8601String(),
82 | ),
83 | );
84 | }
85 |
86 | bool isTypeOf() => _Instance() is _Instance;
87 |
88 | final class _Instance {}
89 |
--------------------------------------------------------------------------------
/chopper/mono_pkg.yaml:
--------------------------------------------------------------------------------
1 | sdk:
2 | - stable
3 |
4 | stages:
5 | - analyze_and_format:
6 | - group:
7 | - format
8 | - analyze: --fatal-infos .
9 | - unit_test:
10 | - test_with_coverage:
11 | - test: -p chrome
12 |
13 | cache:
14 | directories:
15 | - .dart_tool/build
--------------------------------------------------------------------------------
/chopper/pubspec.yaml:
--------------------------------------------------------------------------------
1 | name: chopper
2 | description: Chopper is an http client generator using source_gen, inspired by Retrofit
3 | version: 8.1.0
4 | documentation: https://hadrien-lejard.gitbook.io/chopper
5 | repository: https://github.com/lejard-h/chopper
6 |
7 | environment:
8 | sdk: ^3.0.0
9 |
10 | dependencies:
11 | equatable: ^2.0.5
12 | http: ^1.1.0
13 | logging: ^1.2.0
14 | meta: ^1.9.1
15 | qs_dart: ^1.3.0
16 |
17 | dev_dependencies:
18 | build_runner: ^2.4.9
19 | build_test: ^2.2.2
20 | build_verify: ^3.1.0
21 | collection: ^1.18.0
22 | coverage: ^1.8.0
23 | data_fixture_dart: ^3.0.0
24 | faker: ^2.1.0
25 | http_parser: ^4.0.2
26 | lints: ">=4.0.0 <7.0.0"
27 | test: ^1.25.5
28 | transparent_image: ^2.0.1
29 | chopper_generator: ^8.0.3
30 |
31 | topics:
32 | - api
33 | - client
34 | - http
35 | - rest
36 |
--------------------------------------------------------------------------------
/chopper/pubspec_overrides.yaml:
--------------------------------------------------------------------------------
1 | # Development-only path for chopper_generator
2 | dependency_overrides:
3 | chopper_generator:
4 | path: ../chopper_generator
5 |
--------------------------------------------------------------------------------
/chopper/test/chain/authenticator_interceptor_test.dart:
--------------------------------------------------------------------------------
1 | import 'dart:async';
2 |
3 | import 'package:chopper/chopper.dart';
4 | import 'package:chopper/src/interceptors/authenticator_interceptor.dart';
5 | import 'package:http/http.dart' as http;
6 | import 'package:test/test.dart';
7 |
8 | void main() {
9 | late MockAuthenticator authenticator;
10 | late AuthenticatorInterceptor authenticatorInterceptor;
11 | late MockChain chain;
12 | final request = Request('GET', Uri.parse('bar'), Uri.parse('foo'));
13 |
14 | setUp(() {
15 | chain = MockChain(
16 | request,
17 | () => Response(
18 | http.Response('', 200),
19 | '',
20 | ),
21 | );
22 | authenticator = MockAuthenticator(() => null);
23 | authenticatorInterceptor = AuthenticatorInterceptor(authenticator);
24 | });
25 |
26 | test('Intercepted response is authenticated, chain.proceed called once',
27 | () async {
28 | await authenticatorInterceptor.intercept(chain);
29 |
30 | expect(authenticator.authenticateCalled, 1);
31 | expect(chain.proceedCalled, 1);
32 | });
33 |
34 | test('Intercepted response is not authenticated, chain.proceed called twice',
35 | () async {
36 | authenticator = MockAuthenticator(() => request);
37 | authenticatorInterceptor = AuthenticatorInterceptor(authenticator);
38 |
39 | await authenticatorInterceptor.intercept(chain);
40 |
41 | expect(authenticator.authenticateCalled, 1);
42 | expect(chain.proceedCalled, 2);
43 | });
44 |
45 | test(
46 | 'Intercepted response is not authenticated, authentication is successful',
47 | () async {
48 | authenticator = MockAuthenticator(() => request);
49 | authenticatorInterceptor = AuthenticatorInterceptor(authenticator);
50 |
51 | await authenticatorInterceptor.intercept(chain);
52 |
53 | expect(authenticator.authenticateCalled, 1);
54 | expect(chain.proceedCalled, 2);
55 | expect(authenticator.onAuthenticationSuccessfulCalled, 1);
56 | });
57 |
58 | test('Intercepted response is not authenticated, authentication failed',
59 | () async {
60 | chain = MockChain(
61 | request,
62 | () => Response(
63 | http.Response('', 400),
64 | '',
65 | ),
66 | );
67 | authenticator = MockAuthenticator(() => request);
68 | authenticatorInterceptor = AuthenticatorInterceptor(authenticator);
69 |
70 | await authenticatorInterceptor.intercept(chain);
71 |
72 | expect(authenticator.authenticateCalled, 1);
73 | expect(chain.proceedCalled, 2);
74 | expect(authenticator.onAuthenticationFailedCalled, 1);
75 | });
76 | }
77 |
78 | class MockChain implements Chain {
79 | MockChain(this.request, this.onProceed);
80 |
81 | int proceedCalled = 0;
82 |
83 | final Response Function() onProceed;
84 |
85 | @override
86 | FutureOr> proceed(Request request) async {
87 | proceedCalled++;
88 | return onProceed();
89 | }
90 |
91 | @override
92 | final Request request;
93 | }
94 |
95 | class MockAuthenticator implements Authenticator {
96 | MockAuthenticator(this.onAuthenticate) {
97 | onAuthenticationFailed = (
98 | Request request,
99 | Response response, [
100 | Request? originalRequest,
101 | ]) {
102 | onAuthenticationFailedCalled++;
103 | return;
104 | };
105 |
106 | onAuthenticationSuccessful = (
107 | Request request,
108 | Response response, [
109 | Request? originalRequest,
110 | ]) {
111 | onAuthenticationSuccessfulCalled++;
112 | return;
113 | };
114 | }
115 |
116 | final Request? Function() onAuthenticate;
117 |
118 | int authenticateCalled = 0;
119 | int onAuthenticationFailedCalled = 0;
120 | int onAuthenticationSuccessfulCalled = 0;
121 | @override
122 | AuthenticationCallback? onAuthenticationFailed;
123 |
124 | @override
125 | AuthenticationCallback? onAuthenticationSuccessful;
126 |
127 | @override
128 | FutureOr authenticate(
129 | Request request,
130 | Response response, [
131 | Request? originalRequest,
132 | ]) async {
133 | authenticateCalled++;
134 | return onAuthenticate();
135 | }
136 | }
137 |
--------------------------------------------------------------------------------
/chopper/test/chain/request_converter_interceptor_test.dart:
--------------------------------------------------------------------------------
1 | import 'dart:async';
2 |
3 | import 'package:chopper/src/chain/chain.dart';
4 | import 'package:chopper/src/chain/interceptor_chain.dart';
5 | import 'package:chopper/src/converters.dart';
6 | import 'package:chopper/src/interceptors/interceptor.dart';
7 | import 'package:chopper/src/interceptors/request_converter_interceptor.dart';
8 | import 'package:chopper/src/request.dart';
9 | import 'package:chopper/src/response.dart';
10 | import 'package:http/http.dart' as http;
11 | import 'package:test/test.dart';
12 |
13 | void main() {
14 | late InterceptorChain interceptorChain;
15 |
16 | test('request body is null and parts is empty, is not converted', () async {
17 | final testRequest = Request('GET', Uri.parse('foo'), Uri.parse('bar'));
18 | final converter = RequestConverter();
19 | interceptorChain = InterceptorChain(
20 | interceptors: [
21 | RequestConverterInterceptor(
22 | converter,
23 | null,
24 | ),
25 | RequestInterceptor(onRequest: (request) {
26 | expect(request.body, null);
27 | }),
28 | ],
29 | request: testRequest,
30 | );
31 |
32 | await interceptorChain.proceed(testRequest);
33 |
34 | expect(converter.called, 0);
35 | });
36 |
37 | test(
38 | 'request body is not null and parts is empty, requestConverter is not provided, request is converted by converter',
39 | () async {
40 | final testRequest = Request('GET', Uri.parse('foo'), Uri.parse('bar'),
41 | body: 'not converted');
42 | final converter = RequestConverter();
43 | interceptorChain = InterceptorChain(
44 | interceptors: [
45 | RequestConverterInterceptor(
46 | converter,
47 | null,
48 | ),
49 | RequestInterceptor(onRequest: (request) {
50 | expect(request.body, 'converted');
51 | }),
52 | ],
53 | request: testRequest,
54 | );
55 |
56 | await interceptorChain.proceed(testRequest);
57 |
58 | expect(converter.called, 1);
59 | });
60 |
61 | test(
62 | 'request body is null and parts is not empty, requestConverter is not provided, request is converted by converter',
63 | () async {
64 | final testRequest = Request('GET', Uri.parse('foo'), Uri.parse('bar'),
65 | parts: [PartValue('not converted', 1)]);
66 | final converter = RequestConverter();
67 | interceptorChain = InterceptorChain(
68 | interceptors: [
69 | RequestConverterInterceptor(
70 | converter,
71 | null,
72 | ),
73 | RequestInterceptor(onRequest: (request) {
74 | expect(request.body, 'converted');
75 | }),
76 | ],
77 | request: testRequest,
78 | );
79 |
80 | await interceptorChain.proceed(testRequest);
81 |
82 | expect(converter.called, 1);
83 | });
84 |
85 | test(
86 | 'request body is not null and parts is empty, requestConverter is provided, request is converted by requestConverter',
87 | () async {
88 | final testRequest = Request('GET', Uri.parse('foo'), Uri.parse('bar'),
89 | body: 'not converted');
90 | final converter = RequestConverter();
91 | int called = 0;
92 | interceptorChain = InterceptorChain(
93 | interceptors: [
94 | RequestConverterInterceptor(
95 | converter,
96 | (req) {
97 | called++;
98 | return req.copyWith(body: 'foo');
99 | },
100 | ),
101 | RequestInterceptor(onRequest: (request) {
102 | expect(request.body, 'foo');
103 | }),
104 | ],
105 | request: testRequest,
106 | );
107 |
108 | await interceptorChain.proceed(testRequest);
109 |
110 | expect(called, 1);
111 | expect(converter.called, 0);
112 | });
113 | }
114 |
115 | // ignore mutability warning for test class.
116 | //ignore: must_be_immutable
117 | class RequestConverter implements Converter {
118 | int called = 0;
119 | @override
120 | FutureOr convertRequest(Request request) {
121 | called++;
122 | return request.copyWith(body: 'converted');
123 | }
124 |
125 | @override
126 | FutureOr> convertResponse(
127 | Response response) {
128 | return response as Response;
129 | }
130 | }
131 |
132 | // ignore: must_be_immutable
133 | class RequestInterceptor implements Interceptor {
134 | RequestInterceptor({this.onRequest});
135 |
136 | final void Function(Request)? onRequest;
137 | int called = 0;
138 |
139 | @override
140 | FutureOr> intercept(Chain chain) {
141 | called++;
142 | onRequest?.call(chain.request);
143 | return Response(http.Response('TestResponse', 200, request: chain.request),
144 | 'TestResponse' as BodyType);
145 | }
146 | }
147 |
--------------------------------------------------------------------------------
/chopper/test/chopper_exception_test.dart:
--------------------------------------------------------------------------------
1 | import 'package:chopper/chopper.dart';
2 | import 'package:http/http.dart' as http;
3 | import 'package:test/test.dart';
4 |
5 | void main() {
6 | group('ChopperException', () {
7 | test('toString() with only message', () {
8 | final exception = ChopperException('Test message');
9 | expect(exception.toString(), 'ChopperException: Test message ');
10 | });
11 |
12 | test(
13 | 'toString() with message and response',
14 | () {
15 | final baseResponse = http.Response('Internal server error', 500);
16 | final chopperResponse = Response(baseResponse, 'Error body');
17 | final exception = ChopperException(
18 | 'Test message with response',
19 | response: chopperResponse,
20 | );
21 | expect(
22 | exception.toString(),
23 | // ignore: lines_longer_than_80_chars
24 | 'ChopperException: Test message with response , \nResponse: Response(Instance of \'Response\', Error body, null)',
25 | );
26 | },
27 | testOn: 'vm',
28 | );
29 |
30 | test(
31 | 'toString() with message and response',
32 | () {
33 | final baseResponse = http.Response('Internal server error', 500);
34 | final chopperResponse = Response(baseResponse, 'Error body');
35 | final exception = ChopperException(
36 | 'Test message with response',
37 | response: chopperResponse,
38 | );
39 | expect(
40 | exception.toString(),
41 | // ignore: lines_longer_than_80_chars
42 | 'ChopperException: Test message with response , \nResponse: Response(Instance of \'Response0\', Error body, null)',
43 | );
44 | },
45 | testOn: 'chrome',
46 | );
47 |
48 | test('toString() with message and request', () {
49 | final chopperRequest = Request(
50 | 'GET',
51 | Uri.parse('http://localhost/test'),
52 | Uri.parse('http://baseurl'),
53 | );
54 | final exception = ChopperException(
55 | 'Test message with request',
56 | request: chopperRequest,
57 | );
58 | expect(
59 | exception.toString(),
60 | // ignore: lines_longer_than_80_chars
61 | 'ChopperException: Test message with request , \nRequest: Request(GET, http://localhost/test, http://baseurl, null, {}, {}, false, [], null, null, null)',
62 | );
63 | });
64 |
65 | test(
66 | 'toString() with message, response, and request',
67 | () {
68 | final baseResponse = http.Response('Not found', 404);
69 | final chopperResponse = Response(baseResponse, null);
70 | final chopperRequest = Request(
71 | 'POST',
72 | Uri.parse('http://localhost/another/test'),
73 | Uri.parse('http://baseurl'),
74 | body: {'key': 'value'},
75 | headers: {'foo': 'bar'},
76 | );
77 | final exception = ChopperException(
78 | 'Test message with response and request',
79 | response: chopperResponse,
80 | request: chopperRequest,
81 | );
82 | expect(
83 | exception.toString(),
84 | // ignore: lines_longer_than_80_chars
85 | 'ChopperException: Test message with response and request , \nResponse: Response(Instance of \'Response\', null, null), \nRequest: Request(POST, http://localhost/another/test, http://baseurl, {key: value}, {}, {foo: bar}, false, [], null, null, null)',
86 | );
87 | },
88 | testOn: 'vm',
89 | );
90 |
91 | test(
92 | 'toString() with message, response, and request',
93 | () {
94 | final baseResponse = http.Response('Not found', 404);
95 | final chopperResponse = Response(baseResponse, null);
96 | final chopperRequest = Request(
97 | 'POST',
98 | Uri.parse('http://localhost/another/test'),
99 | Uri.parse('http://baseurl'),
100 | body: {'key': 'value'},
101 | headers: {'foo': 'bar'},
102 | );
103 | final exception = ChopperException(
104 | 'Test message with response and request',
105 | response: chopperResponse,
106 | request: chopperRequest,
107 | );
108 | expect(
109 | exception.toString(),
110 | // ignore: lines_longer_than_80_chars
111 | 'ChopperException: Test message with response and request , \nResponse: Response(Instance of \'Response0\', null, null), \nRequest: Request(POST, http://localhost/another/test, http://baseurl, {key: value}, {}, {foo: bar}, false, [], null, null, null)',
112 | );
113 | },
114 | testOn: 'chrome',
115 | );
116 | });
117 | }
118 |
--------------------------------------------------------------------------------
/chopper/test/chopper_http_exception_test.dart:
--------------------------------------------------------------------------------
1 | import 'package:chopper/src/chopper_http_exception.dart';
2 | import 'package:chopper/src/response.dart';
3 | import 'package:http/http.dart' as http;
4 | import 'package:test/test.dart';
5 |
6 | void main() {
7 | test('ChopperHttpException toString prints available information', () {
8 | final request = http.Request('GET', Uri.parse('http://localhost:8000'));
9 | final base = http.Response('Foobar', 400, request: request);
10 | final response = Response(base, 'Foobar', error: 'FooError');
11 |
12 | final exception = ChopperHttpException(response);
13 |
14 | final result = exception.toString();
15 |
16 | expect(
17 | result,
18 | 'Could not fetch the response for GET http://localhost:8000. Status code: 400, error: FooError',
19 | );
20 | });
21 | }
22 |
--------------------------------------------------------------------------------
/chopper/test/ensure_build_test.dart:
--------------------------------------------------------------------------------
1 | @TestOn('vm')
2 | @Timeout(Duration(seconds: 120))
3 | library;
4 |
5 | import 'package:build_verify/build_verify.dart';
6 | import 'package:test/test.dart';
7 |
8 | void main() {
9 | test(
10 | 'ensure_build',
11 | () async {
12 | await expectBuildClean(
13 | packageRelativeDirectory: 'chopper',
14 | gitDiffPathArguments: [
15 | 'test/test_service.chopper.dart',
16 | 'test/test_service_variable.chopper.dart',
17 | 'test/test_without_response_service.chopper.dart',
18 | 'test/test_service_base_url.chopper.dart',
19 | ],
20 | );
21 | },
22 | );
23 | }
24 |
--------------------------------------------------------------------------------
/chopper/test/extensions_test.dart:
--------------------------------------------------------------------------------
1 | // ignore_for_file: long-method
2 |
3 | import 'package:chopper/src/extensions.dart';
4 | import 'package:test/test.dart';
5 |
6 | void main() {
7 | group('String.leftStrip', () {
8 | test('leftStrip without character any leading whitespace', () {
9 | expect('/foo'.leftStrip(), '/foo');
10 | expect(' /foo'.leftStrip(), '/foo');
11 | expect('/foo '.leftStrip(), '/foo ');
12 | expect(' /foo '.leftStrip(), '/foo ');
13 | });
14 |
15 | test(
16 | 'leftStrip with character removes single leading character and any leading whitespace',
17 | () {
18 | expect('/foo'.leftStrip('/'), 'foo');
19 | expect('//foo'.leftStrip('/'), '/foo');
20 | expect(' /foo'.leftStrip('/'), 'foo');
21 | expect('/foo '.leftStrip('/'), 'foo ');
22 | expect(' /foo '.leftStrip('/'), 'foo ');
23 | },
24 | );
25 | });
26 |
27 | group('String.rightStrip', () {
28 | test('rightStrip without character any trailing whitespace', () {
29 | expect('foo/'.rightStrip(), 'foo/');
30 | expect(' foo/'.rightStrip(), ' foo/');
31 | expect('foo/ '.rightStrip(), 'foo/');
32 | expect(' foo/ '.rightStrip(), ' foo/');
33 | });
34 |
35 | test(
36 | 'rightStrip with character removes single trailing character and any trailing whitespace',
37 | () {
38 | expect('foo/'.rightStrip('/'), 'foo');
39 | expect('foo//'.rightStrip('/'), 'foo/');
40 | expect(' foo/'.rightStrip('/'), ' foo');
41 | expect('foo/ '.rightStrip('/'), 'foo');
42 | expect(' foo/ '.rightStrip('/'), ' foo');
43 | },
44 | );
45 | });
46 |
47 | group('String.strip', () {
48 | test('strip without character any leading and trailing whitespace', () {
49 | expect('/foo/'.strip(), '/foo/');
50 | expect(' /foo/'.strip(), '/foo/');
51 | expect('/foo/ '.strip(), '/foo/');
52 | expect(' /foo/ '.strip(), '/foo/');
53 | });
54 |
55 | test(
56 | 'strip with character removes single leading and trailing character and any leading and trailing whitespace',
57 | () {
58 | expect('/foo/'.strip('/'), 'foo');
59 | expect('//foo//'.strip('/'), '/foo/');
60 | expect(' /foo/'.strip('/'), 'foo');
61 | expect('/foo/ '.strip('/'), 'foo');
62 | expect(' /foo/ '.strip('/'), 'foo');
63 | },
64 | );
65 | });
66 | }
67 |
--------------------------------------------------------------------------------
/chopper/test/fake_authenticator.dart:
--------------------------------------------------------------------------------
1 | import 'dart:async' show FutureOr;
2 |
3 | import 'package:chopper/chopper.dart';
4 |
5 | class FakeAuthenticator extends Authenticator {
6 | Request? capturedRequest;
7 |
8 | Response? capturedResponse;
9 |
10 | Request? capturedOriginalRequest;
11 |
12 | Request? capturedAuthenticateRequest;
13 |
14 | Response? capturedAuthenticateResponse;
15 |
16 | Request? capturedAuthenticateOriginalRequest;
17 |
18 | bool onAuthenticationSuccessfulCalled = false;
19 |
20 | bool onAuthenticationFailedCalled = false;
21 |
22 | @override
23 | FutureOr authenticate(
24 | Request request,
25 | Response response, [
26 | Request? originalRequest,
27 | ]) async {
28 | if (response.statusCode == 401) {
29 | capturedAuthenticateResponse = response;
30 | capturedAuthenticateOriginalRequest = originalRequest;
31 | capturedAuthenticateRequest = request.copyWith(
32 | headers: {
33 | ...request.headers,
34 | 'authorization': 'some_fake_token',
35 | },
36 | );
37 | return capturedAuthenticateRequest;
38 | }
39 |
40 | return null;
41 | }
42 |
43 | @override
44 | AuthenticationCallback? get onAuthenticationSuccessful => (
45 | Request request,
46 | Response response, [
47 | Request? originalRequest,
48 | ]) {
49 | onAuthenticationSuccessfulCalled = true;
50 | capturedRequest = request;
51 | capturedResponse = response;
52 | capturedOriginalRequest = originalRequest;
53 | };
54 |
55 | @override
56 | AuthenticationCallback? get onAuthenticationFailed => (
57 | Request request,
58 | Response response, [
59 | Request? originalRequest,
60 | ]) {
61 | onAuthenticationFailedCalled = true;
62 | capturedRequest = request;
63 | capturedResponse = response;
64 | capturedOriginalRequest = originalRequest;
65 | };
66 | }
67 |
--------------------------------------------------------------------------------
/chopper/test/fixtures/error_fixtures.dart:
--------------------------------------------------------------------------------
1 | class FooErrorType {
2 | const FooErrorType();
3 | }
4 |
--------------------------------------------------------------------------------
/chopper/test/fixtures/example_enum.dart:
--------------------------------------------------------------------------------
1 | enum ExampleEnum {
2 | foo,
3 | bar,
4 | baz;
5 |
6 | @override
7 | String toString() => name;
8 | }
9 |
--------------------------------------------------------------------------------
/chopper/test/fixtures/http_response_fixture.dart:
--------------------------------------------------------------------------------
1 | import 'dart:convert' show jsonEncode;
2 |
3 | import 'package:data_fixture_dart/data_fixture_dart.dart';
4 | import 'package:http/http.dart' as http;
5 | import 'package:meta/meta.dart';
6 |
7 | import '../helpers/http_response_extension.dart';
8 | import 'payload_fixture.dart';
9 |
10 | extension ResponseFixture on http.Response {
11 | static ResponseFactory get factory => ResponseFactory();
12 | }
13 |
14 | @internal
15 | final class ResponseFactory extends FixtureFactory {
16 | @override
17 | FixtureDefinition definition() => define(
18 | (Faker faker, [int index = 0]) => http.Response(
19 | jsonEncode(PayloadFixture.factory.makeSingle().toJson()),
20 | 200,
21 | ),
22 | );
23 |
24 | FixtureRedefinitionBuilder body(String? body) =>
25 | (http.Response response, [int index = 0]) =>
26 | response.copyWith(body: body);
27 |
28 | FixtureRedefinitionBuilder statusCode(int? statusCode) =>
29 | (http.Response response, [int index = 0]) =>
30 | response.copyWith(statusCode: statusCode);
31 | }
32 |
--------------------------------------------------------------------------------
/chopper/test/fixtures/payload_fixture.dart:
--------------------------------------------------------------------------------
1 | import 'package:data_fixture_dart/data_fixture_dart.dart';
2 | import 'package:meta/meta.dart';
3 |
4 | import '../helpers/payload.dart';
5 |
6 | extension PayloadFixture on Payload {
7 | static PayloadFactory get factory => PayloadFactory();
8 | }
9 |
10 | @internal
11 | final class PayloadFactory extends FixtureFactory {
12 | @override
13 | FixtureDefinition definition() => define(
14 | (Faker faker, [int index = 0]) => Payload(
15 | statusCode: 200,
16 | message: faker.lorem.sentence(),
17 | ),
18 | );
19 | }
20 |
--------------------------------------------------------------------------------
/chopper/test/fixtures/request_fixture.dart:
--------------------------------------------------------------------------------
1 | import 'dart:convert' show jsonEncode;
2 |
3 | import 'package:chopper/chopper.dart' show Request;
4 | import 'package:data_fixture_dart/data_fixture_dart.dart';
5 | import 'package:meta/meta.dart';
6 |
7 | extension RequestFixture on Request {
8 | static RequestFixtureFactory get factory => RequestFixtureFactory();
9 | }
10 |
11 | @internal
12 | final class RequestFixtureFactory extends FixtureFactory {
13 | @override
14 | FixtureDefinition definition() {
15 | final String method =
16 | faker.randomGenerator.element(['GET', 'POST', 'PUT', 'DELETE']);
17 |
18 | return define(
19 | (Faker faker, [int index = 0]) => Request(
20 | method,
21 | Uri.parse('/${faker.lorem.word()}'),
22 | Uri.https(faker.internet.domainName()),
23 | headers: faker.randomGenerator.boolean()
24 | ? {'x-${faker.lorem.word()}': faker.lorem.word()}
25 | : {},
26 | parameters: faker.randomGenerator.boolean()
27 | ? {faker.lorem.word(): faker.lorem.word()}
28 | : null,
29 | body:
30 | faker.randomGenerator.boolean() && ['POST', 'PUT'].contains(method)
31 | ? jsonEncode({faker.lorem.word(): faker.lorem.sentences(10)})
32 | : null,
33 | ),
34 | );
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/chopper/test/fixtures/response_fixture.dart:
--------------------------------------------------------------------------------
1 | import 'package:chopper/chopper.dart' show Response;
2 | import 'package:data_fixture_dart/data_fixture_dart.dart';
3 | import 'package:http/http.dart' as http;
4 | import 'package:meta/meta.dart';
5 |
6 | import 'http_response_fixture.dart' as http_fixture;
7 |
8 | extension ResponseFixture on Response {
9 | static ResponseFixtureFactory factory() => ResponseFixtureFactory();
10 | }
11 |
12 | @internal
13 | final class ResponseFixtureFactory extends FixtureFactory> {
14 | @override
15 | FixtureDefinition> definition() {
16 | final http.Response base =
17 | http_fixture.ResponseFixture.factory.makeSingle();
18 |
19 | return define(
20 | (Faker faker, [int index = 0]) => Response(base, null),
21 | );
22 | }
23 |
24 | FixtureRedefinitionBuilder> body(T? body) =>
25 | (Response response, [int index = 0]) => response.copyWith(body: body);
26 |
27 | FixtureRedefinitionBuilder> error(Object? value) =>
28 | (Response response, [int index = 0]) =>
29 | response.copyWith(bodyError: value);
30 | }
31 |
--------------------------------------------------------------------------------
/chopper/test/form_test.dart:
--------------------------------------------------------------------------------
1 | import 'package:chopper/src/base.dart';
2 | import 'package:chopper/src/converters.dart';
3 | import 'package:http/http.dart' as http;
4 | import 'package:http/testing.dart';
5 | import 'package:test/test.dart';
6 |
7 | import 'test_service.dart';
8 |
9 | void main() {
10 | group('Form', () {
11 | ChopperClient buildClient(http.Client httpClient, {bool isJson = false}) =>
12 | ChopperClient(
13 | services: [
14 | // the generated service
15 | HttpTestService.create(),
16 | ],
17 | client: httpClient,
18 | converter: isJson ? JsonConverter() : null,
19 | );
20 |
21 | test('form-urlencoded default if no converter', () async {
22 | final httpClient = MockClient((http.Request req) async {
23 | expect(req.url.toString(), equals('/test/map'));
24 | expect(
25 | req.headers['content-type'],
26 | 'application/x-www-form-urlencoded; charset=utf-8',
27 | );
28 | expect(req.body, 'foo=test&default=hello');
29 |
30 | return http.Response('ok', 200);
31 | });
32 |
33 | final chopper = buildClient(httpClient);
34 |
35 | final result = await chopper.getService().mapTest({
36 | 'foo': 'test',
37 | 'default': 'hello',
38 | });
39 |
40 | expect(result.body, equals('ok'));
41 |
42 | httpClient.close();
43 | });
44 |
45 | test('form-urlencoded factory converter', () async {
46 | final httpClient = MockClient((http.Request req) async {
47 | expect(
48 | req.headers['content-type'],
49 | 'application/x-www-form-urlencoded; charset=utf-8',
50 | );
51 | expect(req.body, 'foo=test&factory=converter');
52 |
53 | return http.Response('ok', 200);
54 | });
55 |
56 | final chopper = buildClient(httpClient, isJson: true);
57 |
58 | final result = await chopper.getService().postForm({
59 | 'foo': 'test',
60 | 'factory': 'converter',
61 | });
62 |
63 | expect(result.body, equals('ok'));
64 |
65 | httpClient.close();
66 | });
67 |
68 | test('form-urlencoded using headers field of annotation', () async {
69 | final httpClient = MockClient((http.Request req) async {
70 | expect(
71 | req.headers['content-type'],
72 | 'application/x-www-form-urlencoded; charset=utf-8',
73 | );
74 | expect(req.body, 'foo=test&factory=converter');
75 |
76 | return http.Response('ok', 200);
77 | });
78 |
79 | final chopper = buildClient(httpClient, isJson: true);
80 |
81 | final result =
82 | await chopper.getService().postFormUsingHeaders({
83 | 'foo': 'test',
84 | 'factory': 'converter',
85 | });
86 |
87 | expect(result.body, equals('ok'));
88 |
89 | httpClient.close();
90 | });
91 |
92 | test('form-urlencoded with @Field()', () async {
93 | final httpClient = MockClient((http.Request req) async {
94 | expect(
95 | req.headers['content-type'],
96 | 'application/x-www-form-urlencoded; charset=utf-8',
97 | );
98 | expect(req.body, 'foo=test&bar=42');
99 |
100 | return http.Response('ok', 200);
101 | });
102 |
103 | final chopper = buildClient(httpClient);
104 |
105 | final result = await chopper
106 | .getService()
107 | .postFormFields('test', 42);
108 |
109 | expect(result.body, equals('ok'));
110 |
111 | httpClient.close();
112 | });
113 | });
114 | }
115 |
--------------------------------------------------------------------------------
/chopper/test/helpers/fake_chain.dart:
--------------------------------------------------------------------------------
1 | import 'dart:async';
2 |
3 | import 'package:chopper/src/chain/chain.dart';
4 | import 'package:chopper/src/request.dart';
5 | import 'package:chopper/src/response.dart';
6 | import 'package:http/http.dart' as http;
7 |
8 | /// A fake implementation of [Chain] for testing purposes.
9 | class FakeChain implements Chain {
10 | FakeChain(
11 | this.request, {
12 | this.response,
13 | this.exception,
14 | }) : assert(
15 | response == null || exception == null,
16 | 'Either response or exception must be provided, not both.',
17 | );
18 |
19 | @override
20 | final Request request;
21 |
22 | /// The fake response to be returned by the chain.
23 | final Response? response;
24 |
25 | /// The fake exception to be returned by the chain.
26 | final Exception? exception;
27 |
28 | @override
29 | FutureOr> proceed(Request request) {
30 | if (exception != null) {
31 | throw exception!;
32 | }
33 |
34 | if (response != null) {
35 | return response!;
36 | }
37 |
38 | return Response(
39 | http.Response('TestChain', 200),
40 | 'TestChain' as BodyType,
41 | );
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/chopper/test/helpers/http_response_extension.dart:
--------------------------------------------------------------------------------
1 | import 'package:http/http.dart' as http;
2 |
3 | extension HttpResponseExtension on http.Response {
4 | http.Response copyWith({
5 | String? body,
6 | int? statusCode,
7 | Map? headers,
8 | bool? isRedirect,
9 | bool? persistentConnection,
10 | String? reasonPhrase,
11 | }) =>
12 | http.Response(
13 | body ?? this.body,
14 | statusCode ?? this.statusCode,
15 | request: request,
16 | headers: headers ?? this.headers,
17 | reasonPhrase: reasonPhrase ?? this.reasonPhrase,
18 | isRedirect: isRedirect ?? this.isRedirect,
19 | persistentConnection: persistentConnection ?? this.persistentConnection,
20 | );
21 | }
22 |
--------------------------------------------------------------------------------
/chopper/test/helpers/payload.dart:
--------------------------------------------------------------------------------
1 | import 'package:equatable/equatable.dart';
2 |
3 | final class Payload with EquatableMixin {
4 | const Payload({
5 | this.statusCode = 200,
6 | this.message = 'OK',
7 | });
8 |
9 | final int statusCode;
10 | final String message;
11 |
12 | factory Payload.fromJson(Map json) => Payload(
13 | statusCode: json['statusCode'] as int? ?? 200,
14 | message: json['message'] as String? ?? 'OK',
15 | );
16 |
17 | Map toJson() => {
18 | 'statusCode': statusCode,
19 | 'message': message,
20 | };
21 |
22 | @override
23 | List get props => [
24 | statusCode,
25 | message,
26 | ];
27 | }
28 |
--------------------------------------------------------------------------------
/chopper/test/http_call_interceptor_test.dart:
--------------------------------------------------------------------------------
1 | import 'dart:async';
2 |
3 | import 'package:chopper/src/chopper_exception.dart';
4 | import 'package:chopper/src/interceptors/http_call_interceptor.dart';
5 | import 'package:chopper/src/request.dart';
6 | import 'package:test/test.dart';
7 | import 'package:http/http.dart' as http;
8 |
9 | import 'helpers/fake_chain.dart';
10 |
11 | void main() {
12 | group('HttpCallInterceptor', () {
13 | late http.Client mockClient;
14 | late HttpCallInterceptor interceptor;
15 |
16 | setUp(() {
17 | mockClient = MockHttpClient();
18 | interceptor = HttpCallInterceptor(mockClient);
19 | });
20 |
21 | test('throws ChopperException for unsupported response type', () async {
22 | final request = Request(
23 | 'GET',
24 | Uri.parse('/test'),
25 | Uri.parse('https://example.com'),
26 | );
27 |
28 | final chain = UnsupportedTypeChain(request);
29 |
30 | expect(
31 | () => interceptor.intercept(chain),
32 | throwsA(isA()
33 | .having((e) => e.message, 'message', 'Unsupported type')
34 | .having((e) => e.request, 'request', equals(request))),
35 | );
36 | });
37 | });
38 | }
39 |
40 | // A custom Chain implementation that forces the HttpCallInterceptor to handle
41 | // an unsupported response body type (int)
42 | class UnsupportedTypeChain extends FakeChain {
43 | UnsupportedTypeChain(super.request);
44 | }
45 |
46 | // Mock HTTP client that doesn't need to return anything for this test
47 | class MockHttpClient extends http.BaseClient {
48 | @override
49 | Future send(http.BaseRequest request) async {
50 | // We won't reach this code because the exception is thrown before the actual HTTP call
51 | return http.StreamedResponse(
52 | Stream.empty(),
53 | 200,
54 | );
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/chopper/test/interceptors_test.dart:
--------------------------------------------------------------------------------
1 | import 'dart:async';
2 |
3 | import 'package:chopper/src/base.dart';
4 | import 'package:chopper/src/chain/chain.dart';
5 | import 'package:chopper/src/interceptors/interceptor.dart';
6 | import 'package:chopper/src/request.dart';
7 | import 'package:chopper/src/response.dart';
8 | import 'package:chopper/src/utils.dart';
9 | import 'package:http/http.dart' as http;
10 | import 'package:http/testing.dart';
11 | import 'package:test/test.dart';
12 |
13 | import 'helpers/fake_chain.dart';
14 | import 'test_service.dart';
15 |
16 | void main() {
17 | group('Interceptors', () {
18 | final requestClient = MockClient(
19 | (request) async {
20 | expect(
21 | request.url.toString(),
22 | equals('/test/get/1234/intercept'),
23 | );
24 |
25 | return http.Response('', 200);
26 | },
27 | );
28 |
29 | final responseClient = MockClient(
30 | (request) async => http.Response('body', 200),
31 | );
32 |
33 | tearDown(() {
34 | requestClient.close();
35 | responseClient.close();
36 | });
37 |
38 | test('RequestInterceptor', () async {
39 | final chopper = ChopperClient(
40 | interceptors: [RequestIntercept()],
41 | services: [
42 | HttpTestService.create(),
43 | ],
44 | client: requestClient,
45 | );
46 |
47 | await chopper.getService().getTest(
48 | '1234',
49 | dynamicHeader: '',
50 | );
51 | });
52 |
53 | test('ResponseInterceptor', () async {
54 | final chopper = ChopperClient(
55 | interceptors: [ResponseIntercept()],
56 | services: [
57 | HttpTestService.create(),
58 | ],
59 | client: responseClient,
60 | );
61 |
62 | await chopper.getService().getTest(
63 | '1234',
64 | dynamicHeader: '',
65 | );
66 |
67 | expect(ResponseIntercept.intercepted, isA<_Intercepted>());
68 | });
69 |
70 | test('headers', () async {
71 | final client = MockClient((http.Request req) async {
72 | expect(req.headers.containsKey('foo'), isTrue);
73 | expect(req.headers['foo'], equals('bar'));
74 |
75 | return http.Response('', 200);
76 | });
77 |
78 | final chopper = ChopperClient(
79 | interceptors: [
80 | HeadersInterceptor({'foo': 'bar'}),
81 | ],
82 | services: [
83 | HttpTestService.create(),
84 | ],
85 | client: client,
86 | );
87 |
88 | await chopper.getService().getTest(
89 | '1234',
90 | dynamicHeader: '',
91 | );
92 | });
93 |
94 | test('Curl interceptors', () async {
95 | final fakeRequest = Request(
96 | 'POST',
97 | Uri.parse('/'),
98 | Uri.parse('base'),
99 | body: 'test',
100 | headers: {'foo': 'bar'},
101 | );
102 |
103 | final curl = CurlInterceptor();
104 | var log = '';
105 | chopperLogger.onRecord.listen((r) => log = r.message);
106 | await curl.intercept(FakeChain(fakeRequest));
107 |
108 | expect(
109 | log,
110 | equals(
111 | r"curl -v -X POST -H 'foo: bar' -H 'content-type: text/plain; charset=utf-8' -d 'test' 'base/'",
112 | ),
113 | );
114 | });
115 |
116 | test('Curl interceptor with escaped text', () async {
117 | final fakeRequest = Request(
118 | 'POST',
119 | Uri.parse('/'),
120 | Uri.parse('base'),
121 | body: r"""Lorem's ipsum "dolor" sit amet""",
122 | );
123 |
124 | final curl = CurlInterceptor();
125 | var log = '';
126 | chopperLogger.onRecord.listen((r) => log = r.message);
127 | await curl.intercept(FakeChain(fakeRequest));
128 |
129 | expect(
130 | log,
131 | equals(
132 | r"""curl -v -X POST -H 'content-type: text/plain; charset=utf-8' -d 'Lorem'\''s ipsum "dolor" sit amet' 'base/'""",
133 | ),
134 | );
135 | });
136 |
137 | test('Curl interceptors Multipart', () async {
138 | final fakeRequestMultipart = Request(
139 | 'POST',
140 | Uri.parse('/'),
141 | Uri.parse('base'),
142 | headers: {'foo': 'bar'},
143 | parts: [
144 | PartValue('p1', 123),
145 | PartValueFile(
146 | 'p2',
147 | http.MultipartFile.fromBytes('file', [0], filename: 'filename'),
148 | ),
149 | ],
150 | multipart: true,
151 | );
152 |
153 | final curl = CurlInterceptor();
154 | var log = '';
155 | chopperLogger.onRecord.listen((r) => log = r.message);
156 | await curl.intercept(FakeChain(fakeRequestMultipart));
157 |
158 | expect(
159 | log,
160 | equals(
161 | r"curl -v -X POST -H 'foo: bar' -f 'p1: 123' -f 'file: filename' 'base/'",
162 | ),
163 | );
164 | });
165 | });
166 | }
167 |
168 | class ResponseIntercept implements Interceptor {
169 | static dynamic intercepted;
170 |
171 | @override
172 | FutureOr> intercept(
173 | Chain chain) async {
174 | final response = await chain.proceed(chain.request);
175 |
176 | intercepted = _Intercepted(response.body);
177 |
178 | return response;
179 | }
180 | }
181 |
182 | class RequestIntercept implements Interceptor {
183 | @override
184 | FutureOr> intercept(
185 | Chain chain) async {
186 | final request = chain.request;
187 | return chain.proceed(
188 | request.copyWith(
189 | uri: request.uri.replace(path: '${request.uri}/intercept'),
190 | ),
191 | );
192 | }
193 | }
194 |
195 | class _Intercepted {
196 | final BodyType body;
197 |
198 | _Intercepted(this.body);
199 | }
200 |
--------------------------------------------------------------------------------
/chopper/test/json_test.dart:
--------------------------------------------------------------------------------
1 | import 'dart:convert';
2 |
3 | import 'package:chopper/src/base.dart';
4 | import 'package:chopper/src/converters.dart';
5 | import 'package:http/http.dart' as http;
6 | import 'package:http/testing.dart';
7 | import 'package:test/test.dart';
8 |
9 | import 'test_service.dart';
10 |
11 | void main() {
12 | final sample = {
13 | 'foo': 'bar',
14 | };
15 |
16 | final res = {
17 | 'result': 'ok',
18 | };
19 | group('JSON', () {
20 | ChopperClient buildClient(bool json, http.Client httpClient) =>
21 | ChopperClient(
22 | services: [
23 | // the generated service
24 | HttpTestService.create(),
25 | ],
26 | client: httpClient,
27 | converter:
28 | json ? JsonConverter() as Converter : FormUrlEncodedConverter(),
29 | );
30 |
31 | test('default json', () async {
32 | final httpClient = MockClient((http.Request req) async {
33 | expect(req.url.toString(), equals('/test/map'));
34 | expect(req.headers['content-type'], 'application/json; charset=utf-8');
35 | expect(req.body, equals(json.encode(sample)));
36 |
37 | return http.Response(
38 | json.encode(res),
39 | 200,
40 | headers: {'content-type': 'application/json; charset=utf-8'},
41 | );
42 | });
43 |
44 | final chopper = buildClient(
45 | true,
46 | httpClient,
47 | );
48 |
49 | final result =
50 | await chopper.getService().mapTest(sample);
51 |
52 | expect(result.body, equals(res));
53 |
54 | httpClient.close();
55 | });
56 |
57 | test('force json', () async {
58 | final httpClient = MockClient((http.Request req) async {
59 | expect(req.url.toString(), equals('/test/map/json'));
60 | expect(req.headers['content-type'], 'application/json; charset=utf-8');
61 | expect(req.headers['customConverter'], 'true');
62 | expect(req.body, equals(json.encode(sample)));
63 |
64 | return http.Response(
65 | json.encode(res),
66 | 200,
67 | headers: {'content-type': 'application/json; charset=utf-8'},
68 | );
69 | });
70 |
71 | final chopper = buildClient(
72 | false,
73 | httpClient,
74 | );
75 |
76 | final result =
77 | await chopper.getService().forceJsonTest(sample);
78 |
79 | expect(result.body, equals(res));
80 |
81 | httpClient.close();
82 | });
83 | });
84 | }
85 |
--------------------------------------------------------------------------------
/chopper/test/request_stream_interceptor_test.dart:
--------------------------------------------------------------------------------
1 | import 'dart:async';
2 |
3 | import 'package:chopper/chopper.dart';
4 | import 'package:http/http.dart' as http;
5 | import 'package:chopper/src/interceptors/request_stream_interceptor.dart';
6 | import 'package:test/test.dart';
7 |
8 | void main() {
9 | group('RequestStreamInterceptor', () {
10 | late List recordedRequests;
11 | late RequestStreamInterceptor interceptor;
12 |
13 | setUp(() {
14 | recordedRequests = [];
15 | interceptor = RequestStreamInterceptor((request) {
16 | recordedRequests.add(request);
17 | });
18 | });
19 |
20 | test('calls the callback with the request', () async {
21 | final request = Request(
22 | 'POST',
23 | Uri.parse('/resource'),
24 | Uri.parse('https://api.example.com'),
25 | body: 'test body',
26 | );
27 |
28 | final chain = CustomFakeChain(request);
29 |
30 | await interceptor.intercept(chain);
31 |
32 | // Verify the callback was called with the request
33 | expect(recordedRequests, hasLength(1));
34 | expect(recordedRequests.first, equals(request));
35 |
36 | // Verify the request was passed through to the chain
37 | expect(chain.processedRequest, equals(request));
38 | });
39 |
40 | test('handles requests with null body', () async {
41 | final request = Request(
42 | 'GET',
43 | Uri.parse('/resource'),
44 | Uri.parse('https://api.example.com'),
45 | );
46 |
47 | final chain = CustomFakeChain(request);
48 |
49 | await interceptor.intercept(chain);
50 |
51 | // Verify the callback was called with the request
52 | expect(recordedRequests, hasLength(1));
53 | expect(recordedRequests.first, equals(request));
54 | });
55 |
56 | test(
57 | 'handles requests with stream body (passes through)',
58 | () async {
59 | final streamController = StreamController();
60 | final completer = Completer();
61 |
62 | // Create a request with a stream body
63 | final request = Request(
64 | 'POST',
65 | Uri.parse('/resource'),
66 | Uri.parse('https://api.example.com'),
67 | body: streamController.stream,
68 | );
69 |
70 | final chain = CustomFakeChain(request);
71 |
72 | // Add data to the stream and close it immediately to avoid hanging
73 | streamController.add('test data');
74 | streamController.close();
75 |
76 | // Properly handle the FutureOr return type
77 | final result = interceptor.intercept(chain);
78 | if (result is Future) {
79 | await result;
80 | }
81 | completer.complete();
82 |
83 | // Wait for the interceptor to complete
84 | await completer.future;
85 |
86 | // Verify the callback was called with the request
87 | expect(recordedRequests, hasLength(1));
88 | expect(recordedRequests.first, equals(request));
89 |
90 | // The stream should be the same instance, but we can't directly compare streams
91 | // Instead verify it's a Stream instance
92 | expect(chain.processedRequest?.body, isA>());
93 | },
94 | timeout: const Timeout(Duration(seconds: 5)),
95 | );
96 | });
97 | }
98 |
99 | /// Custom implementation of FakeChain to track processed requests
100 | class CustomFakeChain implements Chain {
101 | CustomFakeChain(this.request, {this.response});
102 |
103 | @override
104 | final Request request;
105 |
106 | final Response? response;
107 |
108 | Request? processedRequest;
109 |
110 | @override
111 | Future> proceed(Request request) async {
112 | processedRequest = request;
113 | return Response(
114 | http.Response('', 200),
115 | null as T,
116 | );
117 | }
118 | }
119 |
--------------------------------------------------------------------------------
/chopper/test/test_service_base_url.dart:
--------------------------------------------------------------------------------
1 | import 'dart:async';
2 | import 'dart:convert';
3 |
4 | import 'package:chopper/chopper.dart';
5 |
6 | part 'test_service_base_url.chopper.dart';
7 |
8 | @ChopperApi(baseUrl: 'https://localhost:4000/test')
9 | abstract class HttpTestServiceBaseUrl extends ChopperService {
10 | static HttpTestServiceBaseUrl create([ChopperClient? client]) =>
11 | _$HttpTestServiceBaseUrl(client);
12 |
13 | @GET(path: '')
14 | Future getAll();
15 |
16 | @GET(path: '/')
17 | Future getAllWithTrailingSlash();
18 |
19 | @GET(path: '/list/string')
20 | Future>> listString();
21 |
22 | @GET(path: '/query_param_include_null_query_vars', includeNullQueryVars: true)
23 | Future> getUsingQueryParamIncludeNullQueryVars({
24 | @Query('foo') String? foo,
25 | @Query('bar') String? bar,
26 | @Query('baz') String? baz,
27 | });
28 |
29 | @GET(path: '/list_query_param')
30 | Future> getUsingListQueryParam(
31 | @Query('value') List value,
32 | );
33 |
34 | @GET(path: '/list_query_param_with_brackets_legacy', useBrackets: true)
35 | Future> getUsingListQueryParamWithBracketsLegacy(
36 | @Query('value') List value,
37 | );
38 |
39 | @GET(path: '/list_query_param_with_brackets', listFormat: ListFormat.brackets)
40 | Future> getUsingListQueryParamWithBrackets(
41 | @Query('value') List value,
42 | );
43 |
44 | @GET(path: '/list_query_param_with_indices', listFormat: ListFormat.indices)
45 | Future> getUsingListQueryParamWithIndices(
46 | @Query('value') List value,
47 | );
48 |
49 | @GET(path: '/list_query_param_with_repeat', listFormat: ListFormat.repeat)
50 | Future> getUsingListQueryParamWithRepeat(
51 | @Query('value') List value,
52 | );
53 |
54 | @GET(path: '/list_query_param_with_comma', listFormat: ListFormat.comma)
55 | Future> getUsingListQueryParamWithComma(
56 | @Query('value') List value,
57 | );
58 |
59 | @GET(path: '/map_query_param')
60 | Future> getUsingMapQueryParam(
61 | @Query('value') Map value,
62 | );
63 |
64 | @GET(
65 | path: '/map_query_param_include_null_query_vars',
66 | includeNullQueryVars: true,
67 | )
68 | Future> getUsingMapQueryParamIncludeNullQueryVars(
69 | @Query('value') Map value,
70 | );
71 |
72 | @GET(path: '/map_query_param_with_brackets_legacy', useBrackets: true)
73 | Future> getUsingMapQueryParamWithBracketsLegacy(
74 | @Query('value') Map value,
75 | );
76 |
77 | @GET(path: '/map_query_param_with_brackets', listFormat: ListFormat.brackets)
78 | Future> getUsingMapQueryParamWithBrackets(
79 | @Query('value') Map value,
80 | );
81 |
82 | @GET(path: '/map_query_param_with_indices', listFormat: ListFormat.indices)
83 | Future