├── .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 | [![All Contributors](https://img.shields.io/badge/all_contributors-7-orange.svg?style=flat-square)](#contributors-) 5 | 6 | [![pub package](https://img.shields.io/pub/v/chopper.svg)](https://pub.dartlang.org/packages/chopper) 7 | [![Dart CI](https://github.com/lejard-h/chopper/workflows/Dart%20CI/badge.svg)](https://github.com/lejard-h/chopper/actions?query=workflow%3A%22Dart+CI%22) 8 | [![codecov](https://codecov.io/gh/lejard-h/chopper/branch/master/graph/badge.svg)](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 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 |

Hadrien Lejard

💻 👀 ⚠️ 📖

István Juhos

💻 👀 ⚠️ 📖

Klemen Tusar

💻 👀 ⚠️ 📖

Ivan Terekhin

💻 👀 ⚠️ 📖

Job Guldemeester

💻 👀 ⚠️ 📖

Eugeny Sampir

💻

Uladzimir Paliukhovich

💻
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 | [![pub package](https://img.shields.io/pub/v/chopper.svg)](https://pub.dartlang.org/packages/chopper) 4 | [![Dart CI](https://github.com/lejard-h/chopper/workflows/Dart%20CI/badge.svg)](https://github.com/lejard-h/chopper/actions?query=workflow%3A%22Dart+CI%22) 5 | [![codecov](https://codecov.io/gh/lejard-h/chopper/branch/master/graph/badge.svg)](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* ![pub package](https://img.shields.io/pub/v/chopper.svg) 36 | * *chopper_generator* ![pub package](https://img.shields.io/pub/v/chopper_generator.svg) 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> getUsingMapQueryParamWithIndices( 84 | @Query('value') Map value, 85 | ); 86 | 87 | @GET(path: '/map_query_param_with_repeat', listFormat: ListFormat.repeat) 88 | Future> getUsingMapQueryParamWithRepeat( 89 | @Query('value') Map value, 90 | ); 91 | 92 | @GET(path: '/map_query_param_with_comma', listFormat: ListFormat.comma) 93 | Future> getUsingMapQueryParamWithComa( 94 | @Query('value') Map value, 95 | ); 96 | } 97 | 98 | Request customConvertRequest(Request req) { 99 | final r = JsonConverter().convertRequest(req); 100 | 101 | return applyHeader(r, 'customConverter', 'true'); 102 | } 103 | 104 | Response customConvertResponse(Response res) => 105 | res.copyWith(body: json.decode(res.body)); 106 | 107 | Request convertForm(Request req) { 108 | req = applyHeader(req, contentTypeKey, formEncodedHeaders); 109 | 110 | if (req.body is Map) { 111 | final body = {}; 112 | 113 | req.body.forEach((key, val) { 114 | if (val != null) { 115 | body[key.toString()] = val.toString(); 116 | } 117 | }); 118 | 119 | req = req.copyWith(body: body); 120 | } 121 | 122 | return req; 123 | } 124 | -------------------------------------------------------------------------------- /chopper_built_value/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 3.0.3 4 | 5 | - Update dependencies 6 | 7 | ## 3.0.2 8 | 9 | - Remove unnecessary library name 10 | 11 | ## 3.0.1 12 | 13 | - Update dependencies and linters ([#615](https://github.com/lejard-h/chopper/pull/615)) 14 | 15 | ## 3.0.0 16 | 17 | - Require Chopper ^8.0.0 18 | 19 | ## 2.0.1+2 20 | 21 | - Fix Github release workflow permissions ([#512](https://github.com/lejard-h/chopper/pull/512)) 22 | 23 | ## 2.0.1+1 24 | 25 | - Fix pub.dev topic in package metadata ([#498](https://github.com/lejard-h/chopper/pull/498)) 26 | 27 | ## 2.0.1 28 | 29 | - Add pub.dev topics to package metadata ([#495](https://github.com/lejard-h/chopper/pull/495)) 30 | 31 | ## 2.0.0 32 | 33 | - Require Dart 3.0 or later 34 | 35 | ## 1.2.2 36 | 37 | - Update http constraint to ">=0.13.0 <2.0.0" ([#431](https://github.com/lejard-h/chopper/pull/431)) 38 | 39 | ## 1.2.1 40 | 41 | - Packages upgrade, constraints upgrade 42 | 43 | ## 1.2.0 44 | 45 | - Chopper upgraded 46 | 47 | ## 1.1.0 48 | 49 | - Chopper upgraded 50 | 51 | ## 1.0.0 52 | 53 | - Null safety support 54 | 55 | ## 0.0.3 56 | 57 | - Packages upgrade 58 | 59 | ## 0.0.2 60 | 61 | - Maintenance release 62 | 63 | ## 0.0.1 64 | 65 | - Initial version, created by Stagehand 66 | -------------------------------------------------------------------------------- /chopper_built_value/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 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_built_value/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_built_value/README.md: -------------------------------------------------------------------------------- 1 | This package provides a Converter based on built_value that can be used with [Chopper](https://github.com/lejard-h/chopper) to convert objects to JSON and vice versa. 2 | -------------------------------------------------------------------------------- /chopper_built_value/analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:lints/recommended.yaml 2 | 3 | analyzer: 4 | exclude: 5 | - "**.g.dart" 6 | - "**.mocks.dart" 7 | - "example/**" 8 | 9 | linter: 10 | rules: 11 | avoid_print: true 12 | prefer_single_quotes: true 13 | -------------------------------------------------------------------------------- /chopper_built_value/build.yaml: -------------------------------------------------------------------------------- 1 | targets: 2 | $default: 3 | sources: 4 | include: ["test/data.dart", "test/serializers.dart"] -------------------------------------------------------------------------------- /chopper_built_value/lib/chopper_built_value.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:built_collection/built_collection.dart'; 4 | import 'package:built_value/serializer.dart'; 5 | import 'package:chopper/chopper.dart'; 6 | 7 | /// A custom [Converter] and [ErrorConverter] that handles conversion for classes 8 | /// having a serializer implementation made with the built_value package. 9 | class BuiltValueConverter implements Converter, ErrorConverter { 10 | final Serializers serializers; 11 | static const JsonConverter jsonConverter = JsonConverter(); 12 | final Type? errorType; 13 | 14 | /// Builds a new BuiltValueConverter instance that uses built_value serializers defined 15 | /// in the provided [serializers] parameter. 16 | /// 17 | /// If the error body cannot be converted with serializers and [errorType] is provided 18 | /// and it's not `null`, BuiltValueConverter will try to deserialize the error body into 19 | /// [errorType]. 20 | const BuiltValueConverter(this.serializers, {this.errorType}); 21 | 22 | T? _deserialize(dynamic value) { 23 | dynamic serializer; 24 | if (value is Map && value.containsKey('\$')) { 25 | serializer = serializers.serializerForWireName(value['\$']); 26 | } 27 | serializer ??= serializers.serializerForType(T); 28 | 29 | if (serializer == null) { 30 | throw 'Serializer not found for $T'; 31 | } 32 | 33 | return serializers.deserializeWith(serializer, value); 34 | } 35 | 36 | BuiltList _deserializeListOf(Iterable value) { 37 | final Iterable deserialized = 38 | value.map((value) => _deserialize(value)); 39 | 40 | return BuiltList(deserialized.toList(growable: false)); 41 | } 42 | 43 | BodyType? deserialize(dynamic entity) { 44 | if (entity is BodyType) return entity; 45 | if (entity is Iterable) { 46 | return _deserializeListOf(entity) as BodyType; 47 | } 48 | 49 | return _deserialize(entity); 50 | } 51 | 52 | @override 53 | Request convertRequest(Request request) => jsonConverter.convertRequest( 54 | request.copyWith(body: serializers.serialize(request.body)), 55 | ); 56 | 57 | @override 58 | FutureOr> convertResponse( 59 | Response response, 60 | ) async { 61 | final Response jsonResponse = await jsonConverter.convertResponse(response); 62 | 63 | return jsonResponse.copyWith( 64 | body: deserialize(jsonResponse.body), 65 | ); 66 | } 67 | 68 | @override 69 | FutureOr convertError( 70 | Response response, 71 | ) async { 72 | final Response jsonResponse = await jsonConverter.convertResponse(response); 73 | 74 | dynamic body; 75 | 76 | try { 77 | // try to deserialize using wireName 78 | body ??= _deserialize(jsonResponse.body); 79 | } catch (_) { 80 | final type = errorType; 81 | // or check provided error type 82 | if (type != null) { 83 | final serializer = serializers.serializerForType(type); 84 | if (serializer != null) { 85 | body = serializers.deserializeWith(serializer, jsonResponse.body); 86 | } 87 | } 88 | body ??= jsonResponse.body; 89 | } 90 | 91 | return jsonResponse.copyWith(body: body); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /chopper_built_value/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_built_value/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: chopper_built_value 2 | description: A built_value based Converter for Chopper. 3 | version: 3.0.3 4 | documentation: https://hadrien-lejard.gitbook.io/chopper/converters/built-value-converter 5 | repository: https://github.com/lejard-h/chopper 6 | 7 | environment: 8 | sdk: ^3.0.0 9 | 10 | dependencies: 11 | built_value: ^8.9.2 12 | built_collection: ^5.1.1 13 | chopper: ^8.0.3 14 | http: ^1.1.0 15 | 16 | dev_dependencies: 17 | test: ^1.25.5 18 | build_runner: ^2.4.9 19 | build_test: ^2.2.2 20 | built_value_generator: ^8.9.2 21 | lints: ">=4.0.0 <7.0.0" 22 | 23 | topics: 24 | - codegen 25 | - converter 26 | - built-value 27 | -------------------------------------------------------------------------------- /chopper_built_value/pubspec_overrides.yaml: -------------------------------------------------------------------------------- 1 | # Development-only path for chopper 2 | dependency_overrides: 3 | chopper: 4 | path: ../chopper 5 | -------------------------------------------------------------------------------- /chopper_built_value/test/converter_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:built_collection/built_collection.dart'; 2 | import 'package:built_value/standard_json_plugin.dart'; 3 | import 'package:chopper/chopper.dart'; 4 | import 'package:chopper_built_value/chopper_built_value.dart'; 5 | import 'package:http/http.dart' as http; 6 | import 'package:test/test.dart'; 7 | 8 | import 'data.dart'; 9 | import 'serializers.dart'; 10 | 11 | void main() { 12 | final builder = serializers.toBuilder(); 13 | builder.addPlugin(StandardJsonPlugin()); 14 | 15 | final jsonSerializers = builder.build(); 16 | 17 | final converter = BuiltValueConverter( 18 | jsonSerializers, 19 | errorType: ErrorModel, 20 | ); 21 | 22 | final data = DataModel((b) { 23 | b.id = 42; 24 | b.name = 'foo'; 25 | }); 26 | 27 | group('BuiltValueConverter', () { 28 | test('convert request', () { 29 | var request = Request( 30 | HttpMethod.Post, 31 | Uri.parse('https://foo/'), 32 | Uri.parse(''), 33 | body: data, 34 | ); 35 | request = converter.convertRequest(request); 36 | expect(request.body, '{"\$":"DataModel","id":42,"name":"foo"}'); 37 | }); 38 | 39 | test('convert response with wireName', () async { 40 | final string = '{"\$":"DataModel","id":42,"name":"foo"}'; 41 | final response = Response(http.Response(string, 200), string); 42 | final convertedResponse = 43 | await converter.convertResponse(response); 44 | 45 | expect(convertedResponse.body?.id, equals(42)); 46 | expect(convertedResponse.body?.name, equals('foo')); 47 | }); 48 | 49 | test('convert response without wireName', () async { 50 | final string = '{"id":42,"name":"foo"}'; 51 | final response = Response(http.Response(string, 200), string); 52 | final convertedResponse = 53 | await converter.convertResponse(response); 54 | 55 | expect(convertedResponse.body?.id, equals(42)); 56 | expect(convertedResponse.body?.name, equals('foo')); 57 | }); 58 | 59 | test('convert response List', () async { 60 | final string = '[{"id":42,"name":"foo"},{"id":25,"name":"bar"}]'; 61 | final response = Response(http.Response(string, 200), string); 62 | final convertedResponse = await converter 63 | .convertResponse, DataModel>(response); 64 | 65 | final list = convertedResponse.body; 66 | expect(list?.first.id, equals(42)); 67 | expect(list?.first.name, equals('foo')); 68 | expect(list?.last.id, equals(25)); 69 | expect(list?.last.name, equals('bar')); 70 | }); 71 | 72 | test('has json headers', () { 73 | var request = Request( 74 | HttpMethod.Get, 75 | Uri.parse('https://foo/'), 76 | Uri.parse(''), 77 | body: data, 78 | ); 79 | request = converter.convertRequest(request); 80 | 81 | expect(request.headers['content-type'], equals('application/json')); 82 | }); 83 | 84 | test('convert error with wire name', () async { 85 | final string = '{"\$":"DataModel","id":42,"name":"foo"}'; 86 | final response = Response(http.Response(string, 200), string); 87 | final convertedResponse = await converter.convertError(response); 88 | 89 | expect(convertedResponse.body.id, equals(42)); 90 | expect(convertedResponse.body.name, equals('foo')); 91 | }); 92 | 93 | test('convert error using provided type', () async { 94 | final string = '{"message":"Error message"}'; 95 | final response = Response(http.Response(string, 200), string); 96 | final convertedResponse = await converter.convertError(response); 97 | 98 | expect(convertedResponse.body.message, equals('Error message')); 99 | }); 100 | 101 | test('convert error falls back to raw body when deserialization fails', 102 | () async { 103 | // Create a converter without an errorType specified to trigger the fallback path 104 | final converterWithoutErrorType = BuiltValueConverter(jsonSerializers); 105 | 106 | // JSON object that doesn't match any model and has no wireName 107 | final string = 108 | '{"unknown":"structure", "that": "wont", "match": "any model"}'; 109 | final response = Response(http.Response(string, 400), string); 110 | 111 | final convertedResponse = 112 | await converterWithoutErrorType.convertError(response); 113 | 114 | // Check that the body is the raw JSON object (fallback path was taken) 115 | expect(convertedResponse.body, isA>()); 116 | expect(convertedResponse.body['unknown'], equals('structure')); 117 | expect(convertedResponse.body['that'], equals('wont')); 118 | expect(convertedResponse.body['match'], equals('any model')); 119 | }); 120 | }); 121 | } 122 | -------------------------------------------------------------------------------- /chopper_built_value/test/data.dart: -------------------------------------------------------------------------------- 1 | import 'package:built_value/built_value.dart'; 2 | import 'package:built_value/serializer.dart'; 3 | 4 | part 'data.g.dart'; 5 | 6 | abstract class DataModel implements Built { 7 | int get id; 8 | String get name; 9 | 10 | static Serializer get serializer => _$dataModelSerializer; 11 | factory DataModel([Function(DataModelBuilder b) updates]) = _$DataModel; 12 | DataModel._(); 13 | } 14 | 15 | abstract class ErrorModel implements Built { 16 | String get message; 17 | 18 | static Serializer get serializer => _$errorModelSerializer; 19 | factory ErrorModel([Function(ErrorModelBuilder b) updates]) = _$ErrorModel; 20 | ErrorModel._(); 21 | } 22 | -------------------------------------------------------------------------------- /chopper_built_value/test/serializers.dart: -------------------------------------------------------------------------------- 1 | library; 2 | 3 | import 'package:built_value/serializer.dart'; 4 | 5 | import 'data.dart'; 6 | 7 | part 'serializers.g.dart'; 8 | 9 | /// Collection of generated serializers for the built_value 10 | @SerializersFor([ 11 | DataModel, 12 | ErrorModel, 13 | ]) 14 | final Serializers serializers = _$serializers; 15 | -------------------------------------------------------------------------------- /chopper_built_value/test/serializers.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'serializers.dart'; 4 | 5 | // ************************************************************************** 6 | // BuiltValueGenerator 7 | // ************************************************************************** 8 | 9 | Serializers _$serializers = (new Serializers().toBuilder() 10 | ..add(DataModel.serializer) 11 | ..add(ErrorModel.serializer)) 12 | .build(); 13 | 14 | // ignore_for_file: deprecated_member_use_from_same_package,type=lint 15 | -------------------------------------------------------------------------------- /chopper_generator/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_generator/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 | tests: 49 | @# Help: Run Dart unit and widget tests for the current project. 50 | dart test 51 | 52 | upgrade: 53 | @# Help: Upgrade all the project's packages. 54 | dart pub upgrade -------------------------------------------------------------------------------- /chopper_generator/README.md: -------------------------------------------------------------------------------- 1 | # chopper_generator 2 | 3 | [![pub package](https://img.shields.io/pub/v/chopper_generator.svg)](https://pub.dartlang.org/packages/chopper_generator) 4 | 5 | This package provides the code generator for the [Chopper](https://github.com/lejard-h/chopper) package. 6 | 7 | ## Usage 8 | 9 | For examples please refer to the main [Chopper](https://github.com/lejard-h/chopper) package and/or read the 10 | [documentation](https://hadrien-lejard.gitbook.io/chopper). -------------------------------------------------------------------------------- /chopper_generator/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_generator/build.yaml: -------------------------------------------------------------------------------- 1 | # Read about `build.yaml` at https://pub.dartlang.org/packages/build_config 2 | builders: 3 | chopper_generator: 4 | target: ":chopper_generator" 5 | import: "package:chopper_generator/chopper_generator.dart" 6 | builder_factories: ["chopperGeneratorFactory"] 7 | build_extensions: {".dart": [".chopper.dart"]} 8 | auto_apply: root_package 9 | build_to: source -------------------------------------------------------------------------------- /chopper_generator/lib/chopper_generator.dart: -------------------------------------------------------------------------------- 1 | library; 2 | 3 | export 'src/builder_factory.dart'; 4 | -------------------------------------------------------------------------------- /chopper_generator/lib/src/builder_factory.dart: -------------------------------------------------------------------------------- 1 | import 'package:build/build.dart'; 2 | import 'package:chopper/chopper.dart' show ChopperApi; 3 | import 'package:source_gen/source_gen.dart'; 4 | import 'package:yaml/yaml.dart'; 5 | 6 | import 'generator.dart'; 7 | 8 | /// Creates a [PartBuilder] used to generate code for [ChopperApi] annotated 9 | /// classes. The [options] are provided by Dart's build system and read from the 10 | /// `build.yaml` file. 11 | Builder chopperGeneratorFactory(BuilderOptions options) { 12 | final String buildExtension = _getBuildExtension(options); 13 | 14 | return PartBuilder( 15 | [const ChopperGenerator()], 16 | buildExtension, 17 | header: options.config['header'], 18 | formatOutput: PartBuilder( 19 | [const ChopperGenerator()], 20 | buildExtension, 21 | ).formatOutput, 22 | options: !options.config.containsKey('build_extensions') 23 | ? options.overrideWith( 24 | BuilderOptions({ 25 | 'build_extensions': { 26 | '.dart': [buildExtension] 27 | }, 28 | }), 29 | ) 30 | : options, 31 | ); 32 | } 33 | 34 | /// Returns the build extension for the generated file. 35 | /// 36 | /// If the `build.yaml` file contains a `build_extensions` key, it will be used 37 | /// to determine the extension. Otherwise, the default extension `.chopper.dart` 38 | /// will be used. 39 | /// 40 | /// Example `build.yaml`: 41 | /// 42 | /// ```yaml 43 | /// targets: 44 | /// $default: 45 | /// builders: 46 | /// chopper_generator: 47 | /// options: 48 | /// build_extensions: {".dart": [".chopper.g.dart"]} 49 | /// ``` 50 | String _getBuildExtension(BuilderOptions options) { 51 | if (options.config.containsKey('build_extensions')) { 52 | final YamlMap buildExtensions = options.config['build_extensions']; 53 | if (buildExtensions.containsKey('.dart')) { 54 | final YamlList dartBuildExtensions = buildExtensions['.dart']; 55 | if (dartBuildExtensions.isNotEmpty) { 56 | return dartBuildExtensions.first; 57 | } 58 | } 59 | } 60 | return '.chopper.dart'; 61 | } 62 | -------------------------------------------------------------------------------- /chopper_generator/lib/src/extensions.dart: -------------------------------------------------------------------------------- 1 | import 'package:analyzer/dart/element/nullability_suffix.dart'; 2 | import 'package:analyzer/dart/element/type.dart'; 3 | 4 | extension DartTypeExtension on DartType { 5 | bool get isNullable => nullabilitySuffix != NullabilitySuffix.none; 6 | } 7 | -------------------------------------------------------------------------------- /chopper_generator/lib/src/utils.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: deprecated_member_use 2 | 3 | import 'dart:math' show max; 4 | 5 | import 'package:analyzer/dart/element/element.dart'; 6 | import 'package:chopper/chopper.dart' show ListFormat; 7 | import 'package:chopper_generator/src/extensions.dart'; 8 | import 'package:code_builder/code_builder.dart'; 9 | import 'package:collection/collection.dart'; 10 | import 'package:source_gen/source_gen.dart'; 11 | 12 | final class Utils { 13 | static bool getMethodOptionalBody(ConstantReader method) => 14 | method.read('optionalBody').boolValue; 15 | 16 | static String getMethodPath(ConstantReader method) => 17 | method.read('path').stringValue; 18 | 19 | static String getMethodName(ConstantReader method) => 20 | method.read('method').stringValue; 21 | 22 | static ListFormat? getListFormat(ConstantReader method) { 23 | return ListFormat.values.firstWhereOrNull( 24 | (listFormat) => 25 | listFormat.name == 26 | method 27 | .peek('listFormat') 28 | ?.objectValue 29 | .getField('_name') 30 | ?.toStringValue(), 31 | ); 32 | } 33 | 34 | static bool? getUseBrackets(ConstantReader method) => 35 | method.peek('useBrackets')?.boolValue; 36 | 37 | static bool? getIncludeNullQueryVars(ConstantReader method) => 38 | method.peek('includeNullQueryVars')?.boolValue; 39 | 40 | static Duration? getTimeout(ConstantReader method) { 41 | final ConstantReader? timeout = method.peek('timeout'); 42 | if (timeout != null) { 43 | final int? microseconds = 44 | timeout.objectValue.getField('_duration')?.toIntValue(); 45 | if (microseconds != null) { 46 | return Duration(microseconds: max(microseconds, 0)); 47 | } 48 | } 49 | 50 | return null; 51 | } 52 | 53 | /// All positional required params must support nullability 54 | static Parameter buildRequiredPositionalParam(ParameterElement p) => 55 | Parameter( 56 | (ParameterBuilder pb) => pb 57 | ..name = p.name 58 | ..type = Reference( 59 | p.type.getDisplayString(withNullability: p.type.isNullable), 60 | ), 61 | ); 62 | 63 | /// All optional positional params must support nullability 64 | static Parameter buildOptionalPositionalParam(ParameterElement p) => 65 | Parameter((ParameterBuilder pb) { 66 | pb 67 | ..name = p.name 68 | ..type = Reference( 69 | p.type.getDisplayString(withNullability: p.type.isNullable), 70 | ); 71 | 72 | if (p.defaultValueCode != null) { 73 | pb.defaultTo = Code(p.defaultValueCode!); 74 | } 75 | }); 76 | 77 | /// Named params can be optional or required, they also need to support nullability 78 | static Parameter buildNamedParam(ParameterElement p) => 79 | Parameter((ParameterBuilder pb) { 80 | pb 81 | ..named = true 82 | ..name = p.name 83 | ..required = p.isRequiredNamed 84 | ..type = Reference( 85 | p.type.getDisplayString(withNullability: p.type.isNullable), 86 | ); 87 | 88 | if (p.defaultValueCode != null) { 89 | pb.defaultTo = Code(p.defaultValueCode!); 90 | } 91 | }); 92 | } 93 | -------------------------------------------------------------------------------- /chopper_generator/lib/src/vars.dart: -------------------------------------------------------------------------------- 1 | enum Vars { 2 | client('client'), 3 | response(r'$response'), 4 | baseUrl('baseUrl'), 5 | parameters(r'$params'), 6 | headers(r'$headers'), 7 | request(r'$request'), 8 | body(r'$body'), 9 | parts(r'$parts'), 10 | url(r'$url'); 11 | 12 | const Vars(this.name); 13 | 14 | final String name; 15 | 16 | @override 17 | String toString() => name; 18 | } 19 | -------------------------------------------------------------------------------- /chopper_generator/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 | 12 | cache: 13 | directories: 14 | - .dart_tool/build -------------------------------------------------------------------------------- /chopper_generator/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: chopper_generator 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 | analyzer: ">=6.9.0 <8.0.0" 12 | build: ^2.4.1 13 | built_collection: ^5.1.1 14 | chopper: ^8.0.4 15 | code_builder: ^4.10.0 16 | logging: ^1.2.0 17 | meta: ^1.9.1 18 | source_gen: ">=1.5.0 <3.0.0" 19 | yaml: ^3.1.2 20 | collection: ^1.18.0 21 | 22 | dev_dependencies: 23 | build_runner: ^2.4.9 24 | build_verify: ^3.1.0 25 | http: ^1.1.0 26 | lints: ">=4.0.0 <7.0.0" 27 | test: ^1.25.5 28 | 29 | topics: 30 | - api 31 | - codegen 32 | - http 33 | - rest 34 | -------------------------------------------------------------------------------- /chopper_generator/pubspec_overrides.yaml: -------------------------------------------------------------------------------- 1 | # Development-only path for chopper 2 | dependency_overrides: 3 | chopper: 4 | path: ../chopper 5 | -------------------------------------------------------------------------------- /chopper_generator/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_generator', 14 | gitDiffPathArguments: [ 15 | 'test/test_service.chopper.dart', 16 | 'test/test_service_variable.chopper.dart', 17 | 'test/test_without_response_service.chopper.dart', 18 | ], 19 | ); 20 | }, 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | ignore: 3 | - "chopper/lib/src/annotations.dart" 4 | - "chopper/lib/src/constants.dart" -------------------------------------------------------------------------------- /converters/built-value-converter.md: -------------------------------------------------------------------------------- 1 | # BuiltValueConverter 2 | 3 | {% hint style="warning" %} 4 | Experimental 5 | {% endhint %} 6 | 7 | A Chopper Converter for [built\_value](https://pub.dev/packages/built_value) based serialization. 8 | 9 | ## Installation 10 | 11 | Add the chopper_built_value package to your project's dependencies in pubspec.yaml: 12 | 13 | ```yaml 14 | # pubspec.yaml 15 | 16 | dependencies: 17 | chopper_built_value: ^ 18 | ``` 19 | 20 | The latest version is [![pub package](https://img.shields.io/pub/v/chopper_built_value.svg)](https://pub.dartlang.org/packages/chopper_built_value). 21 | 22 | ## Getting started 23 | 24 | ### Built value 25 | 26 | Define your models as you usually do with built_value. 27 | 28 | ```dart 29 | abstract class DataModel implements Built { 30 | int get id; 31 | String get name; 32 | 33 | static Serializer get serializer => _$dataModelSerializer; 34 | factory DataModel([updates(DataModelBuilder b)]) = _$DataModel; 35 | DataModel._(); 36 | } 37 | ``` 38 | 39 | Aggregate all serializers into a top level collection. 40 | 41 | ```dart 42 | /// Collection of generated serializers for the built_value 43 | @SerializersFor([ 44 | DataModel, 45 | ]) 46 | final Serializers serializers = _$serializers; 47 | ``` 48 | 49 | See [built\_value documentation](https://pub.dev/packages/built_value) for more information on how built_value works. 50 | 51 | ### Using BuiltValueConverter with Chopper 52 | 53 | Build a `BuiltValueConverter` by providing the `built_value` serializer collection. 54 | 55 | To use the created converter, pass it to `ChopperClient`'s `converter` constructor parameter. 56 | 57 | ```dart 58 | final builder = serializers.toBuilder(); 59 | builder.addPlugin(StandardJsonPlugin()); 60 | 61 | final jsonSerializers = builder.build(); 62 | final converter = BuiltValueConverter(jsonSerializers); 63 | 64 | final client = ChopperClient(converter: converter); 65 | ``` 66 | 67 | #### Error converter 68 | 69 | `BuiltValueConverter` is also an error converter. It will try to decode error response bodies using the `wireName` inside JSON `{"$":"ErrorModel"}`, if available. 70 | 71 | If `wireName` is not available, `BuiltValueConverter` will try to convert error response bodies to `errorType`, if it was provided and is not `null`. 72 | 73 | ```dart 74 | final jsonSerializers = ... 75 | 76 | final converter = BuiltValueConverter(jsonSerializers, errorType: ErrorModel); 77 | ``` 78 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /converters/converters.md: -------------------------------------------------------------------------------- 1 | # Converters 2 | 3 | Converters are used to apply transformations on request and/or response bodies, for example, transforming a Dart object to a `Map` or vice versa. 4 | 5 | Both `converter` and `errorConverter` are called before request and response interceptors. 6 | 7 | ```dart 8 | final chopper = ChopperClient( 9 | converter: JsonConverter(), 10 | errorConverter: JsonConverter() 11 | ); 12 | ``` 13 | 14 | {% hint style="info" %} 15 | The `errorConverter` is called only on error responses (statusCode < 200 || statusCode >= 300). 16 | {% endhint %} 17 | 18 | ## The built-in JSON converter 19 | 20 | Chopper provides a `JsonConverter` that is able to encode data to JSON and decode JSON strings. It will also apply the correct header to the request \(application/json\). 21 | 22 | However, if content type header is modified (for example by using `@Post(headers: {'content-type': '...'})`), `JsonConverter` won't add the header and it won't call json.encode if content type is not JSON. 23 | 24 | {% hint style="danger" %} 25 | `JsonConverter` itself won't convert a Dart object into a `Map` or a `List`, but it will convert a `Map` into a JSON string. 26 | {% endhint %} 27 | 28 | ## Implementing custom converters 29 | 30 | You can implement custom converters by implementing the `Converter` class. 31 | 32 | ```dart 33 | class MyConverter implements Converter { 34 | @override 35 | Response convertResponse(Response response) { 36 | var body = response.body; 37 | // Convert body to BodyType however you like 38 | response.copyWith(body: body); 39 | } 40 | 41 | @override 42 | Request convertRequest(Request request) { 43 | var body = request.body; 44 | // Convert body to String however you like 45 | return request.copyWith(body: body); 46 | } 47 | } 48 | ``` 49 | 50 | `BodyType`is the expected type of the response body \(e.g., `String` or `CustomObject`). 51 | 52 | If `BodyType` is a `List` or a `BuiltList`, `InnerType` is the type of the generic parameter \(e.g., `convertResponse, CustomObject>(response)`). 53 | 54 | ## Using different converters for specific endpoints 55 | 56 | If you want to apply specific converters only to a single endpoint, you can do so by using the `@FactoryConverter` annotation: 57 | 58 | ```dart 59 | @ChopperApi(baseUrl: "/todos") 60 | abstract class TodosListService extends ChopperService { 61 | 62 | @FactoryConverter( 63 | request: FormUrlEncodedConverter.requestFactory, 64 | response: convertResponse, 65 | ) 66 | @Post(path: '/') 67 | Future post(@Field() String foo, @Field() int bar); 68 | 69 | } 70 | 71 | Response convertResponse(Response res) => 72 | JsonConverter().convertResponse(res); 73 | ``` 74 | -------------------------------------------------------------------------------- /docs/ci/ci_setup.md: -------------------------------------------------------------------------------- 1 | # The CI setup of the project 2 | 3 | ⚠️ This document is heavily WIP. It will contain the full CI setup guide for this project. 4 | 5 | ## Generating the CI config 6 | 7 | We use the [`mono_repo`](https://pub.dev/packages/mono_repo) Dart package project for generating the GitHub CI config. 8 | 9 | To install and use `mono_repo`, refer to its official documentation linked above. -------------------------------------------------------------------------------- /example/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 | upgrade: 45 | @# Help: Upgrade all the project's packages. 46 | dart pub upgrade -------------------------------------------------------------------------------- /example/analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:lints/recommended.yaml 2 | 3 | analyzer: 4 | exclude: 5 | - "**.g.dart" 6 | - "**.chopper.dart" 7 | - "**.mocks.dart" 8 | 9 | linter: 10 | rules: 11 | avoid_print: false 12 | prefer_single_quotes: true 13 | -------------------------------------------------------------------------------- /example/bin/main_built_value.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:built_collection/built_collection.dart'; 4 | import 'package:built_value/serializer.dart'; 5 | import 'package:built_value/standard_json_plugin.dart'; 6 | import 'package:chopper/chopper.dart'; 7 | import 'package:chopper_example/built_value_resource.dart'; 8 | import 'package:chopper_example/built_value_serializers.dart'; 9 | import 'package:http/http.dart' as http; 10 | import 'package:http/testing.dart'; 11 | 12 | final jsonSerializers = 13 | (serializers.toBuilder()..addPlugin(StandardJsonPlugin())).build(); 14 | 15 | /// Simple client to have working example without remote server 16 | final client = MockClient((req) async { 17 | if (req.method == 'POST') { 18 | return http.Response('{"type":"Fatal","message":"fatal erorr"}', 500); 19 | } 20 | if (req.url.path == '/resources/list') { 21 | return http.Response('[{"id":"1","name":"Foo"}]', 200); 22 | } 23 | 24 | return http.Response('{"id":"1","name":"Foo"}', 200); 25 | }); 26 | 27 | main() async { 28 | final chopper = ChopperClient( 29 | client: client, 30 | baseUrl: Uri.parse('http://localhost:8000'), 31 | converter: BuiltValueConverter(), 32 | errorConverter: BuiltValueConverter(), 33 | services: [ 34 | // the generated service 35 | MyService.create(), 36 | ], 37 | ); 38 | 39 | final myService = chopper.getService(); 40 | 41 | final response1 = await myService.getResource('1'); 42 | print('response 1: ${response1.body}'); // undecoded String 43 | 44 | final response2 = await myService.getTypedResource(); 45 | print('response 2: ${response2.body}'); // decoded Resource 46 | 47 | final response3 = await myService.getBuiltListResources(); 48 | print('response 3: ${response3.body}'); 49 | 50 | try { 51 | final builder = ResourceBuilder() 52 | ..id = '3' 53 | ..name = 'Super Name'; 54 | await myService.newResource(builder.build()); 55 | } on Response catch (error) { 56 | print(error.body); 57 | } 58 | } 59 | 60 | class BuiltValueConverter extends JsonConverter { 61 | T? _deserialize(dynamic value) { 62 | final serializer = jsonSerializers.serializerForType(T) as Serializer?; 63 | if (serializer == null) { 64 | throw Exception('No serializer for type $T'); 65 | } 66 | 67 | return jsonSerializers.deserializeWith(serializer, value); 68 | } 69 | 70 | BuiltList _deserializeListOf(Iterable value) => BuiltList( 71 | value.map((value) => _deserialize(value)).toList(growable: false), 72 | ); 73 | 74 | dynamic _decode(dynamic entity) { 75 | /// handle case when we want to access to Map directly 76 | /// getResource or getMapResource 77 | /// Avoid dynamic or unconverted value, this could lead to several issues 78 | if (entity is T) return entity; 79 | 80 | try { 81 | return entity is List 82 | ? _deserializeListOf(entity) 83 | : _deserialize(entity); 84 | } catch (e) { 85 | print(e); 86 | 87 | return null; 88 | } 89 | } 90 | 91 | @override 92 | FutureOr> convertResponse( 93 | Response response, 94 | ) async { 95 | // use [JsonConverter] to decode json 96 | final Response jsonRes = await super.convertResponse(response); 97 | final body = _decode(jsonRes.body); 98 | 99 | return jsonRes.copyWith(body: body); 100 | } 101 | 102 | @override 103 | Request convertRequest(Request request) => super.convertRequest( 104 | request.copyWith( 105 | body: serializers.serialize(request.body), 106 | ), 107 | ); 108 | } 109 | -------------------------------------------------------------------------------- /example/bin/main_json_serializable.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:chopper/chopper.dart'; 4 | import 'package:chopper_example/json_serializable.dart'; 5 | import 'package:http/http.dart' as http; 6 | import 'package:http/testing.dart'; 7 | 8 | /// Simple client to have working example without remote server 9 | final client = MockClient((req) async { 10 | if (req.method == 'POST') { 11 | return http.Response('{"type":"Fatal","message":"fatal erorr"}', 500); 12 | } 13 | if (req.method == 'GET' && req.headers['test'] == 'list') { 14 | return http.Response('[{"id":"1","name":"Foo"}]', 200); 15 | } 16 | 17 | return http.Response('{"id":"1","name":"Foo"}', 200); 18 | }); 19 | 20 | main() async { 21 | final converter = JsonSerializableConverter({ 22 | Resource: Resource.fromJsonFactory, 23 | }); 24 | 25 | final chopper = ChopperClient( 26 | client: client, 27 | baseUrl: Uri.parse('http://localhost:8000'), 28 | // bind your object factories here 29 | converter: converter, 30 | errorConverter: converter, 31 | services: [ 32 | // the generated service 33 | MyService.create(), 34 | ], 35 | /* Interceptors */ 36 | interceptors: [AuthInterceptor()], 37 | ); 38 | 39 | final myService = chopper.getService(); 40 | 41 | final response1 = await myService.getResource('1'); 42 | print('response 1: ${response1.body}'); // undecoded String 43 | 44 | final response2 = await myService.getResources(); 45 | print('response 2: ${response2.body}'); // decoded list of Resources 46 | 47 | final response3 = await myService.getTypedResource(); 48 | print('response 3: ${response3.body}'); // decoded Resource 49 | 50 | final response4 = await myService.getMapResource('1'); 51 | print('response 4: ${response4.body}'); // undecoded Resource 52 | 53 | try { 54 | await myService.newResource(Resource('3', 'Super Name')); 55 | } on Response catch (error) { 56 | print(error.body); 57 | } 58 | } 59 | 60 | class AuthInterceptor implements Interceptor { 61 | @override 62 | FutureOr> intercept( 63 | Chain chain) async { 64 | return chain.proceed( 65 | applyHeader( 66 | chain.request, 67 | 'Authorization', 68 | '42', 69 | ), 70 | ); 71 | } 72 | } 73 | 74 | typedef JsonFactory = T Function(Map json); 75 | 76 | class JsonSerializableConverter extends JsonConverter { 77 | final Map factories; 78 | 79 | const JsonSerializableConverter(this.factories); 80 | 81 | T? _decodeMap(Map values) { 82 | /// Get jsonFactory using Type parameters 83 | /// if not found or invalid, throw error or return null 84 | final jsonFactory = factories[T]; 85 | if (jsonFactory == null || jsonFactory is! JsonFactory) { 86 | /// throw serializer not found error; 87 | return null; 88 | } 89 | 90 | return jsonFactory(values); 91 | } 92 | 93 | List _decodeList(Iterable values) => 94 | values.where((v) => v != null).map((v) => _decode(v)).toList(); 95 | 96 | dynamic _decode(entity) { 97 | if (entity is Iterable) return _decodeList(entity as List); 98 | 99 | if (entity is Map) return _decodeMap(entity as Map); 100 | 101 | return entity; 102 | } 103 | 104 | @override 105 | FutureOr> convertResponse( 106 | Response response, 107 | ) async { 108 | // use [JsonConverter] to decode json 109 | final jsonRes = await super.convertResponse(response); 110 | 111 | return jsonRes.copyWith(body: _decode(jsonRes.body)); 112 | } 113 | 114 | @override 115 | // all objects should implements toJson method 116 | // ignore: unnecessary_overrides 117 | Request convertRequest(Request request) => super.convertRequest(request); 118 | 119 | @override 120 | FutureOr convertError(Response response) async { 121 | // use [JsonConverter] to decode json 122 | final jsonRes = await super.convertError(response); 123 | 124 | return jsonRes.copyWith( 125 | body: ResourceError.fromJsonFactory(jsonRes.body), 126 | ); 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /example/bin/main_json_serializable_squadron_worker_pool.dart: -------------------------------------------------------------------------------- 1 | // This example uses 2 | // - https://github.com/google/json_serializable.dart 3 | // - https://github.com/d-markey/squadron 4 | // - https://github.com/d-markey/squadron_builder 5 | 6 | import 'dart:async' show FutureOr; 7 | import 'dart:convert' show jsonDecode; 8 | 9 | import 'package:chopper/chopper.dart'; 10 | import 'package:chopper_example/json_decode_service.dart'; 11 | import 'package:chopper_example/json_serializable.dart'; 12 | import 'package:http/testing.dart'; 13 | import 'package:squadron/squadron.dart'; 14 | import 'package:http/http.dart' as http; 15 | 16 | import 'main_json_serializable.dart' show AuthInterceptor; 17 | 18 | typedef JsonFactory = T Function(Map json); 19 | 20 | /// This JsonConverter works with or without a WorkerPool 21 | class JsonSerializableWorkerPoolConverter extends JsonConverter { 22 | const JsonSerializableWorkerPoolConverter(this.factories, [this.workerPool]); 23 | 24 | final Map factories; 25 | final JsonDecodeServiceWorkerPool? workerPool; 26 | 27 | T? _decodeMap(Map values) { 28 | /// Get jsonFactory using Type parameters 29 | /// if not found or invalid, throw error or return null 30 | final jsonFactory = factories[T]; 31 | if (jsonFactory == null || jsonFactory is! JsonFactory) { 32 | /// throw serializer not found error; 33 | return null; 34 | } 35 | 36 | return jsonFactory(values); 37 | } 38 | 39 | List _decodeList(Iterable values) => 40 | values.where((v) => v != null).map((v) => _decode(v)).toList(); 41 | 42 | dynamic _decode(entity) { 43 | if (entity is Iterable) return _decodeList(entity as List); 44 | 45 | if (entity is Map) return _decodeMap(entity as Map); 46 | 47 | return entity; 48 | } 49 | 50 | @override 51 | FutureOr> convertResponse( 52 | Response response, 53 | ) async { 54 | // use [JsonConverter] to decode json 55 | final jsonRes = await super.convertResponse(response); 56 | 57 | return jsonRes.copyWith(body: _decode(jsonRes.body)); 58 | } 59 | 60 | @override 61 | FutureOr convertError(Response response) async { 62 | // use [JsonConverter] to decode json 63 | final jsonRes = await super.convertError(response); 64 | 65 | return jsonRes.copyWith( 66 | body: ResourceError.fromJsonFactory(jsonRes.body), 67 | ); 68 | } 69 | 70 | @override 71 | FutureOr tryDecodeJson(String data) async { 72 | try { 73 | // if there is a worker pool use it, otherwise run in the main thread 74 | return workerPool != null 75 | ? await workerPool!.jsonDecode(data) 76 | : jsonDecode(data); 77 | } catch (error) { 78 | print(error); 79 | 80 | chopperLogger.warning(error); 81 | 82 | return data; 83 | } 84 | } 85 | } 86 | 87 | /// Simple client to have working example without remote server 88 | final client = MockClient((http.Request req) async { 89 | if (req.method == 'POST') { 90 | return http.Response('{"type":"Fatal","message":"fatal error"}', 500); 91 | } 92 | if (req.method == 'GET' && req.headers['test'] == 'list') { 93 | return http.Response('[{"id":"1","name":"Foo"}]', 200); 94 | } 95 | 96 | return http.Response('{"id":"1","name":"Foo"}', 200); 97 | }); 98 | 99 | /// inspired by https://github.com/d-markey/squadron_sample/blob/main/lib/main.dart 100 | void initSquadron(String id) { 101 | Squadron.setId(id); 102 | Squadron.setLogger(ConsoleSquadronLogger()); 103 | Squadron.logLevel = SquadronLogLevel.all; 104 | Squadron.debugMode = true; 105 | } 106 | 107 | Future main() async { 108 | /// initialize Squadron before using it 109 | initSquadron('worker_pool_example'); 110 | 111 | final jsonDecodeServiceWorkerPool = JsonDecodeServiceWorkerPool( 112 | // Set whatever you want here 113 | concurrencySettings: ConcurrencySettings.oneCpuThread, 114 | ); 115 | 116 | /// start the Worker Pool 117 | await jsonDecodeServiceWorkerPool.start(); 118 | 119 | final converter = JsonSerializableWorkerPoolConverter( 120 | { 121 | Resource: Resource.fromJsonFactory, 122 | }, 123 | // make sure to provide the WorkerPool to the JsonConverter 124 | jsonDecodeServiceWorkerPool, 125 | ); 126 | 127 | final chopper = ChopperClient( 128 | client: client, 129 | baseUrl: Uri.parse('http://localhost:8000'), 130 | // bind your object factories here 131 | converter: converter, 132 | errorConverter: converter, 133 | services: [ 134 | // the generated service 135 | MyService.create(), 136 | ], 137 | /* Interceptor */ 138 | interceptors: [AuthInterceptor()], 139 | ); 140 | 141 | final myService = chopper.getService(); 142 | 143 | /// All of the calls below will use jsonDecode in an Isolate worker 144 | final response1 = await myService.getResource('1'); 145 | print('response 1: ${response1.body}'); // undecoded String 146 | 147 | final response2 = await myService.getResources(); 148 | print('response 2: ${response2.body}'); // decoded list of Resources 149 | 150 | final response3 = await myService.getTypedResource(); 151 | print('response 3: ${response3.body}'); // decoded Resource 152 | 153 | final response4 = await myService.getMapResource('1'); 154 | print('response 4: ${response4.body}'); // undecoded Resource 155 | 156 | try { 157 | await myService.newResource(Resource('3', 'Super Name')); 158 | } on Response catch (error) { 159 | print(error.body); 160 | } 161 | 162 | /// stop the Worker Pool 163 | jsonDecodeServiceWorkerPool.stop(); 164 | } 165 | -------------------------------------------------------------------------------- /example/build.yaml: -------------------------------------------------------------------------------- 1 | targets: 2 | $default: 3 | builders: 4 | json_serializable: 5 | options: 6 | # Options configure how source code is generated for every 7 | # `@JsonSerializable`-annotated class in the package. 8 | # 9 | # The default value for each is listed. 10 | # 11 | # For usage information, reference the corresponding field in 12 | # `JsonSerializableGenerator`. 13 | any_map: false 14 | checked: false 15 | explicit_to_json: true 16 | create_to_json: true 17 | squadron_builder:worker_builder: 18 | options: 19 | with_finalizers: true 20 | serialization_type: List -------------------------------------------------------------------------------- /example/build_serializers.yaml: -------------------------------------------------------------------------------- 1 | targets: 2 | $default: 3 | sources: 4 | include: ["lib/built_value.dart", 5 | "lib/built_value_serializers.dart", 6 | "lib/json_serializable.dart", 7 | "lib/jaguar_serializer.dart" ] 8 | builders: 9 | json_serializable: 10 | options: 11 | # Options configure how source code is generated for every 12 | # `@JsonSerializable`-annotated class in the package. 13 | # 14 | # The default value for each is listed. 15 | # 16 | # For usage information, reference the corresponding field in 17 | # `JsonSerializableGenerator`. 18 | use_wrappers: false 19 | any_map: false 20 | checked: false 21 | explicit_to_json: true 22 | generate_to_json_function: true -------------------------------------------------------------------------------- /example/lib/built_value_resource.chopper.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'built_value_resource.dart'; 4 | 5 | // ************************************************************************** 6 | // ChopperGenerator 7 | // ************************************************************************** 8 | 9 | // coverage:ignore-file 10 | // ignore_for_file: type=lint 11 | final class _$MyService extends MyService { 12 | _$MyService([ChopperClient? client]) { 13 | if (client == null) return; 14 | this.client = client; 15 | } 16 | 17 | @override 18 | final Type definitionType = MyService; 19 | 20 | @override 21 | Future> getResource(String id) { 22 | final Uri $url = Uri.parse('/resources/${id}/'); 23 | final Request $request = Request( 24 | 'GET', 25 | $url, 26 | client.baseUrl, 27 | ); 28 | return client.send($request); 29 | } 30 | 31 | @override 32 | Future>> getBuiltListResources() { 33 | final Uri $url = Uri.parse('/resources/list'); 34 | final Request $request = Request( 35 | 'GET', 36 | $url, 37 | client.baseUrl, 38 | ); 39 | return client.send, Resource>($request); 40 | } 41 | 42 | @override 43 | Future> getTypedResource() { 44 | final Uri $url = Uri.parse('/resources/'); 45 | final Map $headers = { 46 | 'foo': 'bar', 47 | }; 48 | final Request $request = Request( 49 | 'GET', 50 | $url, 51 | client.baseUrl, 52 | headers: $headers, 53 | ); 54 | return client.send($request); 55 | } 56 | 57 | @override 58 | Future> newResource( 59 | Resource resource, { 60 | String? name, 61 | }) { 62 | final Uri $url = Uri.parse('/resources'); 63 | final Map $headers = { 64 | if (name != null) 'name': name, 65 | }; 66 | final $body = resource; 67 | final Request $request = Request( 68 | 'POST', 69 | $url, 70 | client.baseUrl, 71 | body: $body, 72 | headers: $headers, 73 | ); 74 | return client.send($request); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /example/lib/built_value_resource.dart: -------------------------------------------------------------------------------- 1 | library resource; 2 | 3 | import 'dart:async'; 4 | 5 | import 'package:built_collection/built_collection.dart'; 6 | import 'package:built_value/built_value.dart'; 7 | import 'package:built_value/serializer.dart'; 8 | import 'package:chopper/chopper.dart'; 9 | 10 | part 'built_value_resource.chopper.dart'; 11 | part 'built_value_resource.g.dart'; 12 | 13 | abstract class Resource implements Built { 14 | String get id; 15 | 16 | String get name; 17 | 18 | static Serializer get serializer => _$resourceSerializer; 19 | 20 | factory Resource([Function(ResourceBuilder b) updates]) = _$Resource; 21 | 22 | Resource._(); 23 | } 24 | 25 | abstract class ResourceError 26 | implements Built { 27 | String get type; 28 | 29 | String get message; 30 | 31 | static Serializer get serializer => _$resourceErrorSerializer; 32 | 33 | factory ResourceError([Function(ResourceErrorBuilder b) updates]) = 34 | _$ResourceError; 35 | 36 | ResourceError._(); 37 | } 38 | 39 | @ChopperApi(baseUrl: '/resources') 40 | abstract class MyService extends ChopperService { 41 | static MyService create([ChopperClient? client]) => _$MyService(client); 42 | 43 | @GET(path: '/{id}/') 44 | Future getResource(@Path() String id); 45 | 46 | @GET(path: '/list') 47 | Future>> getBuiltListResources(); 48 | 49 | @GET(path: '/', headers: {'foo': 'bar'}) 50 | Future> getTypedResource(); 51 | 52 | @POST() 53 | Future> newResource( 54 | @Body() Resource resource, { 55 | @Header() String? name, 56 | }); 57 | } 58 | -------------------------------------------------------------------------------- /example/lib/built_value_serializers.dart: -------------------------------------------------------------------------------- 1 | library serializers; 2 | 3 | import 'package:built_value/serializer.dart'; 4 | 5 | import 'built_value_resource.dart'; 6 | 7 | part 'built_value_serializers.g.dart'; 8 | 9 | /// Collection of generated serializers for the built_value chat example. 10 | @SerializersFor([ 11 | Resource, 12 | ResourceError, 13 | ]) 14 | final Serializers serializers = _$serializers; 15 | -------------------------------------------------------------------------------- /example/lib/built_value_serializers.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'built_value_serializers.dart'; 4 | 5 | // ************************************************************************** 6 | // BuiltValueGenerator 7 | // ************************************************************************** 8 | 9 | Serializers _$serializers = (new Serializers().toBuilder() 10 | ..add(Resource.serializer) 11 | ..add(ResourceError.serializer)) 12 | .build(); 13 | 14 | // ignore_for_file: deprecated_member_use_from_same_package,type=lint 15 | -------------------------------------------------------------------------------- /example/lib/json_decode_service.activator.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | // ************************************************************************** 4 | // Generator: WorkerGenerator 2.4.2 5 | // ************************************************************************** 6 | 7 | import 'json_decode_service.vm.g.dart'; 8 | 9 | /// Service activator for JsonDecodeService 10 | final $JsonDecodeServiceActivator = $getJsonDecodeServiceActivator(); 11 | -------------------------------------------------------------------------------- /example/lib/json_decode_service.dart: -------------------------------------------------------------------------------- 1 | // This example uses https://github.com/d-markey/squadron_builder 2 | 3 | import 'dart:async'; 4 | import 'dart:convert' show json; 5 | 6 | import 'package:squadron/squadron.dart'; 7 | import 'package:squadron/squadron_annotations.dart'; 8 | 9 | import 'json_decode_service.activator.g.dart'; 10 | 11 | part 'json_decode_service.worker.g.dart'; 12 | 13 | @SquadronService( 14 | // disable web to keep the number of generated files low for this example 15 | web: false, 16 | ) 17 | class JsonDecodeService { 18 | @SquadronMethod() 19 | Future jsonDecode(String source) async => json.decode(source); 20 | } 21 | -------------------------------------------------------------------------------- /example/lib/json_decode_service.vm.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | // ************************************************************************** 4 | // Generator: WorkerGenerator 2.4.2 5 | // ************************************************************************** 6 | 7 | import 'package:squadron/squadron.dart'; 8 | 9 | import 'json_decode_service.dart'; 10 | 11 | /// VM entry point for JsonDecodeService 12 | void _start$JsonDecodeService(List command) => 13 | run($JsonDecodeServiceInitializer, command, null); 14 | 15 | EntryPoint $getJsonDecodeServiceActivator() => _start$JsonDecodeService; 16 | -------------------------------------------------------------------------------- /example/lib/json_serializable.chopper.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'json_serializable.dart'; 4 | 5 | // ************************************************************************** 6 | // ChopperGenerator 7 | // ************************************************************************** 8 | 9 | // coverage:ignore-file 10 | // ignore_for_file: type=lint 11 | final class _$MyService extends MyService { 12 | _$MyService([ChopperClient? client]) { 13 | if (client == null) return; 14 | this.client = client; 15 | } 16 | 17 | @override 18 | final Type definitionType = MyService; 19 | 20 | @override 21 | Future> getResource(String id) { 22 | final Uri $url = Uri.parse('/resources/${id}/'); 23 | final Request $request = Request( 24 | 'GET', 25 | $url, 26 | client.baseUrl, 27 | ); 28 | return client.send($request); 29 | } 30 | 31 | @override 32 | Future>> getResources() { 33 | final Uri $url = Uri.parse('/resources/all'); 34 | final Map $headers = { 35 | 'test': 'list', 36 | }; 37 | final Request $request = Request( 38 | 'GET', 39 | $url, 40 | client.baseUrl, 41 | headers: $headers, 42 | ); 43 | return client.send, Resource>($request); 44 | } 45 | 46 | @override 47 | Future>> getMapResource(String id) { 48 | final Uri $url = Uri.parse('/resources/'); 49 | final Map $params = {'id': id}; 50 | final Request $request = Request( 51 | 'GET', 52 | $url, 53 | client.baseUrl, 54 | parameters: $params, 55 | ); 56 | return client.send, Map>($request); 57 | } 58 | 59 | @override 60 | Future> getTypedResource() { 61 | final Uri $url = Uri.parse('/resources/'); 62 | final Map $headers = { 63 | 'foo': 'bar', 64 | }; 65 | final Request $request = Request( 66 | 'GET', 67 | $url, 68 | client.baseUrl, 69 | headers: $headers, 70 | ); 71 | return client.send($request); 72 | } 73 | 74 | @override 75 | Future> newResource( 76 | Resource resource, { 77 | String? name, 78 | }) { 79 | final Uri $url = Uri.parse('/resources'); 80 | final Map $headers = { 81 | if (name != null) 'name': name, 82 | }; 83 | final $body = resource; 84 | final Request $request = Request( 85 | 'POST', 86 | $url, 87 | client.baseUrl, 88 | body: $body, 89 | headers: $headers, 90 | ); 91 | return client.send($request); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /example/lib/json_serializable.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:chopper/chopper.dart'; 4 | import 'package:json_annotation/json_annotation.dart'; 5 | 6 | part 'json_serializable.chopper.dart'; 7 | part 'json_serializable.g.dart'; 8 | 9 | @JsonSerializable() 10 | class Resource { 11 | final String id; 12 | final String name; 13 | 14 | Resource(this.id, this.name); 15 | 16 | static const fromJsonFactory = _$ResourceFromJson; 17 | 18 | Map toJson() => _$ResourceToJson(this); 19 | 20 | @override 21 | String toString() => 'Resource{id: $id, name: $name}'; 22 | } 23 | 24 | @JsonSerializable() 25 | class ResourceError { 26 | final String type; 27 | final String message; 28 | 29 | ResourceError(this.type, this.message); 30 | 31 | static const fromJsonFactory = _$ResourceErrorFromJson; 32 | 33 | Map toJson() => _$ResourceErrorToJson(this); 34 | } 35 | 36 | @ChopperApi(baseUrl: '/resources') 37 | abstract class MyService extends ChopperService { 38 | static MyService create([ChopperClient? client]) => _$MyService(client); 39 | 40 | @GET(path: '/{id}/') 41 | Future getResource(@Path() String id); 42 | 43 | @GET(path: '/all', headers: {'test': 'list'}) 44 | Future>> getResources(); 45 | 46 | @GET(path: '/') 47 | Future> getMapResource(@Query() String id); 48 | 49 | @GET(path: '/', headers: {'foo': 'bar'}) 50 | Future> getTypedResource(); 51 | 52 | @POST() 53 | Future> newResource( 54 | @Body() Resource resource, { 55 | @Header() String? name, 56 | }); 57 | } 58 | -------------------------------------------------------------------------------- /example/lib/json_serializable.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'json_serializable.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | Resource _$ResourceFromJson(Map json) => Resource( 10 | json['id'] as String, 11 | json['name'] as String, 12 | ); 13 | 14 | Map _$ResourceToJson(Resource instance) => { 15 | 'id': instance.id, 16 | 'name': instance.name, 17 | }; 18 | 19 | ResourceError _$ResourceErrorFromJson(Map json) => 20 | ResourceError( 21 | json['type'] as String, 22 | json['message'] as String, 23 | ); 24 | 25 | Map _$ResourceErrorToJson(ResourceError instance) => 26 | { 27 | 'type': instance.type, 28 | 'message': instance.message, 29 | }; 30 | -------------------------------------------------------------------------------- /example/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: chopper_example 2 | description: Example usage of the Chopper package 3 | version: 0.0.6 4 | documentation: https://hadrien-lejard.gitbook.io/chopper/ 5 | #author: Hadrien Lejard 6 | 7 | environment: 8 | sdk: ^3.0.0 9 | 10 | dependencies: 11 | chopper: 12 | json_annotation: ^4.9.0 13 | built_value: 14 | analyzer: ^6.4.1 15 | http: ^1.1.0 16 | built_collection: ^5.1.1 17 | squadron: ^5.1.6 18 | 19 | dev_dependencies: 20 | build_runner: ^2.4.9 21 | chopper_generator: 22 | json_serializable: ^6.8.0 23 | built_value_generator: ^8.9.2 24 | lints: ^4.0.0 25 | squadron_builder: ^2.4.5 26 | 27 | dependency_overrides: 28 | chopper: 29 | path: ../chopper 30 | chopper_generator: 31 | path: ../chopper_generator 32 | -------------------------------------------------------------------------------- /flutter_favorite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lejard-h/chopper/2f026ca594ce18626af3b2344cfe3833a61cfe4b/flutter_favorite.png -------------------------------------------------------------------------------- /getting-started.md: -------------------------------------------------------------------------------- 1 | # Getting started 2 | 3 | ## How does Chopper work? 4 | 5 | Due to limitations to Dart on Flutter and the Web browser, Chopper doesn't use reflection but code generation with the help of the [build](https://pub.dev/packages/build) and [source\_gen](https://pub.dev/packages/source_gen) packages from the Dart Team. 6 | 7 | ## Installation 8 | 9 | Add the `chopper` and the `chopper_generator` packages to your project dependencies. 10 | 11 | ```yaml 12 | # pubspec.yaml 13 | 14 | dependencies: 15 | chopper: ^3.0.7 16 | 17 | dev_dependencies: 18 | build_runner: ^1.12.1 19 | chopper_generator: ^3.0.7 20 | ``` 21 | 22 | Run `pub get` to start using Chopper in your project. 23 | 24 | ## Define your API 25 | 26 | ### ChopperApi 27 | 28 | To define a client, use the `@ChopperApi` annotation on an abstract class that extends the `ChopperService` class. 29 | 30 | ```dart 31 | // YOUR_FILE.dart 32 | 33 | import "dart:async"; 34 | import 'package:chopper/chopper.dart'; 35 | 36 | // This is necessary for the generator to work. 37 | part "YOUR_FILE.chopper.dart"; 38 | 39 | @ChopperApi(baseUrl: "/todos") 40 | abstract class TodosListService extends ChopperService { 41 | 42 | // A helper method that helps instantiating the service. You can omit this method and use the generated class directly instead. 43 | static TodosListService create([ChopperClient? client]) => 44 | _$TodosListService(client); 45 | } 46 | ``` 47 | 48 | The `@ChopperApi` annotation takes one optional parameter - the `baseUrl` - that will prefix all the request's URLs defined in the class. 49 | 50 | > There's an exception from this behavior described in the [Requests](requests.md) section of the documentation. 51 | 52 | ### Defining a request 53 | 54 | Use one of the following annotations on abstract methods of a service class to define requests: 55 | 56 | * `@GET` 57 | 58 | * `@POST` 59 | 60 | * `@PUT` 61 | 62 | * `@PATCH` 63 | 64 | * `@DELETE` 65 | 66 | * `@HEAD` 67 | 68 | Request methods must return with values of the type `Future`, `Future>` or `Future`. 69 | The `Response` class is a wrapper around the HTTP response that contains the response body, the status code and the error (if any) of the request. 70 | This class can be omitted if only the response body is needed. When omitting the `Response` class, the request will throw an exception if the response status code is not in the range of `< 200` to ` > 300`. 71 | 72 | To define a `GET` request to the endpoint `/todos` in the service class above, add one of the following method declarations to the class: 73 | 74 | ```dart 75 | @GET() 76 | Future getTodos(); 77 | ``` 78 | 79 | or 80 | 81 | ```dart 82 | @GET() 83 | Future>> getTodos(); 84 | ``` 85 | 86 | or 87 | 88 | ```dart 89 | @GET() 90 | Future> getTodos(); 91 | ``` 92 | 93 | URL manipulation with dynamic path, and query parameters is also supported. To learn more about URL manipulation with Chopper, have a look at the [Requests](requests.md) section of the documentation. 94 | 95 | ## Defining a ChopperClient 96 | 97 | After defining one or more `ChopperService`s, you need to bind instances of them to a `ChopperClient`. The `ChopperClient` provides the base URL for every service and it is also responsible for applying [interceptors](interceptors.md) and [converters](converters/converters.md) on the requests it handles. 98 | 99 | ```dart 100 | import "dart:async"; 101 | import 'package:chopper/chopper.dart'; 102 | 103 | import 'YOUR_FILE.dart'; 104 | 105 | void main() async { 106 | final chopper = ChopperClient( 107 | baseUrl: "http://my-server:8000", 108 | services: [ 109 | // Create and pass an instance of the generated service to the client 110 | TodosListService.create() 111 | ], 112 | ); 113 | 114 | /// Get a reference to the client-bound service instance... 115 | final todosService = chopper.getService(); 116 | /// ... or create a new instance by explicitly binding it to a client. 117 | final anotherTodosService = TodosListService.create(chopper); 118 | 119 | /// Making a request is as easy as calling a function of the service. 120 | final response = await todosService.getTodos(); 121 | 122 | if (response.isSuccessful) { 123 | // Successful request 124 | final body = response.body; 125 | } else { 126 | // Error code received from server 127 | final code = response.statusCode; 128 | final error = response.error; 129 | } 130 | } 131 | ``` 132 | 133 | Handling I/O and other exceptions should be done by surrounding requests with `try-catch` blocks. 134 | -------------------------------------------------------------------------------- /interceptors.md: -------------------------------------------------------------------------------- 1 | # Interceptors 2 | 3 | ## **Request** 4 | 5 | Implement `Interceptor` class. 6 | 7 | {% hint style="info" %} 8 | Request interceptor are called just before sending request. 9 | {% endhint %} 10 | 11 | ```dart 12 | class MyRequestInterceptor implements Interceptor { 13 | 14 | MyRequestInterceptor(this.token); 15 | 16 | final String token; 17 | 18 | @override 19 | FutureOr> intercept(Chain chain) async { 20 | final request = applyHeader(chain.request, 'auth_token', 'Bearer $token'); 21 | return chain.proceed(request); 22 | } 23 | } 24 | ``` 25 | 26 | ## **Response** 27 | 28 | Implement `Interceptor` class. 29 | 30 | {% hint style="info" %} 31 | Called after successful or failed request. 32 | {% endhint %} 33 | 34 | ```dart 35 | class MyResponseInterceptor implements Interceptor { 36 | MyResponseInterceptor(this._token); 37 | 38 | String _token; 39 | 40 | @override 41 | FutureOr> intercept(Chain chain) async { 42 | final response = await chain.proceed(chain.request); 43 | _token = response.headers['auth_token']; 44 | return response; 45 | } 46 | } 47 | ``` 48 | 49 | ## Breaking out of an interceptor 50 | 51 | In some cases you may run into a case where it's not possible to continue within an interceptor and want to break out/cancel the request. This can be achieved by throwing an exception. 52 | This will not return a response and the request will not be executed. 53 | 54 | >Keep in mind that when throwing an exception you also need to handle/catch the exception in calling code. 55 | 56 | For example if you want to stop the request if the token is expired: 57 | 58 | ```dart 59 | class AuthInterceptor implements Interceptor { 60 | 61 | @override 62 | FutureOr> intercept(Chain chain) async { 63 | final request = applyHeader(chain.request, 'authorization', 64 | SharedPrefs.localStorage.getString(tokenHeader), 65 | override: false); 66 | 67 | final response = await chain.proceed(request); 68 | 69 | if (response?.statusCode == 401) { 70 | // Refreshing fails 71 | final bool isRefreshed = await _refreshToken(); 72 | if(!isRefreshed){ 73 | // Throw a exception to stop the request. 74 | throw Exception('Token expired'); 75 | } 76 | } 77 | 78 | return response; 79 | } 80 | } 81 | ``` 82 | 83 | It's not strictly needed to throw an exception in order to break out of the interceptor. 84 | Other construction can also be used depending on how the project is structured. 85 | Another could be calling a service that is injected or providing a callback that handles the state of the app. 86 | 87 | ## Builtins 88 | * [CurlInterceptor](https://pub.dev/documentation/chopper/latest/chopper/CurlInterceptor-class.html): Interceptor that prints curl commands for each execute request 89 | * [HeadersInterceptor](https://pub.dev/documentation/chopper/latest/chopper/HeadersInterceptor-class.html): Interceptor that adds headers to each request 90 | * [HttpLoggingInterceptor](https://pub.dev/documentation/chopper/latest/chopper/HttpLoggingInterceptor-class.html): Interceptor that logs request and response data 91 | 92 | Both the `CurlInterceptor` and `HttpLoggingInterceptor` use the dart [logging package](https://pub.dev/packages/logging). 93 | In order to see logging in console the logging package also needs to be added to your project and configured. 94 | 95 | For example: 96 | ```dart 97 | Logger.root.level = Level.ALL; // defaults to Level.INFO 98 | Logger.root.onRecord.listen((record) { 99 | print('${record.level.name}: ${record.time}: ${record.message}'); 100 | }); 101 | ``` 102 | 103 | -------------------------------------------------------------------------------- /mono_repo.yaml: -------------------------------------------------------------------------------- 1 | self_validate: analyze_and_format 2 | 3 | github: 4 | on: 5 | push: 6 | branches: 7 | - master 8 | - develop 9 | pull_request: 10 | branches: 11 | - master 12 | - develop 13 | env: 14 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 15 | 16 | merge_stages: 17 | - analyze_and_format 18 | - unit_test 19 | 20 | coverage_service: 21 | - codecov 22 | -------------------------------------------------------------------------------- /tool/ci.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Created with package:mono_repo v6.6.1 3 | 4 | # Support built in commands on windows out of the box. 5 | 6 | # When it is a flutter repo (check the pubspec.yaml for "sdk: flutter") 7 | # then "flutter pub" is called instead of "dart pub". 8 | # This assumes that the Flutter SDK has been installed in a previous step. 9 | function pub() { 10 | if grep -Fq "sdk: flutter" "${PWD}/pubspec.yaml"; then 11 | command flutter pub "$@" 12 | else 13 | command dart pub "$@" 14 | fi 15 | } 16 | 17 | function format() { 18 | command dart format "$@" 19 | } 20 | 21 | # When it is a flutter repo (check the pubspec.yaml for "sdk: flutter") 22 | # then "flutter analyze" is called instead of "dart analyze". 23 | # This assumes that the Flutter SDK has been installed in a previous step. 24 | function analyze() { 25 | if grep -Fq "sdk: flutter" "${PWD}/pubspec.yaml"; then 26 | command flutter analyze "$@" 27 | else 28 | command dart analyze "$@" 29 | fi 30 | } 31 | 32 | if [[ -z ${PKGS} ]]; then 33 | echo -e '\033[31mPKGS environment variable must be set! - TERMINATING JOB\033[0m' 34 | exit 64 35 | fi 36 | 37 | if [[ "$#" == "0" ]]; then 38 | echo -e '\033[31mAt least one task argument must be provided! - TERMINATING JOB\033[0m' 39 | exit 64 40 | fi 41 | 42 | SUCCESS_COUNT=0 43 | declare -a FAILURES 44 | 45 | for PKG in ${PKGS}; do 46 | echo -e "\033[1mPKG: ${PKG}\033[22m" 47 | EXIT_CODE=0 48 | pushd "${PKG}" >/dev/null || EXIT_CODE=$? 49 | 50 | if [[ ${EXIT_CODE} -ne 0 ]]; then 51 | echo -e "\033[31mPKG: '${PKG}' does not exist - TERMINATING JOB\033[0m" 52 | exit 64 53 | fi 54 | 55 | dart pub upgrade || EXIT_CODE=$? 56 | 57 | if [[ ${EXIT_CODE} -ne 0 ]]; then 58 | echo -e "\033[31mPKG: ${PKG}; 'dart pub upgrade' - FAILED (${EXIT_CODE})\033[0m" 59 | FAILURES+=("${PKG}; 'dart pub upgrade'") 60 | else 61 | for TASK in "$@"; do 62 | EXIT_CODE=0 63 | echo 64 | echo -e "\033[1mPKG: ${PKG}; TASK: ${TASK}\033[22m" 65 | case ${TASK} in 66 | analyze) 67 | echo 'dart analyze --fatal-infos .' 68 | dart analyze --fatal-infos . || EXIT_CODE=$? 69 | ;; 70 | format) 71 | echo 'dart format --output=none --set-exit-if-changed .' 72 | dart format --output=none --set-exit-if-changed . || EXIT_CODE=$? 73 | ;; 74 | test) 75 | echo 'dart test -p chrome' 76 | dart test -p chrome || EXIT_CODE=$? 77 | ;; 78 | test_with_coverage) 79 | echo 'dart pub global run coverage:test_with_coverage' 80 | dart pub global run coverage:test_with_coverage || EXIT_CODE=$? 81 | ;; 82 | *) 83 | echo -e "\033[31mUnknown TASK '${TASK}' - TERMINATING JOB\033[0m" 84 | exit 64 85 | ;; 86 | esac 87 | 88 | if [[ ${EXIT_CODE} -ne 0 ]]; then 89 | echo -e "\033[31mPKG: ${PKG}; TASK: ${TASK} - FAILED (${EXIT_CODE})\033[0m" 90 | FAILURES+=("${PKG}; TASK: ${TASK}") 91 | else 92 | echo -e "\033[32mPKG: ${PKG}; TASK: ${TASK} - SUCCEEDED\033[0m" 93 | SUCCESS_COUNT=$((SUCCESS_COUNT + 1)) 94 | fi 95 | 96 | done 97 | fi 98 | 99 | echo 100 | echo -e "\033[32mSUCCESS COUNT: ${SUCCESS_COUNT}\033[0m" 101 | 102 | if [ ${#FAILURES[@]} -ne 0 ]; then 103 | echo -e "\033[31mFAILURES: ${#FAILURES[@]}\033[0m" 104 | for i in "${FAILURES[@]}"; do 105 | echo -e "\033[31m $i\033[0m" 106 | done 107 | fi 108 | 109 | popd >/dev/null || exit 70 110 | echo 111 | done 112 | 113 | if [ ${#FAILURES[@]} -ne 0 ]; then 114 | exit 1 115 | fi 116 | -------------------------------------------------------------------------------- /tool/compare_versions.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io' show exitCode, stderr, stdout; 2 | import 'package:cli_script/cli_script.dart' show wrapMain; 3 | import 'package:pub_semver/pub_semver.dart' show Version; 4 | 5 | void main(List args) { 6 | wrapMain(() { 7 | exitCode = 0; 8 | 9 | if (args.length != 2) { 10 | stderr.write( 11 | 'Please provide two arguments!\n\nExample usage:\ndart run compare_versions.dart 2.0.0+1 1.9.0+5\n', 12 | ); 13 | exitCode = 1; 14 | return; 15 | } 16 | 17 | late final Version v1; 18 | late final Version v2; 19 | 20 | try { 21 | v1 = Version.parse(args[0]); 22 | } on FormatException catch (e) { 23 | stderr.write('Error parsing version 1: ${e.message}'); 24 | exitCode = 1; 25 | return; 26 | } 27 | 28 | try { 29 | v2 = Version.parse(args[1]); 30 | } on FormatException catch (e) { 31 | stderr.write('Error parsing version 2: ${e.message}'); 32 | exitCode = 1; 33 | return; 34 | } 35 | 36 | stdout.write(v1 > v2 ? 1 : 0); 37 | }); 38 | } 39 | -------------------------------------------------------------------------------- /tool/makefile_helpers.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Opens a link with the default browser of OS (It works cross-platform) 4 | # 5 | ## You can call it like `open_link balad.ir` to open balad website on your default browser 6 | open_link() { 7 | case "$(uname -s)" in 8 | Darwin) 9 | # macOS 10 | open "$1" 11 | ;; 12 | 13 | Linux) 14 | # Linux: 15 | xdg-open "$1" 16 | ;; 17 | 18 | CYGWIN* | MINGW32* | MSYS* | MINGW*) 19 | # Windows 20 | start "$1" 21 | ;; 22 | 23 | *) 24 | echo 'Not supported OS' 25 | ;; 26 | esac 27 | } 28 | -------------------------------------------------------------------------------- /tool/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: compare_versions 2 | 3 | publish_to: 'none' 4 | 5 | version: 1.0.1 6 | 7 | environment: 8 | sdk: ">=2.17.0 <4.0.0" 9 | 10 | dependencies: 11 | cli_script: ^1.0.0 12 | pub_semver: ^2.1.4 13 | --------------------------------------------------------------------------------