├── .code-samples.meilisearch.yaml ├── .editorconfig ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── config.yml │ └── feature_request.md ├── dependabot.yml ├── release-draft-template.yml ├── scripts │ └── check-release.sh └── workflows │ ├── pre-release-tests.yml │ ├── publish.yml │ ├── release-drafter.yml │ └── tests.yml ├── .gitignore ├── .yamllint.yml ├── CONTRIBUTING.md ├── Cargo.toml ├── LICENSE ├── README.md ├── README.tpl ├── bors.toml ├── docker-compose.yml ├── examples ├── cli-app-with-awc │ ├── Cargo.toml │ ├── assets │ │ └── clothes.json │ └── src │ │ └── main.rs ├── cli-app │ ├── Cargo.toml │ ├── assets │ │ └── clothes.json │ └── src │ │ └── main.rs ├── settings.rs ├── web_app │ ├── Cargo.toml │ ├── README.md │ ├── pkg │ │ └── index.html │ └── src │ │ ├── document.rs │ │ └── lib.rs └── web_app_graphql │ ├── .env.example.txt │ ├── .gitignore │ ├── Cargo.toml │ ├── README.md │ ├── diesel.toml │ ├── migrations │ ├── .keep │ ├── 00000000000000_diesel_initial_setup │ │ ├── down.sql │ │ └── up.sql │ └── 2023-12-20-105441_users │ │ ├── down.sql │ │ └── up.sql │ └── src │ ├── app_env_vars.rs │ ├── errors │ └── mod.rs │ ├── graphql_schema │ ├── mod.rs │ └── users │ │ ├── mod.rs │ │ ├── mutation │ │ ├── add_user.rs │ │ └── mod.rs │ │ └── query │ │ ├── get_users.rs │ │ ├── mod.rs │ │ └── search.rs │ ├── lib.rs │ ├── main.rs │ ├── models.rs │ └── schema.rs ├── meilisearch-index-setting-macro ├── Cargo.toml └── src │ └── lib.rs ├── meilisearch-test-macro ├── Cargo.toml ├── README.md └── src │ └── lib.rs ├── scripts ├── check-readme.sh ├── update-readme.sh └── update_macro_versions.sh └── src ├── client.rs ├── documents.rs ├── dumps.rs ├── errors.rs ├── features.rs ├── indexes.rs ├── key.rs ├── lib.rs ├── request.rs ├── reqwest.rs ├── search.rs ├── settings.rs ├── snapshots.rs ├── task_info.rs ├── tasks.rs ├── tenant_tokens.rs └── utils.rs /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | end_of_line = lf 9 | indent_style = space 10 | 11 | [*.rs] 12 | indent_style = space 13 | indent_size = 4 14 | 15 | [*.{yml,yaml}] 16 | indent_size = 2 17 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report 🐞 3 | about: Create a report to help us improve. 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | --- 8 | 9 | 10 | 11 | **Description** 12 | Description of what the bug is about. 13 | 14 | **Expected behavior** 15 | What you expected to happen. 16 | 17 | **Current behavior** 18 | What happened. 19 | 20 | **Screenshots or Logs** 21 | If applicable, add screenshots or logs to help explain your problem. 22 | 23 | **Environment (please complete the following information):** 24 | - OS: [e.g. Debian GNU/Linux] 25 | - Meilisearch version: [e.g. v.0.20.0] 26 | - meilisearch-rust version: [e.g v0.20.1] 27 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Support questions & other 4 | url: https://discord.meilisearch.com/ 5 | about: Support is not handled here but on our Discord 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature Request & Enhancement 💡 3 | about: Suggest a new idea for the project. 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | --- 8 | 9 | 10 | 11 | **Description** 12 | Brief explanation of the feature. 13 | 14 | **Basic example** 15 | If the proposal involves something new or a change, include a basic example. How would you use the feature? In which context? 16 | 17 | **Other** 18 | Any other things you want to add. 19 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "monthly" 7 | labels: 8 | - 'skip-changelog' 9 | - 'dependencies' 10 | rebase-strategy: disabled 11 | 12 | - package-ecosystem: cargo 13 | directory: "/" 14 | schedule: 15 | interval: "monthly" 16 | time: "04:00" 17 | open-pull-requests-limit: 10 18 | labels: 19 | - skip-changelog 20 | - dependencies 21 | rebase-strategy: disabled 22 | -------------------------------------------------------------------------------- /.github/release-draft-template.yml: -------------------------------------------------------------------------------- 1 | name-template: 'v$RESOLVED_VERSION 🦀' 2 | tag-template: 'v$RESOLVED_VERSION' 3 | exclude-labels: 4 | - 'skip-changelog' 5 | version-resolver: 6 | minor: 7 | labels: 8 | - 'breaking-change' 9 | default: patch 10 | categories: 11 | - title: '⚠️ Breaking changes' 12 | label: 'breaking-change' 13 | - title: '🚀 Enhancements' 14 | label: 'enhancement' 15 | - title: '🐛 Bug Fixes' 16 | label: 'bug' 17 | - title: '🔒 Security' 18 | label: 'security' 19 | - title: '⚙️ Maintenance/misc' 20 | label: 21 | - 'maintenance' 22 | - 'documentation' 23 | template: | 24 | $CHANGES 25 | 26 | Thanks again to $CONTRIBUTORS! 🎉 27 | no-changes-template: 'Changes are coming soon 😎' 28 | sort-direction: 'ascending' 29 | replacers: 30 | - search: '/(?:and )?@dependabot-preview(?:\[bot\])?,?/g' 31 | replace: '' 32 | - search: '/(?:and )?@dependabot(?:\[bot\])?,?/g' 33 | replace: '' 34 | - search: '/(?:and )?@bors(?:\[bot\])?,?/g' 35 | replace: '' 36 | - search: '/(?:and )?@meili-bot(?:\[bot\])?,?/g' 37 | replace: '' 38 | - search: '/(?:and )?@meili-bors(?:\[bot\])?,?/g' 39 | replace: '' 40 | -------------------------------------------------------------------------------- /.github/scripts/check-release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Checking if current tag matches the package version 4 | current_tag=$(echo "$GITHUB_REF" | cut -d '/' -f 3 | sed -r 's/^v//') 5 | 6 | file1='Cargo.toml' 7 | file2='README.tpl' 8 | file3='.code-samples.meilisearch.yaml' 9 | file4='README.md' 10 | file5='./meilisearch-index-setting-macro/Cargo.toml' 11 | 12 | file_tag1=$(grep '^version = ' $file1 | cut -d '=' -f 2 | tr -d '"' | tr -d ' ') 13 | file_tag_1_1=$(grep '{ path = "meilisearch-index-setting-macro", version =' $file1 | grep -Eo '[0-9]+.[0-9]+.[0-9]+') 14 | file_tag2=$(grep 'meilisearch-sdk = ' $file2 | cut -d '=' -f 2 | tr -d '"' | tr -d ' ') 15 | file_tag3=$(grep 'meilisearch-sdk = ' $file3 | cut -d '=' -f 2 | tr -d '"' | tr -d ' ') 16 | file_tag4=$(grep 'meilisearch-sdk = ' $file4 | cut -d '=' -f 2 | tr -d '"' | tr -d ' ') 17 | file_tag5=$(grep '^version = ' $file5 | grep -Eo '[0-9]+.[0-9]+.[0-9]+') 18 | 19 | if [ "$current_tag" != "$file_tag1" ] || 20 | [ "$current_tag" != "$file_tag_1_1" ] || 21 | [ "$current_tag" != "$file_tag2" ] || 22 | [ "$current_tag" != "$file_tag3" ] || 23 | [ "$current_tag" != "$file_tag4" ] || 24 | [ "$current_tag" != "$file_tag5" ] \ 25 | ; then 26 | echo "Error: the current tag does not match the version in package file(s)." 27 | echo "$file1: found $file_tag1 - expected $current_tag" 28 | echo "$file1: found $file_tag_1_1 - expected $current_tag" 29 | echo "$file2: found $file_tag2 - expected $current_tag" 30 | echo "$file3: found $file_tag3 - expected $current_tag" 31 | echo "$file4: found $file_tag4 - expected $current_tag" 32 | echo "$file5: found $file_tag5 - expected $current_tag" 33 | exit 1 34 | fi 35 | 36 | echo 'OK' 37 | exit 0 38 | -------------------------------------------------------------------------------- /.github/workflows/pre-release-tests.yml: -------------------------------------------------------------------------------- 1 | # Testing the code base against the Meilisearch pre-releases 2 | name: Pre-Release Tests 3 | 4 | # Will only run for PRs and pushes to bump-meilisearch-v* 5 | on: 6 | push: 7 | branches: bump-meilisearch-v* 8 | pull_request: 9 | branches: bump-meilisearch-v* 10 | 11 | env: 12 | CARGO_TERM_COLOR: always 13 | 14 | jobs: 15 | integration_tests: 16 | name: integration-tests-against-rc 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v4 20 | - name: Build 21 | run: cargo build --verbose 22 | - name: Get the latest Meilisearch RC 23 | run: echo "MEILISEARCH_VERSION=$(curl https://raw.githubusercontent.com/meilisearch/integration-guides/main/scripts/get-latest-meilisearch-rc.sh | bash)" >> $GITHUB_ENV 24 | - name: Meilisearch (${{ env.MEILISEARCH_VERSION }}) setup with Docker 25 | run: docker run -d -p 7700:7700 getmeili/meilisearch:${{ env.MEILISEARCH_VERSION }} meilisearch --master-key=masterKey --no-analytics 26 | - name: Run tests 27 | run: cargo test --verbose -- --test-threads=1 28 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish to crates.io 2 | on: 3 | push: 4 | tags: 5 | - v* 6 | 7 | jobs: 8 | publish: 9 | name: Rust project 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | - uses: hecrj/setup-rust-action@master 14 | with: 15 | rust-version: stable 16 | - name: Check release validity 17 | run: sh .github/scripts/check-release.sh 18 | - name: Build meilisearch crate 19 | run: cargo build --release 20 | - name: Build meilisearch crate 21 | run: |- 22 | cd meilisearch-index-setting-macro 23 | cargo build --release 24 | - name: Login 25 | run: cargo login ${{ secrets.CRATES_TOKEN }} 26 | - name: Publish meilisearch-index-setting-macro crate to crates.io 27 | run: |- 28 | cd meilisearch-index-setting-macro 29 | cargo publish 30 | - name: Publish meilisearch crate to crates.io 31 | run: cargo publish 32 | -------------------------------------------------------------------------------- /.github/workflows/release-drafter.yml: -------------------------------------------------------------------------------- 1 | name: Release Drafter 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | update_release_draft: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: release-drafter/release-drafter@v6 13 | with: 14 | config-name: release-draft-template.yml 15 | env: 16 | GITHUB_TOKEN: ${{ secrets.RELEASE_DRAFTER_TOKEN }} 17 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | pull_request: 5 | push: 6 | # trying and staging branches are for BORS config 7 | branches: 8 | - trying 9 | - staging 10 | - main 11 | 12 | env: 13 | CARGO_TERM_COLOR: always 14 | 15 | jobs: 16 | integration_tests: 17 | # Will not run if the event is a PR to bump-meilisearch-v* (so a pre-release PR) 18 | # Will still run for each push to bump-meilisearch-v* 19 | if: github.event_name != 'pull_request' || !startsWith(github.base_ref, 'bump-meilisearch-v') 20 | runs-on: ubuntu-latest 21 | name: integration-tests 22 | steps: 23 | - uses: actions/checkout@v4 24 | - name: Build 25 | run: cargo build --verbose 26 | - name: Meilisearch (latest version) setup with Docker 27 | run: docker run -d -p 7700:7700 getmeili/meilisearch:latest meilisearch --no-analytics --master-key=masterKey 28 | - name: Run tests 29 | run: cargo test --verbose 30 | - name: Cargo check 31 | uses: actions-rs/cargo@v1 32 | with: 33 | command: check 34 | args: --workspace --all-targets --all 35 | - name: Cargo check no default features 36 | uses: actions-rs/cargo@v1 37 | with: 38 | command: check 39 | args: --workspace --all-targets --all --no-default-features 40 | 41 | linter: 42 | name: clippy-check 43 | runs-on: ubuntu-latest 44 | steps: 45 | - uses: actions/checkout@v4 46 | - name: Install clippy 47 | run: rustup component add clippy 48 | - name: Run linter (clippy) 49 | # Will fail when encountering warnings 50 | run: cargo clippy -- -D warnings 51 | 52 | formatter: 53 | name: rust-format 54 | runs-on: ubuntu-latest 55 | steps: 56 | - uses: actions/checkout@v4 57 | - name: Run formatter 58 | run: cargo fmt --all -- --check 59 | 60 | readme_check: 61 | name: readme-check 62 | runs-on: ubuntu-latest 63 | steps: 64 | - uses: actions/checkout@v4 65 | - name: Check the README.md file is up-to-date 66 | run: sh scripts/check-readme.sh 67 | 68 | wasm_build: 69 | name: wasm-build 70 | runs-on: ubuntu-latest 71 | steps: 72 | - uses: actions/checkout@v4 73 | - name: Build 74 | run: | 75 | rustup target add wasm32-unknown-unknown 76 | cargo check -p web_app --target wasm32-unknown-unknown 77 | yaml-lint: 78 | name: Yaml linting check 79 | runs-on: ubuntu-latest 80 | steps: 81 | - uses: actions/checkout@v4 82 | - name: Yaml lint check 83 | uses: ibiqlik/action-yamllint@v3 84 | with: 85 | config_file: .yamllint.yml 86 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | examples/web_app/target/* 4 | .vscode 5 | -------------------------------------------------------------------------------- /.yamllint.yml: -------------------------------------------------------------------------------- 1 | extends: default 2 | ignore: | 3 | node_modules 4 | rules: 5 | comments-indentation: disable 6 | line-length: disable 7 | document-start: disable 8 | brackets: disable 9 | truthy: disable 10 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | First of all, thank you for contributing to Meilisearch! The goal of this document is to provide everything you need to know in order to contribute to Meilisearch and its different integrations. 4 | 5 | - [Assumptions](#assumptions) 6 | - [How to Contribute](#how-to-contribute) 7 | - [Development Workflow](#development-workflow) 8 | - [Git Guidelines](#git-guidelines) 9 | - [Release Process (for internal team only)](#release-process-for-internal-team-only) 10 | 11 | 12 | ## Assumptions 13 | 14 | 1. **You're familiar with [GitHub](https://github.com) and the [Pull Request](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/about-pull-requests)(PR) workflow.** 15 | 2. **You've read the Meilisearch [documentation](https://www.meilisearch.com/docs) and the [README](/README.md).** 16 | 3. **You know about the [Meilisearch community](https://discord.com/invite/meilisearch). Please use this for help.** 17 | 18 | ## How to Contribute 19 | 20 | 1. Make sure that the contribution you want to make is explained or detailed in a GitHub issue! Find an [existing issue](https://github.com/meilisearch/meilisearch-rust/issues/) or [open a new one](https://github.com/meilisearch/meilisearch-rust/issues/new). 21 | 2. Once done, [fork the meilisearch-rust repository](https://help.github.com/en/github/getting-started-with-github/fork-a-repo) in your own GitHub account. Ask a maintainer if you want your issue to be checked before making a PR. 22 | 3. [Create a new Git branch](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/creating-and-deleting-branches-within-your-repository). 23 | 4. Review the [Development Workflow](#development-workflow) section that describes the steps to maintain the repository. 24 | 5. Make the changes on your branch. 25 | 6. [Submit the branch as a PR](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/creating-a-pull-request-from-a-fork) pointing to the `main` branch of the main meilisearch-rust repository. A maintainer should comment and/or review your Pull Request within a few days. Although depending on the circumstances, it may take longer.
26 | We do not enforce a naming convention for the PRs, but **please use something descriptive of your changes**, having in mind that the title of your PR will be automatically added to the next [release changelog](https://github.com/meilisearch/meilisearch-rust/releases/). 27 | 28 | ## Development Workflow 29 | 30 | You can set up your local environment natively or using `docker`, check out the [`docker-compose.yml`](/docker-compose.yml). 31 | 32 | Example of running all the checks with docker: 33 | ```bash 34 | docker-compose run --rm package bash -c "cargo test" 35 | ``` 36 | 37 | To install dependencies: 38 | 39 | ```bash 40 | cargo build --release 41 | ``` 42 | 43 | To ensure the same dependency versions in all environments, for example the CI, update the dependencies by running: `cargo update`. 44 | 45 | ### Tests 46 | 47 | To run the tests, run: 48 | 49 | ```bash 50 | # Tests 51 | curl -L https://install.meilisearch.com | sh # download Meilisearch 52 | ./meilisearch --master-key=masterKey --no-analytics # run Meilisearch 53 | cargo test 54 | ``` 55 | 56 | There are two kinds of tests, documentation tests and unit tests. 57 | If you need to write or read the unit tests you should consider reading this 58 | [readme](meilisearch-test-macro/README.md) about our custom testing macro. 59 | 60 | Also, the WASM example compilation should be checked: 61 | 62 | ```bash 63 | rustup target add wasm32-unknown-unknown 64 | cargo check -p web_app --target wasm32-unknown-unknown 65 | ``` 66 | 67 | Each PR should pass the tests to be accepted. 68 | 69 | ### Clippy 70 | 71 | Each PR should pass [`clippy`](https://github.com/rust-lang/rust-clippy) (the linter) to be accepted. 72 | 73 | ```bash 74 | cargo clippy -- -D warnings 75 | ``` 76 | 77 | If you don't have `clippy` installed on your machine yet, run: 78 | 79 | ```bash 80 | rustup update 81 | rustup component add clippy 82 | ``` 83 | 84 | ⚠️ Also, if you have installed `clippy` a long time ago, you might need to update it: 85 | 86 | ```bash 87 | rustup update 88 | ``` 89 | 90 | ### Fmt 91 | 92 | Each PR should pass the format test to be accepted. 93 | 94 | Run the following to fix the formatting errors: 95 | 96 | ``` 97 | cargo fmt 98 | ``` 99 | 100 | and the following to test if the formatting is correct: 101 | ``` 102 | cargo fmt --all -- --check 103 | ``` 104 | 105 | ### Update the README 106 | 107 | The README is generated. Please do not update manually the `README.md` file. 108 | 109 | Instead, update the `README.tpl` and `src/lib.rs` files, and run: 110 | 111 | ```sh 112 | sh scripts/update-readme.sh 113 | ``` 114 | 115 | Then, push the changed files. 116 | 117 | You can check the current `README.md` is up-to-date by running: 118 | 119 | ```sh 120 | sh scripts/check-readme.sh 121 | # To see the diff 122 | sh scripts/check-readme.sh --diff 123 | ``` 124 | 125 | If it's not, the CI will fail on your PR. 126 | 127 | ### Yaml lint 128 | 129 | To check if your `yaml` files are correctly formatted, you need to [install yamllint](https://yamllint.readthedocs.io/en/stable/quickstart.html#installing-yamllint) and then run `yamllint .` 130 | 131 | ## Git Guidelines 132 | 133 | ### Git Branches 134 | 135 | All changes must be made in a branch and submitted as PR. 136 | We do not enforce any branch naming style, but please use something descriptive of your changes. 137 | 138 | ### Git Commits 139 | 140 | As minimal requirements, your commit message should: 141 | - be capitalized 142 | - not finished by a dot or any other punctuation character (!,?) 143 | - start with a verb so that we can read your commit message this way: "This commit will ...", where "..." is the commit message. 144 | e.g.: "Fix the home page button" or "Add more tests for create_index method" 145 | 146 | We don't follow any other convention, but if you want to use one, we recommend [this one](https://chris.beams.io/posts/git-commit/). 147 | 148 | ### GitHub Pull Requests 149 | 150 | Some notes on GitHub PRs: 151 | 152 | - [Convert your PR as a draft](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/changing-the-stage-of-a-pull-request) if your changes are a work in progress: no one will review it until you pass your PR as ready for review.
153 | The draft PR can be very useful if you want to show that you are working on something and make your work visible. 154 | - The branch related to the PR must be **up-to-date with `main`** before merging. Fortunately, this project [integrates a bot](https://github.com/meilisearch/integration-guides/blob/main/resources/bors.md) to automatically enforce this requirement without the PR author having to do it manually. 155 | - All PRs must be reviewed and approved by at least one maintainer. 156 | - The PR title should be accurate and descriptive of the changes. The title of the PR will be indeed automatically added to the next [release changelogs](https://github.com/meilisearch/meilisearch-rust/releases/). 157 | 158 | ## Release Process (for the internal team only) 159 | 160 | Meilisearch tools follow the [Semantic Versioning Convention](https://semver.org/). 161 | 162 | ### Automation to Rebase and Merge the PRs 163 | 164 | This project integrates a bot that helps us manage pull requests merging.
165 | _[Read more about this](https://github.com/meilisearch/integration-guides/blob/main/resources/bors.md)._ 166 | 167 | ### Automated Changelogs 168 | 169 | This project integrates a tool to create automated changelogs.
170 | _[Read more about this](https://github.com/meilisearch/integration-guides/blob/main/resources/release-drafter.md)._ 171 | 172 | ### How to Publish the Release 173 | 174 | ⚠️ Before doing anything, make sure you get through the guide about [Releasing an Integration](https://github.com/meilisearch/integration-guides/blob/main/resources/integration-release.md). 175 | 176 | Make a PR modifying the file [`Cargo.toml`](/Cargo.toml): 177 | 178 | ```toml 179 | version = "X.X.X" 180 | ``` 181 | 182 | the [`README.tpl`](/README.tpl): 183 | 184 | ```rust 185 | //! meilisearch-sdk = "X.X.X" 186 | ``` 187 | 188 | and the [code-samples file](/.code-samples.meilisearch.yaml): 189 | 190 | ```yml 191 | meilisearch-sdk = "X.X.X" 192 | ``` 193 | 194 | with the right version. 195 | 196 | 197 | After the changes on `Cargo.toml`, run the following command: 198 | 199 | ``` 200 | sh scripts/update_macro_versions.sh 201 | ``` 202 | 203 | After the changes on `lib.rs`, run the following command: 204 | 205 | ```bash 206 | sh scripts/update-readme.sh 207 | ``` 208 | 209 | Once the changes are merged on `main`, you can publish the current draft release via the [GitHub interface](https://github.com/meilisearch/meilisearch-rust/releases): on this page, click on `Edit` (related to the draft release) > update the description (be sure you apply [these recommendations](https://github.com/meilisearch/integration-guides/blob/main/resources/integration-release.md#writting-the-release-description)) > when you are ready, click on `Publish release`. 210 | 211 | GitHub Actions will be triggered and push the package to [crates.io](https://crates.io/crates/meilisearch-sdk). 212 | 213 |
214 | 215 | Thank you again for reading this through. We cannot wait to begin to work with you if you make your way through this contributing guide ❤️ 216 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "meilisearch-sdk" 3 | version = "0.28.0" 4 | authors = ["Mubelotix "] 5 | edition = "2018" 6 | description = "Rust wrapper for the Meilisearch API. Meilisearch is a powerful, fast, open-source, easy to use and deploy search engine." 7 | license = "MIT" 8 | readme = "README.md" 9 | repository = "https://github.com/meilisearch/meilisearch-sdk" 10 | resolver = "2" 11 | 12 | [workspace] 13 | members = ["examples/*"] 14 | 15 | [dependencies] 16 | async-trait = "0.1.51" 17 | iso8601 = "0.6.1" 18 | log = "0.4" 19 | serde = { version = "1.0", features = ["derive"] } 20 | serde_json = "1.0" 21 | time = { version = "0.3.7", features = ["serde-well-known", "formatting", "parsing"] } 22 | yaup = "0.3.1" 23 | either = { version = "1.8.0", features = ["serde"] } 24 | thiserror = "1.0.37" 25 | meilisearch-index-setting-macro = { path = "meilisearch-index-setting-macro", version = "0.28.0" } 26 | pin-project-lite = { version = "0.2.13", optional = true } 27 | reqwest = { version = "0.12.3", optional = true, default-features = false, features = ["rustls-tls", "http2", "stream"] } 28 | bytes = { version = "1.6", optional = true } 29 | uuid = { version = "1.1.2", features = ["v4"] } 30 | futures-io = "0.3.30" 31 | futures = "0.3" 32 | 33 | [target.'cfg(not(target_arch = "wasm32"))'.dependencies] 34 | jsonwebtoken = { version = "9", default-features = false } 35 | 36 | [target.'cfg(target_arch = "wasm32")'.dependencies] 37 | uuid = { version = "1.8.0", default-features = false, features = ["v4", "js"] } 38 | web-sys = "0.3" 39 | wasm-bindgen-futures = "0.4" 40 | 41 | [features] 42 | default = ["reqwest"] 43 | reqwest = ["dep:reqwest", "pin-project-lite", "bytes"] 44 | futures-unsend = [] 45 | 46 | [dev-dependencies] 47 | futures-await-test = "0.3" 48 | futures = "0.3" 49 | mockito = "1.0.0" 50 | meilisearch-test-macro = { path = "meilisearch-test-macro" } 51 | tokio = { version = "1", features = ["rt", "macros"] } 52 | 53 | # The following dependencies are required for examples 54 | wasm-bindgen = "0.2" 55 | wasm-bindgen-futures = "0.4" 56 | yew = "0.21" 57 | lazy_static = "1.4" 58 | web-sys = "0.3" 59 | console_error_panic_hook = "0.1" 60 | big_s = "1.0.2" 61 | insta = "1.38.0" 62 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020-2025 Meili SAS 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |

6 | Meilisearch-Rust 7 |

8 | 9 |

Meilisearch Rust SDK

10 | 11 |

12 | Meilisearch | 13 | Meilisearch Cloud | 14 | Documentation | 15 | Discord | 16 | Roadmap | 17 | Website | 18 | FAQ 19 |

20 | 21 |

22 | crates.io 23 | Tests 24 | License 25 | 26 | Bors enabled 27 |

28 | 29 |

⚡ The Meilisearch API client written for Rust 🦀

30 | 31 | **Meilisearch Rust** is the Meilisearch API client for Rust developers. 32 | 33 | **Meilisearch** is an open-source search engine. [Learn more about Meilisearch.](https://github.com/meilisearch/meilisearch) 34 | 35 | ## Table of Contents 36 | 37 | - [📖 Documentation](#-documentation) 38 | - [🔧 Installation](#-installation) 39 | - [🚀 Getting started](#-getting-started) 40 | - [🌐 Running in the Browser with WASM](#-running-in-the-browser-with-wasm) 41 | - [🤖 Compatibility with Meilisearch](#-compatibility-with-meilisearch) 42 | - [⚙️ Contributing](#️-contributing) 43 | 44 | ## 📖 Documentation 45 | 46 | This readme contains all the documentation you need to start using this Meilisearch SDK. 47 | 48 | For general information on how to use Meilisearch—such as our API reference, tutorials, guides, and in-depth articles—refer to our [main documentation website](https://www.meilisearch.com/docs). 49 | 50 | ## 🔧 Installation 51 | 52 | To use `meilisearch-sdk`, add this to your `Cargo.toml`: 53 | 54 | ```toml 55 | [dependencies] 56 | meilisearch-sdk = "0.28.0" 57 | ``` 58 | 59 | The following optional dependencies may also be useful: 60 | 61 | ```toml 62 | futures = "0.3" # To be able to block on async functions if you are not using an async runtime 63 | serde = { version = "1.0", features = ["derive"] } 64 | ``` 65 | 66 | This crate is `async` but you can choose to use an async runtime like [tokio](https://crates.io/crates/tokio) or just [block on futures](https://docs.rs/futures/latest/futures/executor/fn.block_on.html). 67 | You can enable the `sync` feature to make most structs `Sync`. It may be a bit slower. 68 | 69 | Using this crate is possible without [serde](https://crates.io/crates/serde), but a lot of features require serde. 70 | 71 | ### Run Meilisearch 72 | 73 | ⚡️ **Launch, scale, and streamline in minutes with Meilisearch Cloud**—no maintenance, no commitment, cancel anytime. [Try it free now](https://cloud.meilisearch.com/login?utm_campaign=oss&utm_source=github&utm_medium=meilisearch-rust). 74 | 75 | 🪨 Prefer to self-host? [Download and deploy](https://www.meilisearch.com/docs/learn/self_hosted/getting_started_with_self_hosted_meilisearch?utm_campaign=oss&utm_source=github&utm_medium=meilisearch-rust) our fast, open-source search engine on your own infrastructure. 76 | 77 | ## 🚀 Getting started 78 | 79 | #### Add Documents 80 | 81 | ```rust 82 | use meilisearch_sdk::client::*; 83 | use serde::{Serialize, Deserialize}; 84 | use futures::executor::block_on; 85 | 86 | #[derive(Serialize, Deserialize, Debug)] 87 | struct Movie { 88 | id: usize, 89 | title: String, 90 | genres: Vec, 91 | } 92 | 93 | 94 | #[tokio::main(flavor = "current_thread")] 95 | async fn main() { 96 | // Create a client (without sending any request so that can't fail) 97 | let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); 98 | 99 | // An index is where the documents are stored. 100 | let movies = client.index("movies"); 101 | 102 | // Add some movies in the index. If the index 'movies' does not exist, Meilisearch creates it when you first add the documents. 103 | movies.add_documents(&[ 104 | Movie { id: 1, title: String::from("Carol"), genres: vec!["Romance".to_string(), "Drama".to_string()] }, 105 | Movie { id: 2, title: String::from("Wonder Woman"), genres: vec!["Action".to_string(), "Adventure".to_string()] }, 106 | Movie { id: 3, title: String::from("Life of Pi"), genres: vec!["Adventure".to_string(), "Drama".to_string()] }, 107 | Movie { id: 4, title: String::from("Mad Max"), genres: vec!["Adventure".to_string(), "Science Fiction".to_string()] }, 108 | Movie { id: 5, title: String::from("Moana"), genres: vec!["Fantasy".to_string(), "Action".to_string()] }, 109 | Movie { id: 6, title: String::from("Philadelphia"), genres: vec!["Drama".to_string()] }, 110 | ], Some("id")).await.unwrap(); 111 | } 112 | ``` 113 | 114 | With the `uid`, you can check the status (`enqueued`, `canceled`, `processing`, `succeeded` or `failed`) of your documents addition using the [task](https://www.meilisearch.com/docs/reference/api/tasks#get-task). 115 | 116 | #### Basic Search 117 | 118 | ```rust 119 | // Meilisearch is typo-tolerant: 120 | println!("{:?}", client.index("movies_2").search().with_query("caorl").execute::().await.unwrap().hits); 121 | ``` 122 | 123 | Output: 124 | ``` 125 | [Movie { id: 1, title: String::from("Carol"), genres: vec!["Romance", "Drama"] }] 126 | ``` 127 | 128 | Json output: 129 | ```json 130 | { 131 | "hits": [{ 132 | "id": 1, 133 | "title": "Carol", 134 | "genres": ["Romance", "Drama"] 135 | }], 136 | "offset": 0, 137 | "limit": 10, 138 | "processingTimeMs": 1, 139 | "query": "caorl" 140 | } 141 | ``` 142 | 143 | #### Custom Search 144 | 145 | ```rust 146 | let search_result = client.index("movies_3") 147 | .search() 148 | .with_query("phil") 149 | .with_attributes_to_highlight(Selectors::Some(&["*"])) 150 | .execute::() 151 | .await 152 | .unwrap(); 153 | println!("{:?}", search_result.hits); 154 | ``` 155 | 156 | Json output: 157 | ```json 158 | { 159 | "hits": [ 160 | { 161 | "id": 6, 162 | "title": "Philadelphia", 163 | "_formatted": { 164 | "id": 6, 165 | "title": "Philadelphia", 166 | "genre": ["Drama"] 167 | } 168 | } 169 | ], 170 | "offset": 0, 171 | "limit": 20, 172 | "processingTimeMs": 0, 173 | "query": "phil" 174 | } 175 | ``` 176 | 177 | #### Custom Search With Filters 178 | 179 | If you want to enable filtering, you must add your attributes to the `filterableAttributes` 180 | index setting. 181 | 182 | ```rust 183 | let filterable_attributes = [ 184 | "id", 185 | "genres", 186 | ]; 187 | client.index("movies_4").set_filterable_attributes(&filterable_attributes).await.unwrap(); 188 | ``` 189 | 190 | You only need to perform this operation once. 191 | 192 | Note that Meilisearch will rebuild your index whenever you update `filterableAttributes`. Depending on the size of your dataset, this might take time. You can track the process using the [tasks](https://www.meilisearch.com/docs/reference/api/tasks#get-task). 193 | 194 | Then, you can perform the search: 195 | 196 | ```rust 197 | let search_result = client.index("movies_5") 198 | .search() 199 | .with_query("wonder") 200 | .with_filter("id > 1 AND genres = Action") 201 | .execute::() 202 | .await 203 | .unwrap(); 204 | println!("{:?}", search_result.hits); 205 | ``` 206 | 207 | Json output: 208 | ```json 209 | { 210 | "hits": [ 211 | { 212 | "id": 2, 213 | "title": "Wonder Woman", 214 | "genres": ["Action", "Adventure"] 215 | } 216 | ], 217 | "offset": 0, 218 | "limit": 20, 219 | "estimatedTotalHits": 1, 220 | "processingTimeMs": 0, 221 | "query": "wonder" 222 | } 223 | ``` 224 | 225 | #### Customize the `HttpClient` 226 | 227 | By default, the SDK uses [`reqwest`](https://docs.rs/reqwest/latest/reqwest/) to make http calls. 228 | The SDK lets you customize the http client by implementing the `HttpClient` trait yourself and 229 | initializing the `Client` with the `new_with_client` method. 230 | You may be interested by the `futures-unsend` feature which lets you specify a non-Send http client. 231 | 232 | #### Wasm support 233 | 234 | The SDK supports wasm through reqwest. You'll need to enable the `futures-unsend` feature while importing it, though. 235 | 236 | ## 🌐 Running in the Browser with WASM 237 | 238 | This crate fully supports WASM. 239 | 240 | The only difference between the WASM and the native version is that the native version has one more variant (`Error::Http`) in the Error enum. That should not matter so much but we could add this variant in WASM too. 241 | 242 | However, making a program intended to run in a web browser requires a **very** different design than a CLI program. To see an example of a simple Rust web app using Meilisearch, see the [our demo](./examples/web_app). 243 | 244 | WARNING: `meilisearch-sdk` will panic if no Window is available (ex: Web extension). 245 | 246 | ## 🤖 Compatibility with Meilisearch 247 | 248 | This package guarantees compatibility with [version v1.x of Meilisearch](https://github.com/meilisearch/meilisearch/releases/latest), but some features may not be present. Please check the [issues](https://github.com/meilisearch/meilisearch-rust/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22+label%3Aenhancement) for more info. 249 | 250 | ## ⚙️ Contributing 251 | 252 | Any new contribution is more than welcome in this project! 253 | 254 | If you want to know more about the development workflow or want to contribute, please visit our [contributing guidelines](/CONTRIBUTING.md) for detailed instructions! 255 | 256 |
257 | 258 | **Meilisearch** provides and maintains many **SDKs and Integration tools** like this one. We want to provide everyone with an **amazing search experience for any kind of project**. If you want to contribute, make suggestions, or just know what's going on right now, visit us in the [integration-guides](https://github.com/meilisearch/integration-guides) repository. 259 | -------------------------------------------------------------------------------- /README.tpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |

6 | Meilisearch-Rust 7 |

8 | 9 |

Meilisearch Rust SDK

10 | 11 |

12 | Meilisearch | 13 | Meilisearch Cloud | 14 | Documentation | 15 | Discord | 16 | Roadmap | 17 | Website | 18 | FAQ 19 |

20 | 21 |

22 | crates.io 23 | Tests 24 | License 25 | 26 | Bors enabled 27 |

28 | 29 |

⚡ The Meilisearch API client written for Rust 🦀

30 | 31 | **Meilisearch Rust** is the Meilisearch API client for Rust developers. 32 | 33 | **Meilisearch** is an open-source search engine. [Learn more about Meilisearch.](https://github.com/meilisearch/meilisearch) 34 | 35 | ## Table of Contents 36 | 37 | - [📖 Documentation](#-documentation) 38 | - [🔧 Installation](#-installation) 39 | - [🚀 Getting started](#-getting-started) 40 | - [🌐 Running in the Browser with WASM](#-running-in-the-browser-with-wasm) 41 | - [🤖 Compatibility with Meilisearch](#-compatibility-with-meilisearch) 42 | - [⚙️ Contributing](#️-contributing) 43 | 44 | ## 📖 Documentation 45 | 46 | This readme contains all the documentation you need to start using this Meilisearch SDK. 47 | 48 | For general information on how to use Meilisearch—such as our API reference, tutorials, guides, and in-depth articles—refer to our [main documentation website](https://www.meilisearch.com/docs). 49 | 50 | ## 🔧 Installation 51 | 52 | To use `meilisearch-sdk`, add this to your `Cargo.toml`: 53 | 54 | ```toml 55 | [dependencies] 56 | meilisearch-sdk = "0.28.0" 57 | ``` 58 | 59 | The following optional dependencies may also be useful: 60 | 61 | ```toml 62 | futures = "0.3" # To be able to block on async functions if you are not using an async runtime 63 | serde = { version = "1.0", features = ["derive"] } 64 | ``` 65 | 66 | This crate is `async` but you can choose to use an async runtime like [tokio](https://crates.io/crates/tokio) or just [block on futures](https://docs.rs/futures/latest/futures/executor/fn.block_on.html). 67 | You can enable the `sync` feature to make most structs `Sync`. It may be a bit slower. 68 | 69 | Using this crate is possible without [serde](https://crates.io/crates/serde), but a lot of features require serde. 70 | 71 | ### Run Meilisearch 72 | 73 | ⚡️ **Launch, scale, and streamline in minutes with Meilisearch Cloud**—no maintenance, no commitment, cancel anytime. [Try it free now](https://cloud.meilisearch.com/login?utm_campaign=oss&utm_source=github&utm_medium=meilisearch-rust). 74 | 75 | 🪨 Prefer to self-host? [Download and deploy](https://www.meilisearch.com/docs/learn/self_hosted/getting_started_with_self_hosted_meilisearch?utm_campaign=oss&utm_source=github&utm_medium=meilisearch-rust) our fast, open-source search engine on your own infrastructure. 76 | 77 | {{readme}} 78 | 79 | ## 🌐 Running in the Browser with WASM 80 | 81 | This crate fully supports WASM. 82 | 83 | The only difference between the WASM and the native version is that the native version has one more variant (`Error::Http`) in the Error enum. That should not matter so much but we could add this variant in WASM too. 84 | 85 | However, making a program intended to run in a web browser requires a **very** different design than a CLI program. To see an example of a simple Rust web app using Meilisearch, see the [our demo](./examples/web_app). 86 | 87 | WARNING: `meilisearch-sdk` will panic if no Window is available (ex: Web extension). 88 | 89 | ## 🤖 Compatibility with Meilisearch 90 | 91 | This package guarantees compatibility with [version v1.x of Meilisearch](https://github.com/meilisearch/meilisearch/releases/latest), but some features may not be present. Please check the [issues](https://github.com/meilisearch/meilisearch-rust/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22+label%3Aenhancement) for more info. 92 | 93 | ## ⚙️ Contributing 94 | 95 | Any new contribution is more than welcome in this project! 96 | 97 | If you want to know more about the development workflow or want to contribute, please visit our [contributing guidelines](/CONTRIBUTING.md) for detailed instructions! 98 | 99 |
100 | 101 | **Meilisearch** provides and maintains many **SDKs and Integration tools** like this one. We want to provide everyone with an **amazing search experience for any kind of project**. If you want to contribute, make suggestions, or just know what's going on right now, visit us in the [integration-guides](https://github.com/meilisearch/integration-guides) repository. 102 | -------------------------------------------------------------------------------- /bors.toml: -------------------------------------------------------------------------------- 1 | status = [ 2 | 'integration-tests', 3 | 'clippy-check', 4 | 'rust-format', 5 | 'readme-check', 6 | 'wasm-build', 7 | 'Yaml linting check' 8 | ] 9 | # 1 hour timeout 10 | timeout-sec = 3600 11 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.8" 2 | 3 | # remove this line if you don't need a volume to map your dependencies 4 | # Check how to cache the build 5 | volumes: 6 | cargo: 7 | 8 | services: 9 | package: 10 | image: rust:1 11 | tty: true 12 | stdin_open: true 13 | working_dir: /home/package 14 | environment: 15 | - MEILISEARCH_URL=http://meilisearch:7700 16 | - CARGO_HOME=/vendor/cargo 17 | depends_on: 18 | - meilisearch 19 | links: 20 | - meilisearch 21 | volumes: 22 | - ./:/home/package 23 | - cargo:/vendor/cargo 24 | 25 | meilisearch: 26 | image: getmeili/meilisearch:latest 27 | ports: 28 | - "7700" 29 | environment: 30 | - MEILI_MASTER_KEY=masterKey 31 | - MEILI_NO_ANALYTICS=true 32 | -------------------------------------------------------------------------------- /examples/cli-app-with-awc/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cli-app-with-awc" 3 | version = "0.0.0" 4 | edition = "2021" 5 | publish = false 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | meilisearch-sdk = { path = "../..", default-features = false, features = ["futures-unsend"] } 11 | futures = "0.3" 12 | serde = { version = "1.0", features = ["derive"] } 13 | serde_json = "1.0" 14 | lazy_static = "1.4.0" 15 | awc = "3.4" 16 | async-trait = "0.1.51" 17 | tokio = { version = "1.27.0", features = ["full"] } 18 | yaup = "0.3.0" 19 | tokio-util = { version = "0.7.10", features = ["full"] } 20 | actix-rt = "2.9.0" 21 | anyhow = "1.0.82" 22 | -------------------------------------------------------------------------------- /examples/cli-app-with-awc/assets/clothes.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": 1, 4 | "seaon": "winter", 5 | "article": "sweater", 6 | "cost": 63.40, 7 | "size":"L", 8 | "pattern":"striped" 9 | }, 10 | { 11 | "id": 2, 12 | "seaon": "spring", 13 | "article": "sweat pants", 14 | "cost": 18.00, 15 | "size":"XXXL", 16 | "pattern":"floral" 17 | }, 18 | { 19 | "id": 3, 20 | "seaon": "fall", 21 | "article": "t-shirt", 22 | "cost": 1634.90, 23 | "size":"M", 24 | "pattern":"solid black" 25 | }, 26 | { 27 | "id": 4, 28 | "seaon": "summer", 29 | "article": "tank top", 30 | "cost": 3.40, 31 | "size":"L", 32 | "pattern":"diagonal" 33 | }, 34 | { 35 | "id": 5, 36 | "seaon": "winter", 37 | "article": "jeans", 38 | "cost": 4.20, 39 | "size":"XL", 40 | "pattern":"striped" 41 | }, 42 | { 43 | "id": 6, 44 | "seaon": "spring", 45 | "article": "sun dress", 46 | "cost": 12634.56, 47 | "size":"L", 48 | "pattern":"floral" 49 | }, 50 | { 51 | "id": 7, 52 | "seaon": "fall", 53 | "article": "sweatshirt", 54 | "cost": 90.80, 55 | "size":"M", 56 | "pattern":"checker" 57 | }, 58 | { 59 | "id": 8, 60 | "seaon": "summer", 61 | "article": "shorts", 62 | "cost": 16.34, 63 | "size":"XS", 64 | "pattern":"solid beige" 65 | }, 66 | { 67 | "id": 9, 68 | "seaon": "winter", 69 | "article": "jacket", 70 | "cost": 634, 71 | "size":"L", 72 | "pattern":"camo" 73 | } 74 | ] -------------------------------------------------------------------------------- /examples/cli-app-with-awc/src/main.rs: -------------------------------------------------------------------------------- 1 | use async_trait::async_trait; 2 | use meilisearch_sdk::errors::Error; 3 | use meilisearch_sdk::request::{parse_response, HttpClient, Method}; 4 | use meilisearch_sdk::{client::*, settings::Settings}; 5 | use serde::de::DeserializeOwned; 6 | use serde::{Deserialize, Serialize}; 7 | use std::fmt; 8 | use std::io::stdin; 9 | 10 | #[derive(Debug, Clone)] 11 | pub struct AwcClient { 12 | api_key: Option, 13 | } 14 | 15 | impl AwcClient { 16 | pub fn new(api_key: Option<&str>) -> Result { 17 | Ok(AwcClient { 18 | api_key: api_key.map(|key| key.to_string()), 19 | }) 20 | } 21 | } 22 | 23 | #[async_trait(?Send)] 24 | impl HttpClient for AwcClient { 25 | async fn stream_request< 26 | Query: Serialize + Send + Sync, 27 | Body: futures::AsyncRead + Send + Sync + 'static, 28 | Output: DeserializeOwned + 'static, 29 | >( 30 | &self, 31 | url: &str, 32 | method: Method, 33 | content_type: &str, 34 | expected_status_code: u16, 35 | ) -> Result { 36 | let mut builder = awc::ClientBuilder::new(); 37 | if let Some(ref api_key) = self.api_key { 38 | builder = builder.bearer_auth(api_key); 39 | } 40 | builder = builder.add_default_header(("User-Agent", "Rust client with Awc")); 41 | let client = builder.finish(); 42 | 43 | let query = method.query(); 44 | let query = yaup::to_string(query)?; 45 | 46 | let url = if query.is_empty() { 47 | url.to_string() 48 | } else { 49 | format!("{url}?{query}") 50 | }; 51 | 52 | let url = add_query_parameters(&url, method.query())?; 53 | let request = client.request(verb(&method), &url); 54 | 55 | let mut response = if let Some(body) = method.into_body() { 56 | let reader = tokio_util::compat::FuturesAsyncReadCompatExt::compat(body); 57 | let stream = tokio_util::io::ReaderStream::new(reader); 58 | request 59 | .content_type(content_type) 60 | .send_stream(stream) 61 | .await 62 | .map_err(|err| Error::Other(anyhow::anyhow!(err.to_string()).into()))? 63 | } else { 64 | request 65 | .send() 66 | .await 67 | .map_err(|err| Error::Other(anyhow::anyhow!(err.to_string()).into()))? 68 | }; 69 | 70 | let status = response.status().as_u16(); 71 | let mut body = String::from_utf8( 72 | response 73 | .body() 74 | .await 75 | .map_err(|err| Error::Other(anyhow::anyhow!(err.to_string()).into()))? 76 | .to_vec(), 77 | ) 78 | .map_err(|err| Error::Other(anyhow::anyhow!(err.to_string()).into()))?; 79 | 80 | if body.is_empty() { 81 | body = "null".to_string(); 82 | } 83 | 84 | parse_response(status, expected_status_code, &body, url.to_string()) 85 | } 86 | } 87 | 88 | #[actix_rt::main] 89 | async fn main() { 90 | let http_client = AwcClient::new(Some("masterKey")).unwrap(); 91 | let client = Client::new_with_client("http://localhost:7700", Some("masterKey"), http_client); 92 | 93 | // build the index 94 | build_index(&client).await; 95 | 96 | // enter in search queries or quit 97 | loop { 98 | println!("Enter a search query or type \"q\" or \"quit\" to quit:"); 99 | let mut input_string = String::new(); 100 | stdin() 101 | .read_line(&mut input_string) 102 | .expect("Failed to read line"); 103 | match input_string.trim() { 104 | "quit" | "q" | "" => { 105 | println!("exiting..."); 106 | break; 107 | } 108 | _ => { 109 | search(&client, input_string.trim()).await; 110 | } 111 | } 112 | } 113 | // get rid of the index at the end, doing this only so users don't have the index without knowing 114 | let _ = client.delete_index("clothes").await.unwrap(); 115 | } 116 | 117 | async fn search(client: &Client, query: &str) { 118 | // make the search query, which excutes and serializes hits into the 119 | // ClothesDisplay struct 120 | let query_results = client 121 | .index("clothes") 122 | .search() 123 | .with_query(query) 124 | .execute::() 125 | .await 126 | .unwrap() 127 | .hits; 128 | 129 | // display the query results 130 | if query_results.is_empty() { 131 | println!("no results..."); 132 | } else { 133 | for clothes in query_results { 134 | let display = clothes.result; 135 | println!("{}", format_args!("{}", display)); 136 | } 137 | } 138 | } 139 | 140 | async fn build_index(client: &Client) { 141 | // reading and parsing the file 142 | let content = include_str!("../assets/clothes.json"); 143 | 144 | // serialize the string to clothes objects 145 | let clothes: Vec = serde_json::from_str(content).unwrap(); 146 | 147 | //create displayed attributes 148 | let displayed_attributes = ["article", "cost", "size", "pattern"]; 149 | 150 | // Create ranking rules 151 | let ranking_rules = ["words", "typo", "attribute", "exactness", "cost:asc"]; 152 | 153 | //create searchable attributes 154 | let searchable_attributes = ["seaon", "article", "size", "pattern"]; 155 | 156 | // create the synonyms hashmap 157 | let mut synonyms = std::collections::HashMap::new(); 158 | synonyms.insert("sweater", vec!["cardigan", "long-sleeve"]); 159 | synonyms.insert("sweat pants", vec!["joggers", "gym pants"]); 160 | synonyms.insert("t-shirt", vec!["tees", "tshirt"]); 161 | 162 | //create the settings struct 163 | let settings = Settings::new() 164 | .with_ranking_rules(ranking_rules) 165 | .with_searchable_attributes(searchable_attributes) 166 | .with_displayed_attributes(displayed_attributes) 167 | .with_synonyms(synonyms); 168 | 169 | //add the settings to the index 170 | let result = client 171 | .index("clothes") 172 | .set_settings(&settings) 173 | .await 174 | .unwrap() 175 | .wait_for_completion(client, None, None) 176 | .await 177 | .unwrap(); 178 | 179 | if result.is_failure() { 180 | panic!( 181 | "Encountered an error while setting settings for index: {:?}", 182 | result.unwrap_failure() 183 | ); 184 | } 185 | 186 | // add the documents 187 | let result = client 188 | .index("clothes") 189 | .add_or_update(&clothes, Some("id")) 190 | .await 191 | .unwrap() 192 | .wait_for_completion(client, None, None) 193 | .await 194 | .unwrap(); 195 | 196 | if result.is_failure() { 197 | panic!( 198 | "Encountered an error while sending the documents: {:?}", 199 | result.unwrap_failure() 200 | ); 201 | } 202 | } 203 | 204 | /// Base search object. 205 | #[derive(Serialize, Deserialize, Debug)] 206 | pub struct Clothes { 207 | id: usize, 208 | seaon: String, 209 | article: String, 210 | cost: f32, 211 | size: String, 212 | pattern: String, 213 | } 214 | 215 | /// Search results get serialized to this struct 216 | #[derive(Serialize, Deserialize, Debug)] 217 | pub struct ClothesDisplay { 218 | article: String, 219 | cost: f32, 220 | size: String, 221 | pattern: String, 222 | } 223 | 224 | impl fmt::Display for ClothesDisplay { 225 | // This trait requires `fmt` with this exact signature. 226 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 227 | // Write strictly the first element into the supplied output 228 | // stream: `f`. Returns `fmt::Result` which indicates whether the 229 | // operation succeeded or failed. Note that `write!` uses syntax which 230 | // is very similar to `println!`. 231 | write!( 232 | f, 233 | "result\n article: {},\n price: {},\n size: {},\n pattern: {}\n", 234 | self.article, self.cost, self.size, self.pattern 235 | ) 236 | } 237 | } 238 | 239 | fn add_query_parameters(url: &str, query: &Query) -> Result { 240 | let query = yaup::to_string(query)?; 241 | 242 | if query.is_empty() { 243 | Ok(url.to_string()) 244 | } else { 245 | Ok(format!("{url}?{query}")) 246 | } 247 | } 248 | 249 | fn verb(method: &Method) -> awc::http::Method { 250 | match method { 251 | Method::Get { .. } => awc::http::Method::GET, 252 | Method::Delete { .. } => awc::http::Method::DELETE, 253 | Method::Post { .. } => awc::http::Method::POST, 254 | Method::Put { .. } => awc::http::Method::PUT, 255 | Method::Patch { .. } => awc::http::Method::PATCH, 256 | } 257 | } 258 | -------------------------------------------------------------------------------- /examples/cli-app/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cli-app" 3 | version = "0.0.0" 4 | edition = "2021" 5 | publish = false 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | meilisearch-sdk = {path="../.."} 11 | futures = "0.3" 12 | serde = { version="1.0", features = ["derive"] } 13 | serde_json = "1.0" 14 | lazy_static = "1.4.0" 15 | yaup = "0.3.0" 16 | -------------------------------------------------------------------------------- /examples/cli-app/assets/clothes.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": 1, 4 | "season": "winter", 5 | "article": "sweater", 6 | "cost": 63.40, 7 | "size":"L", 8 | "pattern":"striped" 9 | }, 10 | { 11 | "id": 2, 12 | "season": "spring", 13 | "article": "sweat pants", 14 | "cost": 18.00, 15 | "size":"XXXL", 16 | "pattern":"floral" 17 | }, 18 | { 19 | "id": 3, 20 | "season": "fall", 21 | "article": "t-shirt", 22 | "cost": 1634.90, 23 | "size":"M", 24 | "pattern":"solid black" 25 | }, 26 | { 27 | "id": 4, 28 | "season": "summer", 29 | "article": "tank top", 30 | "cost": 3.40, 31 | "size":"L", 32 | "pattern":"diagonal" 33 | }, 34 | { 35 | "id": 5, 36 | "season": "winter", 37 | "article": "jeans", 38 | "cost": 4.20, 39 | "size":"XL", 40 | "pattern":"striped" 41 | }, 42 | { 43 | "id": 6, 44 | "season": "spring", 45 | "article": "sun dress", 46 | "cost": 12634.56, 47 | "size":"L", 48 | "pattern":"floral" 49 | }, 50 | { 51 | "id": 7, 52 | "season": "fall", 53 | "article": "sweatshirt", 54 | "cost": 90.80, 55 | "size":"M", 56 | "pattern":"checker" 57 | }, 58 | { 59 | "id": 8, 60 | "season": "summer", 61 | "article": "shorts", 62 | "cost": 16.34, 63 | "size":"XS", 64 | "pattern":"solid beige" 65 | }, 66 | { 67 | "id": 9, 68 | "season": "winter", 69 | "article": "jacket", 70 | "cost": 634, 71 | "size":"L", 72 | "pattern":"camo" 73 | } 74 | ] 75 | -------------------------------------------------------------------------------- /examples/cli-app/src/main.rs: -------------------------------------------------------------------------------- 1 | use futures::executor::block_on; 2 | use lazy_static::lazy_static; 3 | use meilisearch_sdk::client::Client; 4 | use meilisearch_sdk::settings::Settings; 5 | use serde::{Deserialize, Serialize}; 6 | use std::fmt; 7 | use std::io::stdin; 8 | 9 | // instantiate the client. load it once 10 | lazy_static! { 11 | static ref CLIENT: Client = Client::new("http://localhost:7700", Some("masterKey")).unwrap(); 12 | } 13 | 14 | fn main() { 15 | block_on(async move { 16 | // build the index 17 | build_index().await; 18 | 19 | // enter in search queries or quit 20 | loop { 21 | println!("Enter a search query or type \"q\" or \"quit\" to quit:"); 22 | let mut input_string = String::new(); 23 | stdin() 24 | .read_line(&mut input_string) 25 | .expect("Failed to read line"); 26 | match input_string.trim() { 27 | "quit" | "q" | "" => { 28 | println!("exiting..."); 29 | break; 30 | } 31 | _ => { 32 | search(input_string.trim()).await; 33 | } 34 | } 35 | } 36 | // get rid of the index at the end, doing this only so users don't have the index without knowing 37 | let _ = CLIENT.delete_index("clothes").await.unwrap(); 38 | }) 39 | } 40 | 41 | async fn search(query: &str) { 42 | // make the search query, which executes and serializes hits into the 43 | // ClothesDisplay struct 44 | let query_results = CLIENT 45 | .index("clothes") 46 | .search() 47 | .with_query(query) 48 | .execute::() 49 | .await 50 | .unwrap() 51 | .hits; 52 | 53 | // display the query results 54 | if query_results.is_empty() { 55 | println!("no results..."); 56 | } else { 57 | for clothes in query_results { 58 | let display = clothes.result; 59 | println!("{}", format_args!("{}", display)); 60 | } 61 | } 62 | } 63 | 64 | async fn build_index() { 65 | // reading and parsing the file 66 | let content = include_str!("../assets/clothes.json"); 67 | 68 | // serialize the string to clothes objects 69 | let clothes: Vec = serde_json::from_str(content).unwrap(); 70 | 71 | // create displayed attributes 72 | let displayed_attributes = ["article", "cost", "size", "pattern"]; 73 | 74 | // Create ranking rules 75 | let ranking_rules = ["words", "typo", "attribute", "exactness", "cost:asc"]; 76 | 77 | // create searchable attributes 78 | let searchable_attributes = ["season", "article", "size", "pattern"]; 79 | 80 | // create the synonyms hashmap 81 | let mut synonyms = std::collections::HashMap::new(); 82 | synonyms.insert("sweater", vec!["cardigan", "long-sleeve"]); 83 | synonyms.insert("sweat pants", vec!["joggers", "gym pants"]); 84 | synonyms.insert("t-shirt", vec!["tees", "tshirt"]); 85 | 86 | // create the settings struct 87 | let settings = Settings::new() 88 | .with_ranking_rules(ranking_rules) 89 | .with_searchable_attributes(searchable_attributes) 90 | .with_displayed_attributes(displayed_attributes) 91 | .with_synonyms(synonyms); 92 | 93 | // add the settings to the index 94 | let result = CLIENT 95 | .index("clothes") 96 | .set_settings(&settings) 97 | .await 98 | .unwrap() 99 | .wait_for_completion(&CLIENT, None, None) 100 | .await 101 | .unwrap(); 102 | 103 | if result.is_failure() { 104 | panic!( 105 | "Encountered an error while setting settings for index: {:?}", 106 | result.unwrap_failure() 107 | ); 108 | } 109 | 110 | // add the documents 111 | let result = CLIENT 112 | .index("clothes") 113 | .add_or_update(&clothes, Some("id")) 114 | .await 115 | .unwrap() 116 | .wait_for_completion(&CLIENT, None, None) 117 | .await 118 | .unwrap(); 119 | 120 | if result.is_failure() { 121 | panic!( 122 | "Encountered an error while sending the documents: {:?}", 123 | result.unwrap_failure() 124 | ); 125 | } 126 | } 127 | 128 | /// Base search object. 129 | #[derive(Serialize, Deserialize, Debug)] 130 | pub struct Clothes { 131 | id: usize, 132 | season: String, 133 | article: String, 134 | cost: f32, 135 | size: String, 136 | pattern: String, 137 | } 138 | 139 | /// Search results get serialized to this struct 140 | #[derive(Serialize, Deserialize, Debug)] 141 | pub struct ClothesDisplay { 142 | article: String, 143 | cost: f32, 144 | size: String, 145 | pattern: String, 146 | } 147 | 148 | impl fmt::Display for ClothesDisplay { 149 | // This trait requires `fmt` with this exact signature. 150 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 151 | // Write strictly the first element into the supplied output 152 | // stream: `f`. Returns `fmt::Result` which indicates whether the 153 | // operation succeeded or failed. Note that `write!` uses syntax which 154 | // is very similar to `println!`. 155 | write!( 156 | f, 157 | "result\n article: {},\n price: {},\n size: {},\n pattern: {}\n", 158 | self.article, self.cost, self.size, self.pattern 159 | ) 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /examples/settings.rs: -------------------------------------------------------------------------------- 1 | use meilisearch_sdk::{client::Client, indexes::Index, settings::Settings}; 2 | 3 | // we need an async runtime 4 | #[tokio::main(flavor = "current_thread")] 5 | async fn main() { 6 | let client: Client = Client::new("http://localhost:7700", Some("masterKey")).unwrap(); 7 | 8 | // We try to create an index called `movies` with a primary_key of `movie_id`. 9 | let my_index: Index = client 10 | .create_index("movies", Some("movie_id")) 11 | .await 12 | .expect("Could not join the remote server.") 13 | // The creation of indexes is asynchronous. But for the sake of the example so we will 14 | // wait until the update is entirely processed. 15 | .wait_for_completion(&client, None, None) 16 | .await 17 | .expect("Could not join the remote server.") 18 | // If the creation was successful we can generate an `Index` out of it. 19 | .try_make_index(&client) 20 | // This error comes from meilisearch itself. 21 | .expect("An error happened with the index creation."); 22 | 23 | // And now we can update the settings! 24 | // You can read more about the available options here: https://www.meilisearch.com/docs/learn/configuration/settings#index-settings 25 | let settings: Settings = Settings::new() 26 | .with_searchable_attributes(["name", "title"]) 27 | .with_filterable_attributes(["created_at"]); 28 | 29 | // Updating the settings is also an asynchronous operation. 30 | let task = my_index 31 | .set_settings(&settings) 32 | .await 33 | .expect("Could not join the remote server.") 34 | // And here we wait for the operation to execute entirely so we can check any error happened. 35 | .wait_for_completion(&client, None, None) 36 | .await 37 | .expect("Could not join the remote server."); 38 | 39 | // We check if the task failed. 40 | assert!( 41 | !task.is_failure(), 42 | "Could not update the settings. {}", 43 | task.unwrap_failure().error_message 44 | ); 45 | 46 | // And finally we delete the `Index`. 47 | my_index 48 | .delete() 49 | .await 50 | .expect("Could not join the remote server.") 51 | .wait_for_completion(&client, None, None) 52 | .await 53 | .expect("Could not join the remote server."); 54 | 55 | // We check if the task failed. 56 | assert!( 57 | !task.is_failure(), 58 | "Could not delete the index. {}", 59 | task.unwrap_failure().error_message 60 | ); 61 | } 62 | -------------------------------------------------------------------------------- /examples/web_app/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "web_app" 3 | version = "0.0.0" 4 | authors = ["Mubelotix "] 5 | edition = "2018" 6 | publish = false 7 | 8 | [lib] 9 | crate-type = ["cdylib", "rlib"] 10 | 11 | [dependencies] 12 | serde_json = "1.0" 13 | wasm-bindgen = "0.2" 14 | wasm-bindgen-futures = "0.4.18" 15 | yew = {version="0.21", features = ["csr"]} 16 | meilisearch-sdk = { path="../..", features = ["futures-unsend"] } 17 | lazy_static = "1.4" 18 | serde = {version="1.0", features=["derive"]} 19 | web-sys = "0.3" 20 | console_error_panic_hook = "0.1" 21 | -------------------------------------------------------------------------------- /examples/web_app/README.md: -------------------------------------------------------------------------------- 1 | # Build your front-end page in Rust with WebAssembly 2 | 3 | > **Note** 4 | > It is not possible to run Meilisearch in the browser without a server. This demo uses the Rust SDK in a browser using WASM, and communicates with a Meilisearch instance that is running on a remote server. 5 | 6 | This example is a clone of [crates.meilisearch.com](https://crates.meilisearch.com), but the front-end is written in Rust! 7 | The Rust source files are compiled into WebAssembly and so can be readable by the browsers. 8 | 9 | ## Checking 10 | 11 | If you only want to check if this example compiles, you can run: 12 | 13 | ```console 14 | cargo build 15 | ``` 16 | 17 | ## Building 18 | 19 | To build this example, you need [wasm-pack](https://github.com/rustwasm/wasm-pack).\ 20 | You can install `wasm-pack` with this command: 21 | ```console 22 | curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh 23 | ``` 24 | 25 | ```console 26 | wasm-pack build . --target=web --no-typescript 27 | ``` 28 | 29 | The compiled files will be stored in the `examples/web_app/pkg` folder. 30 | 31 | ## Using 32 | 33 | Theoretically, you could just open the `examples/web_app/pkg/index.html` file but due to browsers' security restrictions, you need a web server. For example: 34 | 35 | ```console 36 | python3 -m http.server 8080 37 | ``` 38 | 39 | And then go to the `http://localhost:8080/` URL in your browser. 40 | -------------------------------------------------------------------------------- /examples/web_app/src/document.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use serde_json::{Map, Value}; 3 | use yew::prelude::*; 4 | 5 | #[derive(Debug, Serialize, Deserialize)] 6 | pub struct Crate { 7 | name: String, 8 | downloads: Option, 9 | description: String, 10 | keywords: Vec, 11 | categories: Vec, 12 | readme: String, 13 | version: String, 14 | } 15 | 16 | fn get_readable_download_count(this: &Map) -> String { 17 | if let Some(downloads) = this["downloads"].as_f64() { 18 | if downloads < 1000.0 { 19 | downloads.to_string() 20 | } else if downloads < 1000000.0 { 21 | format!("{:.1}k", downloads / 1000.0) 22 | } else { 23 | format!("{:.1}M", downloads / 1000000.0) 24 | } 25 | } else { 26 | String::from("?") 27 | } 28 | } 29 | 30 | pub fn display(this: &Map) -> Html { 31 | let mut url = format!( 32 | "https://lib.rs/crates/{}", 33 | this["name"].as_str().unwrap_or_default() 34 | ); 35 | url = url.replace("", ""); 36 | url = url.replace("", ""); 37 | 38 | html! { 39 |
  • 40 |
    41 |

    42 | { 43 | // This field is formatted so we don't want Yew to escape the HTML tags 44 | unescaped_html(this["name"].as_str().unwrap_or_default()) 45 | } 46 |

    47 |

    {unescaped_html(this["description"].as_str().unwrap_or_default())}

    48 |
    49 |
    50 | 51 | {"v"} 52 | {&this["version"].as_str().unwrap_or_default()} 53 | 54 | 55 | {get_readable_download_count(this)} 56 | 57 | {for this["keywords"].as_array().unwrap().iter().map(|keyword| 58 | html! { 59 | 60 | {"#"} 61 | {keyword.as_str().unwrap_or_default()} 62 | 63 | } 64 | )} 65 |
    66 | 67 |
  • 68 | } 69 | } 70 | 71 | use web_sys::Node; 72 | use yew::virtual_dom::VNode; 73 | 74 | /// Creates an element from raw HTML 75 | fn unescaped_html(html: &str) -> VNode { 76 | let element = web_sys::window() 77 | .unwrap() 78 | .document() 79 | .unwrap() 80 | .create_element("div") 81 | .unwrap(); 82 | element.set_inner_html(html); 83 | 84 | VNode::VRef(Node::from(element)) 85 | } 86 | -------------------------------------------------------------------------------- /examples/web_app/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![recursion_limit = "512"] 2 | use lazy_static::lazy_static; 3 | use meilisearch_sdk::client::Client; 4 | use meilisearch_sdk::indexes::Index; 5 | use meilisearch_sdk::search::{SearchResults, Selectors::All}; 6 | use serde_json::{Map, Value}; 7 | use std::rc::Rc; 8 | use wasm_bindgen::prelude::*; 9 | use wasm_bindgen_futures::spawn_local; 10 | use yew::prelude::*; 11 | use yew::{html::Scope, Html}; 12 | 13 | mod document; 14 | use crate::document::{display, Crate}; 15 | 16 | lazy_static! { 17 | static ref CLIENT: Client = Client::new("http://localhost:7700", Some("masterKey")).unwrap(); 18 | } 19 | 20 | struct Model { 21 | index: Rc, 22 | results: Vec>, 23 | processing_time_ms: usize, 24 | 25 | // These two fields are used to avoid rollbacks by giving an ID to each request 26 | latest_sent_request_id: usize, 27 | displayed_request_id: usize, 28 | } 29 | 30 | enum Msg { 31 | /// An event sent to update the results with a query 32 | Input(String), 33 | /// The event sent to display new results once they are received 34 | Update { 35 | results: Vec>, 36 | processing_time_ms: usize, 37 | request_id: usize, 38 | }, 39 | } 40 | 41 | impl Component for Model { 42 | type Message = Msg; 43 | type Properties = (); 44 | fn create(_ctx: &Context) -> Model { 45 | Model { 46 | // The index method avoids checking the existence of the index. 47 | // It won't make any HTTP request so the function is not async so it's easier to use. 48 | // Use only if you are sure that the index exists. 49 | index: Rc::new(CLIENT.index("crates")), 50 | results: Vec::new(), 51 | processing_time_ms: 0, 52 | 53 | latest_sent_request_id: 0, 54 | displayed_request_id: 0, 55 | } 56 | } 57 | 58 | fn update(&mut self, ctx: &Context, msg: Self::Message) -> bool { 59 | match msg { 60 | // Sent when the value of the text input changed (so we have to make a new request) 61 | Msg::Input(value) => { 62 | let index = Rc::clone(&self.index); 63 | let link = ctx.link().clone(); 64 | self.latest_sent_request_id += 1; 65 | let request_id = self.latest_sent_request_id; 66 | // Spawn a task loading results 67 | spawn_local(async move { 68 | // Load the results 69 | let fresh_results: SearchResults = index 70 | .search() 71 | .with_query(&value) 72 | .with_attributes_to_highlight(All) 73 | .execute() 74 | .await 75 | .expect("Failed to execute query"); 76 | 77 | let mut fresh_formatted_results = Vec::new(); 78 | for result in fresh_results.hits { 79 | fresh_formatted_results.push(result.formatted_result.unwrap()); 80 | } 81 | 82 | // We send a new event with the up-to-date data so that we can update the results and display them. 83 | link.send_message(Msg::Update { 84 | results: fresh_formatted_results, 85 | processing_time_ms: fresh_results.processing_time_ms, 86 | request_id, 87 | }); 88 | }); 89 | false 90 | } 91 | 92 | // Sent when new results are received 93 | Msg::Update { 94 | results, 95 | processing_time_ms, 96 | request_id, 97 | } => { 98 | if request_id >= self.latest_sent_request_id { 99 | self.results = results; 100 | self.processing_time_ms = processing_time_ms; 101 | self.displayed_request_id = request_id; 102 | true 103 | } else { 104 | // We are already displaying more up-to-date results. 105 | // This request is too late so we cannot display these results to avoid rollbacks. 106 | false 107 | } 108 | } 109 | } 110 | } 111 | 112 | fn changed(&mut self, _ctx: &Context, _old_props: &Self::Properties) -> bool { 113 | false 114 | } 115 | 116 | fn view(&self, ctx: &Context) -> Html { 117 | html! { 118 | <> 119 |
    120 | {header_content(self.processing_time_ms, ctx.link())} 121 |
    122 |
    123 |
    124 |
      125 | { 126 | // Display the results 127 | for self.results.iter().map(display) 128 | } 129 |
    130 |
    131 |
    132 |
    133 |
    134 |

    {"Search powered by "}{"MeiliDB"}{"."}

    135 |
    136 |
    137 | 138 | } 139 | } 140 | } 141 | 142 | fn header_content(processing_time_ms: usize, link: &Scope) -> Html { 143 | html! { 144 |
    145 |

    {"Meili crates browser 2000"}

    146 |

    147 | {"This search bar is provided by "}{"Meili"}{", it is a demonstration of our instant search engine."}
    148 | {"If you want to take a look at the project source code, it's your lucky day as it is "}{"available on github"}{"."}
    149 | {"We wrote a blog post about "}{"how we made this search engine available for you"}{"."}
    150 | {"What you are currently using is not the original front end, but a clone using "}{"the Meilisearch Rust SDK"}{" and "}{"Yew"}{". The code is available "}{"here"}{"."}
    151 | {"The whole design was taken from "}{"lib.rs"}{" because we love it."}
    152 |
    {"We pull new crates and crates update every "}{"10 minutes"}{" from "}{"docs.rs"}{" and all the downloads count "}{"every day at 3:30 PM UTC"}{" from "}{"crates.io"}{". Currently we have something like "}{" 31 729 crates"}{"."}
    153 |
    {"Have fun using it "}⌨️{" "}💨
    154 |

    155 | 164 | 169 |
    170 | } 171 | } 172 | 173 | // The main() function of wasm 174 | #[wasm_bindgen(start)] 175 | pub fn run_app() { 176 | console_error_panic_hook::set_once(); 177 | yew::Renderer::::new().render(); 178 | } 179 | -------------------------------------------------------------------------------- /examples/web_app_graphql/.env.example.txt: -------------------------------------------------------------------------------- 1 | DATABASE_URL=postgres://[username]:[password]@localhost/[database_name] 2 | MEILISEARCH_HOST=http://localhost:7700 3 | MEILISEARCH_API_KEY=[your-master-key] 4 | MIGRATIONS_DIR_PATH=migrations -------------------------------------------------------------------------------- /examples/web_app_graphql/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /Cargo.lock 3 | 4 | .env -------------------------------------------------------------------------------- /examples/web_app_graphql/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "meilisearch-ex" 3 | version = "0.1.0" 4 | edition = "2021" 5 | authors = ["Eugene Korir "] 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | actix-cors = "0.7.0" 11 | actix-web = "4.4.0" 12 | async-graphql = "6.0.11" 13 | async-graphql-actix-web = "6.0.11" 14 | diesel = { version = "2.1.4", features = ["postgres"] } 15 | diesel-async = { version = "0.5.0", features = ["postgres", "deadpool"] } 16 | diesel_migrations = "2.1.0" 17 | dotenvy = "0.15.7" 18 | env_logger = "0.11.3" 19 | envy = "0.4.2" 20 | futures = "0.3.29" 21 | log = "0.4.20" 22 | meilisearch-sdk = "0.24.3" 23 | serde = { version = "1.0.192", features = ["derive"] } 24 | serde_json = "1.0.108" 25 | thiserror = "1.0.51" 26 | validator = { version = "0.18.1", features = ["derive"] } 27 | -------------------------------------------------------------------------------- /examples/web_app_graphql/README.md: -------------------------------------------------------------------------------- 1 | # Meilisearch example with graphql using `diesel`, `async_graphql` and `postgres` 2 | 3 | ## Contents 4 | 5 | Setting up a graphql server using `async_graphql` and `actix-web` 6 | 7 | Using `diesel` to query the database 8 | 9 | Using `meilisearch-sdk` to search for records that match a given criteria 10 | 11 | ## Running the example 12 | 13 | The meilisearch server needs to be running. You can run it by the command below 14 | 15 | ```bash 16 | meilisearch --master-key 17 | ``` 18 | 19 | Then you can run the application by simply running 20 | 21 | ```bash 22 | cargo run --release 23 | ``` 24 | 25 | The above command will display a link to your running instance and you can simply proceed by clicking the link or navigating to your browser. 26 | 27 | ### Running the resolvers 28 | 29 | On your browser, you will see a graphql playground in which you can use to run some queries 30 | 31 | You can use the `searchUsers` query as follows: 32 | 33 | ```gpl 34 | query { 35 | users{ 36 | search(queryString: "Eugene"){ 37 | lastName 38 | firstName 39 | email 40 | } 41 | } 42 | } 43 | ``` 44 | 45 | ### Errors 46 | 47 | Incase you run into the following error: 48 | 49 | ```bash 50 | = note: ld: library not found for -lpq 51 | clang: error: linker command failed with exit code 1 (use -v to see invocation) 52 | ``` 53 | 54 | Run: 55 | 56 | ```bash 57 | sudo apt install libpq-dev 58 | ``` 59 | 60 | This should fix the error 61 | -------------------------------------------------------------------------------- /examples/web_app_graphql/diesel.toml: -------------------------------------------------------------------------------- 1 | # For documentation on how to configure this file, 2 | # see https://diesel.rs/guides/configuring-diesel-cli 3 | 4 | [print_schema] 5 | file = "src/schema.rs" 6 | custom_type_derives = ["diesel::query_builder::QueryId"] 7 | 8 | [migrations_directory] 9 | dir = "migrations" 10 | -------------------------------------------------------------------------------- /examples/web_app_graphql/migrations/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meilisearch/meilisearch-rust/90a153c88112cddefc1936d3bef4763907211c2b/examples/web_app_graphql/migrations/.keep -------------------------------------------------------------------------------- /examples/web_app_graphql/migrations/00000000000000_diesel_initial_setup/down.sql: -------------------------------------------------------------------------------- 1 | -- This file was automatically created by Diesel to setup helper functions 2 | -- and other internal bookkeeping. This file is safe to edit, any future 3 | -- changes will be added to existing projects as new migrations. 4 | 5 | DROP FUNCTION IF EXISTS diesel_manage_updated_at(_tbl regclass); 6 | DROP FUNCTION IF EXISTS diesel_set_updated_at(); 7 | -------------------------------------------------------------------------------- /examples/web_app_graphql/migrations/00000000000000_diesel_initial_setup/up.sql: -------------------------------------------------------------------------------- 1 | -- This file was automatically created by Diesel to setup helper functions 2 | -- and other internal bookkeeping. This file is safe to edit, any future 3 | -- changes will be added to existing projects as new migrations. 4 | 5 | 6 | 7 | 8 | -- Sets up a trigger for the given table to automatically set a column called 9 | -- `updated_at` whenever the row is modified (unless `updated_at` was included 10 | -- in the modified columns) 11 | -- 12 | -- # Example 13 | -- 14 | -- ```sql 15 | -- CREATE TABLE users (id SERIAL PRIMARY KEY, updated_at TIMESTAMP NOT NULL DEFAULT NOW()); 16 | -- 17 | -- SELECT diesel_manage_updated_at('users'); 18 | -- ``` 19 | CREATE OR REPLACE FUNCTION diesel_manage_updated_at(_tbl regclass) RETURNS VOID AS $$ 20 | BEGIN 21 | EXECUTE format('CREATE TRIGGER set_updated_at BEFORE UPDATE ON %s 22 | FOR EACH ROW EXECUTE PROCEDURE diesel_set_updated_at()', _tbl); 23 | END; 24 | $$ LANGUAGE plpgsql; 25 | 26 | CREATE OR REPLACE FUNCTION diesel_set_updated_at() RETURNS trigger AS $$ 27 | BEGIN 28 | IF ( 29 | NEW IS DISTINCT FROM OLD AND 30 | NEW.updated_at IS NOT DISTINCT FROM OLD.updated_at 31 | ) THEN 32 | NEW.updated_at := current_timestamp; 33 | END IF; 34 | RETURN NEW; 35 | END; 36 | $$ LANGUAGE plpgsql; 37 | -------------------------------------------------------------------------------- /examples/web_app_graphql/migrations/2023-12-20-105441_users/down.sql: -------------------------------------------------------------------------------- 1 | -- This file should undo anything in `up.sql` 2 | drop table if exists users; -------------------------------------------------------------------------------- /examples/web_app_graphql/migrations/2023-12-20-105441_users/up.sql: -------------------------------------------------------------------------------- 1 | -- Your SQL goes here 2 | create table if not exists users( 3 | id serial primary key, 4 | first_name varchar not null, 5 | last_name varchar not null, 6 | email varchar not null unique 7 | ); -------------------------------------------------------------------------------- /examples/web_app_graphql/src/app_env_vars.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | 3 | //Environment variables required for the app to run 4 | #[derive(Deserialize, Debug, Clone)] 5 | pub struct AppEnvVars { 6 | pub meilisearch_api_key: String, 7 | pub meilisearch_host: String, 8 | pub database_url: String, 9 | pub migrations_dir_path: String, 10 | } 11 | -------------------------------------------------------------------------------- /examples/web_app_graphql/src/errors/mod.rs: -------------------------------------------------------------------------------- 1 | use diesel::ConnectionError; 2 | use diesel_async::pooled_connection::deadpool::{BuildError, PoolError}; 3 | use diesel_migrations::MigrationError; 4 | use serde_json::Error as SerdeError; 5 | use thiserror::Error; 6 | 7 | #[derive(Debug, Error)] 8 | pub enum ApplicationError { 9 | #[error("Missing environment variable")] 10 | Envy(#[from] envy::Error), 11 | #[error("Input/Output error")] 12 | Io(#[from] std::io::Error), 13 | #[error("Database error")] 14 | Diesel(#[from] diesel::result::Error), 15 | #[error("Deadpool build error")] 16 | DeadpoolBuild(#[from] BuildError), 17 | #[error("Migration error")] 18 | Migration(#[from] MigrationError), 19 | #[error("Connection error")] 20 | DieselConnection(#[from] ConnectionError), 21 | #[error("Pool Error")] 22 | Pool(#[from] PoolError), 23 | #[error("Serde json error")] 24 | SerDe(#[from] SerdeError), 25 | } 26 | -------------------------------------------------------------------------------- /examples/web_app_graphql/src/graphql_schema/mod.rs: -------------------------------------------------------------------------------- 1 | use async_graphql::SimpleObject; 2 | pub mod users; 3 | 4 | use users::mutation::UsersMut; 5 | use users::query::UsersQuery; 6 | 7 | #[derive(Default, SimpleObject)] 8 | pub struct Query { 9 | users: UsersQuery, 10 | } 11 | 12 | #[derive(Default, SimpleObject)] 13 | pub struct Mutation { 14 | users: UsersMut, 15 | } 16 | -------------------------------------------------------------------------------- /examples/web_app_graphql/src/graphql_schema/users/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod mutation; 2 | pub mod query; 3 | -------------------------------------------------------------------------------- /examples/web_app_graphql/src/graphql_schema/users/mutation/add_user.rs: -------------------------------------------------------------------------------- 1 | use async_graphql::{Context, InputObject, Object, Result}; 2 | use diesel_async::RunQueryDsl; 3 | use validator::Validate; 4 | 5 | use crate::{ 6 | models::{NewUser, User}, 7 | validate_input, GraphQlData, 8 | }; 9 | 10 | #[derive(Default)] 11 | pub struct AddUser; 12 | 13 | #[derive(InputObject, Validate)] 14 | pub struct IAddUser { 15 | #[validate(length(min = 1))] 16 | pub first_name: String, 17 | #[validate(length(min = 1))] 18 | pub last_name: String, 19 | #[validate(email)] 20 | pub email: String, 21 | } 22 | 23 | #[Object] 24 | impl AddUser { 25 | ///Resolver for creating a new user and storing that data in the database 26 | /// 27 | /// The mutation can be run as follows 28 | /// ```gpl 29 | /// mutation AddUser{ 30 | /// users { 31 | /// signup(input: {firstName: "",lastName: "",email: ""}){ 32 | /// id 33 | /// firstName 34 | /// lastName 35 | /// email 36 | /// } 37 | /// } 38 | /// } 39 | pub async fn signup(&self, ctx: &Context<'_>, input: IAddUser) -> Result { 40 | validate_input(&input)?; 41 | 42 | use crate::schema::users::dsl::users; 43 | 44 | let GraphQlData { pool, .. } = ctx.data().map_err(|e| { 45 | log::error!("Failed to get app data: {:?}", e); 46 | e 47 | })?; 48 | 49 | let mut connection = pool.get().await?; 50 | 51 | let value = NewUser { 52 | first_name: input.first_name, 53 | last_name: input.last_name, 54 | email: input.email, 55 | }; 56 | 57 | let result = diesel::insert_into(users) 58 | .values(&value) 59 | .get_result::(&mut connection) 60 | .await 61 | .map_err(|e| { 62 | log::error!("Could not create new user: {:#?}", e); 63 | e 64 | })?; 65 | 66 | Ok(result) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /examples/web_app_graphql/src/graphql_schema/users/mutation/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod add_user; 2 | 3 | use add_user::AddUser; 4 | use async_graphql::MergedObject; 5 | 6 | //Combines user queries into one struct 7 | #[derive(Default, MergedObject)] 8 | pub struct UsersMut(pub AddUser); 9 | -------------------------------------------------------------------------------- /examples/web_app_graphql/src/graphql_schema/users/query/get_users.rs: -------------------------------------------------------------------------------- 1 | use async_graphql::{Context, Object, Result}; 2 | use diesel_async::RunQueryDsl; 3 | 4 | use crate::{models::User, GraphQlData}; 5 | 6 | #[derive(Default)] 7 | pub struct GetUsers; 8 | 9 | #[Object] 10 | impl GetUsers { 11 | //Resolver for querying the database for user records 12 | pub async fn get_users(&self, ctx: &Context<'_>) -> Result> { 13 | use crate::schema::users::dsl::users; 14 | 15 | let GraphQlData { pool, .. } = ctx.data().map_err(|e| { 16 | log::error!("Failed to get app data: {:?}", e); 17 | e 18 | })?; 19 | 20 | let mut connection = pool.get().await?; 21 | 22 | let list_users = users.load::(&mut connection).await?; 23 | 24 | Ok(list_users) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /examples/web_app_graphql/src/graphql_schema/users/query/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod get_users; 2 | pub mod search; 3 | 4 | use async_graphql::MergedObject; 5 | use get_users::GetUsers; 6 | use search::SearchUsers; 7 | 8 | //Combines user queries into one struct 9 | #[derive(Default, MergedObject)] 10 | pub struct UsersQuery(pub GetUsers, pub SearchUsers); 11 | -------------------------------------------------------------------------------- /examples/web_app_graphql/src/graphql_schema/users/query/search.rs: -------------------------------------------------------------------------------- 1 | use async_graphql::{Context, Object, Result}; 2 | use diesel_async::RunQueryDsl; 3 | use meilisearch_sdk::search::{SearchQuery, SearchResults}; 4 | 5 | use crate::{models::User, GraphQlData}; 6 | 7 | #[derive(Default)] 8 | pub struct SearchUsers; 9 | 10 | #[Object] 11 | impl SearchUsers { 12 | async fn search(&self, ctx: &Context<'_>, query_string: String) -> Result> { 13 | use crate::schema::users::dsl::users; 14 | 15 | let GraphQlData { pool, client } = ctx.data().map_err(|e| { 16 | log::error!("Failed to get app data: {:?}", e); 17 | e 18 | })?; 19 | 20 | let mut connection = pool.get().await?; 21 | 22 | let list_users = users.load::(&mut connection).await?; 23 | 24 | match client.get_index("users").await { 25 | //If getting the index is successful, we add documents to it 26 | Ok(index) => { 27 | index.add_documents(&list_users, Some("id")).await?; 28 | } 29 | 30 | //If getting the index fails, we create it and then add documents to the new index 31 | Err(_) => { 32 | let task = client.create_index("users", Some("id")).await?; 33 | let task = task.wait_for_completion(client, None, None).await?; 34 | let index = task.try_make_index(client).unwrap(); 35 | 36 | index.add_documents(&list_users, Some("id")).await?; 37 | } 38 | } 39 | 40 | let index = client.get_index("users").await?; 41 | 42 | //We build the query 43 | let query = SearchQuery::new(&index).with_query(&query_string).build(); 44 | 45 | let results: SearchResults = index.execute_query(&query).await?; 46 | 47 | //Tranform the results into a type that implements OutputType 48 | //Required for return types to implement this trait 49 | let search_results: Vec = results 50 | .hits 51 | .into_iter() 52 | .map(|hit| User { 53 | id: hit.result.id, 54 | email: hit.result.email, 55 | first_name: hit.result.first_name, 56 | last_name: hit.result.last_name, 57 | }) 58 | .collect(); 59 | 60 | Ok(search_results) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /examples/web_app_graphql/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod app_env_vars; 2 | pub mod errors; 3 | mod graphql_schema; 4 | mod models; 5 | mod schema; 6 | 7 | use actix_web::{web, HttpResponse, Result}; 8 | use app_env_vars::AppEnvVars; 9 | use async_graphql::{ 10 | http::GraphiQLSource, EmptySubscription, Error, Result as GraphqlResult, Schema, 11 | }; 12 | use async_graphql_actix_web::{GraphQLRequest, GraphQLResponse}; 13 | use diesel_async::{ 14 | pooled_connection::{deadpool::Pool, AsyncDieselConnectionManager}, 15 | AsyncPgConnection, 16 | }; 17 | use errors::ApplicationError; 18 | use graphql_schema::{Mutation, Query}; 19 | use meilisearch_sdk::Client as SearchClient; 20 | use validator::Validate; 21 | 22 | pub type ApplicationSchema = Schema; 23 | 24 | /// Represents application data passed to graphql resolvers 25 | pub struct GraphQlData { 26 | pub pool: Pool, 27 | pub client: SearchClient, 28 | } 29 | 30 | pub async fn index_graphiql() -> Result { 31 | Ok(HttpResponse::Ok() 32 | .content_type("text/html; charset=utf-8") 33 | .body(GraphiQLSource::build().endpoint("/").finish())) 34 | } 35 | 36 | pub async fn index(schema: web::Data, req: GraphQLRequest) -> GraphQLResponse { 37 | let req_inner = req.into_inner(); 38 | 39 | schema.execute(req_inner).await.into() 40 | } 41 | 42 | /// We build the graphql schema and any data required to be passed to all resolvers 43 | pub fn build_schema(app_env_vars: &AppEnvVars) -> Result { 44 | let client = SearchClient::new( 45 | &app_env_vars.meilisearch_host, 46 | Some(&app_env_vars.meilisearch_api_key), 47 | ); 48 | 49 | let config = AsyncDieselConnectionManager::::new(&app_env_vars.database_url); 50 | let pool = Pool::builder(config).build()?; 51 | 52 | let schema_data = GraphQlData { pool, client }; 53 | 54 | Ok( 55 | Schema::build(Query::default(), Mutation::default(), EmptySubscription) 56 | .data(schema_data) 57 | .finish(), 58 | ) 59 | } 60 | 61 | /// Helper function for returning an error if inputs do not match the set conditions 62 | pub fn validate_input(input: &T) -> GraphqlResult<()> { 63 | if let Err(e) = input.validate() { 64 | log::error!("Validation error: {}", e); 65 | let err = serde_json::to_string(&e).unwrap(); 66 | let err = Error::from(err); 67 | return Err(err); 68 | } 69 | Ok(()) 70 | } 71 | -------------------------------------------------------------------------------- /examples/web_app_graphql/src/main.rs: -------------------------------------------------------------------------------- 1 | use actix_cors::Cors; 2 | use actix_web::middleware::Logger; 3 | use actix_web::web; 4 | use actix_web::{guard, App, HttpServer}; 5 | use diesel::migration::MigrationSource; 6 | use diesel::{Connection, PgConnection}; 7 | use diesel_migrations::FileBasedMigrations; 8 | use meilisearch_ex::{ 9 | app_env_vars::AppEnvVars, build_schema, errors::ApplicationError, index, index_graphiql, 10 | }; 11 | 12 | #[actix_web::main] 13 | async fn main() -> Result<(), ApplicationError> { 14 | let _ = dotenvy::dotenv(); 15 | 16 | let app_env_vars = envy::from_env::()?; 17 | 18 | env_logger::init_from_env(env_logger::Env::new().default_filter_or("info")); 19 | 20 | //Run migrations on app start 21 | let mut db_connection = PgConnection::establish(&app_env_vars.database_url)?; 22 | let mut migrations = FileBasedMigrations::from_path(&app_env_vars.migrations_dir_path)? 23 | .migrations() 24 | .unwrap(); 25 | 26 | migrations.sort_by_key(|m| m.name().to_string()); 27 | 28 | for migration in migrations { 29 | migration.run(&mut db_connection).unwrap(); 30 | } 31 | 32 | let schema = build_schema(&app_env_vars)?; 33 | 34 | println!("GraphiQL IDE: http://localhost:8081"); 35 | 36 | HttpServer::new(move || { 37 | App::new() 38 | .wrap(Logger::default()) 39 | .wrap( 40 | Cors::default() 41 | .allow_any_origin() 42 | .allow_any_method() 43 | .allow_any_header() 44 | .max_age(3600) 45 | .supports_credentials(), 46 | ) 47 | //Add schema to application `Data` extractor 48 | .app_data(web::Data::new(schema.clone())) 49 | .service(web::resource("/").guard(guard::Post()).to(index)) 50 | .service(web::resource("/").guard(guard::Get()).to(index_graphiql)) 51 | }) 52 | .bind("0.0.0.0:8081")? 53 | .run() 54 | .await?; 55 | 56 | Ok(()) 57 | } 58 | -------------------------------------------------------------------------------- /examples/web_app_graphql/src/models.rs: -------------------------------------------------------------------------------- 1 | use async_graphql::SimpleObject; 2 | use diesel::{prelude::Insertable, Queryable, Selectable}; 3 | use serde::{Deserialize, Serialize}; 4 | 5 | use crate::schema::users; 6 | 7 | //Struct that corresponds to our database structure for users table 8 | #[derive(SimpleObject, Deserialize, Serialize, Queryable, Selectable, Debug)] 9 | #[diesel(table_name = users)] 10 | pub struct User { 11 | pub id: i32, 12 | pub first_name: String, 13 | pub last_name: String, 14 | pub email: String, 15 | } 16 | 17 | #[derive(Insertable, Debug)] 18 | #[diesel(table_name = users)] 19 | pub struct NewUser { 20 | pub first_name: String, 21 | pub last_name: String, 22 | pub email: String, 23 | } 24 | -------------------------------------------------------------------------------- /examples/web_app_graphql/src/schema.rs: -------------------------------------------------------------------------------- 1 | // @generated automatically by Diesel CLI. 2 | 3 | diesel::table! { 4 | users (id) { 5 | id -> Int4, 6 | first_name -> Varchar, 7 | last_name -> Varchar, 8 | email -> Varchar, 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /meilisearch-index-setting-macro/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "meilisearch-index-setting-macro" 3 | version = "0.28.0" 4 | description = "Helper tool to generate settings of a Meilisearch index" 5 | edition = "2021" 6 | license = "MIT" 7 | repository = "https://github.com/meilisearch/meilisearch-rust" 8 | 9 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 10 | 11 | [lib] 12 | proc-macro = true 13 | 14 | [dependencies] 15 | syn = { version = "2.0.48", features = ["extra-traits"] } 16 | quote = "1.0.21" 17 | proc-macro2 = "1.0.46" 18 | convert_case = "0.6.0" 19 | structmeta = "0.3" 20 | -------------------------------------------------------------------------------- /meilisearch-index-setting-macro/src/lib.rs: -------------------------------------------------------------------------------- 1 | use convert_case::{Case, Casing}; 2 | use proc_macro2::Ident; 3 | use quote::quote; 4 | use structmeta::{Flag, StructMeta}; 5 | use syn::{parse_macro_input, spanned::Spanned}; 6 | 7 | #[derive(Clone, StructMeta, Default)] 8 | struct FieldAttrs { 9 | primary_key: Flag, 10 | displayed: Flag, 11 | searchable: Flag, 12 | distinct: Flag, 13 | filterable: Flag, 14 | sortable: Flag, 15 | } 16 | 17 | #[proc_macro_derive(IndexConfig, attributes(index_config))] 18 | pub fn generate_index_settings(input: proc_macro::TokenStream) -> proc_macro::TokenStream { 19 | let ast = parse_macro_input!(input as syn::DeriveInput); 20 | 21 | let fields: &syn::Fields = match ast.data { 22 | syn::Data::Struct(ref data) => &data.fields, 23 | _ => { 24 | return proc_macro::TokenStream::from( 25 | syn::Error::new(ast.ident.span(), "Applicable only to struct").to_compile_error(), 26 | ); 27 | } 28 | }; 29 | 30 | let struct_ident = &ast.ident; 31 | 32 | let index_config_implementation = get_index_config_implementation(struct_ident, fields); 33 | proc_macro::TokenStream::from(quote! { 34 | #index_config_implementation 35 | }) 36 | } 37 | 38 | fn get_index_config_implementation( 39 | struct_ident: &Ident, 40 | fields: &syn::Fields, 41 | ) -> proc_macro2::TokenStream { 42 | let mut primary_key_attribute = String::new(); 43 | let mut distinct_key_attribute = String::new(); 44 | let mut displayed_attributes = vec![]; 45 | let mut searchable_attributes = vec![]; 46 | let mut filterable_attributes = vec![]; 47 | let mut sortable_attributes = vec![]; 48 | 49 | let index_name = struct_ident 50 | .to_string() 51 | .from_case(Case::UpperCamel) 52 | .to_case(Case::Snake); 53 | 54 | let mut primary_key_found = false; 55 | let mut distinct_found = false; 56 | 57 | for field in fields { 58 | let attrs = field 59 | .attrs 60 | .iter() 61 | .filter(|attr| attr.path().is_ident("index_config")) 62 | .map(|attr| attr.parse_args::().unwrap()) 63 | .collect::>() 64 | .first() 65 | .cloned() 66 | .unwrap_or_default(); 67 | 68 | // Check if the primary key field is unique 69 | if attrs.primary_key.value() { 70 | if primary_key_found { 71 | return syn::Error::new( 72 | field.span(), 73 | "Only one field can be marked as primary key", 74 | ) 75 | .to_compile_error(); 76 | } 77 | primary_key_attribute = field.ident.clone().unwrap().to_string(); 78 | primary_key_found = true; 79 | } 80 | 81 | // Check if the distinct field is unique 82 | if attrs.distinct.value() { 83 | if distinct_found { 84 | return syn::Error::new(field.span(), "Only one field can be marked as distinct") 85 | .to_compile_error(); 86 | } 87 | distinct_key_attribute = field.ident.clone().unwrap().to_string(); 88 | distinct_found = true; 89 | } 90 | 91 | if attrs.displayed.value() { 92 | displayed_attributes.push(field.ident.clone().unwrap().to_string()); 93 | } 94 | 95 | if attrs.searchable.value() { 96 | searchable_attributes.push(field.ident.clone().unwrap().to_string()); 97 | } 98 | 99 | if attrs.filterable.value() { 100 | filterable_attributes.push(field.ident.clone().unwrap().to_string()); 101 | } 102 | 103 | if attrs.sortable.value() { 104 | sortable_attributes.push(field.ident.clone().unwrap().to_string()); 105 | } 106 | } 107 | 108 | let primary_key_token: proc_macro2::TokenStream = if primary_key_attribute.is_empty() { 109 | quote! { 110 | ::std::option::Option::None 111 | } 112 | } else { 113 | quote! { 114 | ::std::option::Option::Some(#primary_key_attribute) 115 | } 116 | }; 117 | 118 | let display_attr_tokens = 119 | get_settings_token_for_list(&displayed_attributes, "with_displayed_attributes"); 120 | let sortable_attr_tokens = 121 | get_settings_token_for_list(&sortable_attributes, "with_sortable_attributes"); 122 | let filterable_attr_tokens = 123 | get_settings_token_for_list(&filterable_attributes, "with_filterable_attributes"); 124 | let searchable_attr_tokens = 125 | get_settings_token_for_list(&searchable_attributes, "with_searchable_attributes"); 126 | let distinct_attr_token = get_settings_token_for_string_for_some_string( 127 | &distinct_key_attribute, 128 | "with_distinct_attribute", 129 | ); 130 | 131 | quote! { 132 | #[::meilisearch_sdk::macro_helper::async_trait(?Send)] 133 | impl ::meilisearch_sdk::documents::IndexConfig for #struct_ident { 134 | const INDEX_STR: &'static str = #index_name; 135 | 136 | fn generate_settings() -> ::meilisearch_sdk::settings::Settings { 137 | ::meilisearch_sdk::settings::Settings::new() 138 | #display_attr_tokens 139 | #sortable_attr_tokens 140 | #filterable_attr_tokens 141 | #searchable_attr_tokens 142 | #distinct_attr_token 143 | } 144 | 145 | async fn generate_index(client: &::meilisearch_sdk::client::Client) -> std::result::Result<::meilisearch_sdk::indexes::Index, ::meilisearch_sdk::tasks::Task> { 146 | return client.create_index(#index_name, #primary_key_token) 147 | .await.unwrap() 148 | .wait_for_completion(&client, ::std::option::Option::None, ::std::option::Option::None) 149 | .await.unwrap() 150 | .try_make_index(&client); 151 | } 152 | } 153 | } 154 | } 155 | 156 | fn get_settings_token_for_list( 157 | field_name_list: &[String], 158 | method_name: &str, 159 | ) -> proc_macro2::TokenStream { 160 | let string_attributes = field_name_list.iter().map(|attr| { 161 | quote! { 162 | #attr 163 | } 164 | }); 165 | let method_ident = Ident::new(method_name, proc_macro2::Span::call_site()); 166 | 167 | if field_name_list.is_empty() { 168 | quote! { 169 | .#method_ident(::std::iter::empty::<&str>()) 170 | } 171 | } else { 172 | quote! { 173 | .#method_ident([#(#string_attributes),*]) 174 | } 175 | } 176 | } 177 | 178 | fn get_settings_token_for_string_for_some_string( 179 | field_name: &String, 180 | method_name: &str, 181 | ) -> proc_macro2::TokenStream { 182 | let method_ident = Ident::new(method_name, proc_macro2::Span::call_site()); 183 | 184 | if field_name.is_empty() { 185 | proc_macro2::TokenStream::new() 186 | } else { 187 | quote! { 188 | .#method_ident(::std::option::Option::Some(#field_name)) 189 | } 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /meilisearch-test-macro/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "meilisearch-test-macro" 3 | version = "0.0.0" 4 | edition = "2021" 5 | publish = false 6 | 7 | [lib] 8 | proc-macro = true 9 | 10 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 11 | 12 | [dependencies] 13 | proc-macro2 = "1.0.0" 14 | quote = "1.0.0" 15 | syn = { version = "2.0.48", features = ["clone-impls", "full", "parsing", "printing", "proc-macro"], default-features = false } 16 | -------------------------------------------------------------------------------- /meilisearch-test-macro/README.md: -------------------------------------------------------------------------------- 1 | # Meilisearch test macro 2 | 3 | This crate defines the `meilisearch_test` macro. 4 | 5 | Since the code is a little bit harsh to read, here is a complete explanation of how to use it. 6 | The macro aims to ease the writing of tests by: 7 | 8 | 1. Reducing the amount of code you need to write and maintain for each test. 9 | 2. Ensuring All your indexes as a unique name so they can all run in parallel. 10 | 3. Ensuring you never forget to delete your index if you need one. 11 | 12 | Before explaining its usage, we're going to see a simple test _before_ this macro: 13 | 14 | ```rust 15 | #[async_test] 16 | async fn test_get_tasks() -> Result<(), Error> { 17 | let client = Client::new(MEILISEARCH_URL, MEILISEARCH_API_KEY); 18 | 19 | let index = client 20 | .create_index("test_get_tasks", None) 21 | .await? 22 | .wait_for_completion(&client, None, None) 23 | .await? 24 | .try_make_index(&client) 25 | .unwrap(); 26 | 27 | let tasks = index.get_tasks().await?; 28 | // The only task is the creation of the index 29 | assert_eq!(status.results.len(), 1); 30 | 31 | index.delete() 32 | .await? 33 | .wait_for_completion(&client, None, None) 34 | .await?; 35 | Ok(()) 36 | } 37 | ``` 38 | 39 | I have multiple problems with this test: 40 | 41 | - `let client = Client::new(MEILISEARCH_URL, MEILISEARCH_API_KEY);`: This line is always the same in every test. 42 | And if you make a typo on the http addr or the master key, you'll have an error. 43 | - `let index = client.create_index("test_get_tasks", None)...`: Each test needs to have an unique name. 44 | This means we currently need to write the name of the test everywhere; it's not practical. 45 | - There are 11 lines dedicated to the creation and deletion of the index; this is once again something that'll never change 46 | whatever the test is. But, if you ever forget to delete the index at the end, you'll get in some trouble to re-run 47 | the tests. 48 | 49 | --- 50 | 51 | With this macro, all these problems are solved. See a rewrite of this test: 52 | 53 | ```rust 54 | #[meilisearch_test] 55 | async fn test_get_tasks(index: Index, client: Client) -> Result<(), Error> { 56 | let tasks = index.get_tasks().await?; 57 | // The only task is the creation of the index 58 | assert_eq!(status.results.len(), 1); 59 | } 60 | ``` 61 | 62 | So now you're probably seeing what happened. By using an index and a client in the parameter of 63 | the test, the macro automatically did the same thing we've seen before. 64 | There are a few rules, though: 65 | 66 | 1. The macro only handles three types of arguments: 67 | 68 | - `String`: It returns the name of the test. 69 | - `Client`: It creates a client like that: `Client::new("http://localhost:7700", "masterKey")`. 70 | - `Index`: It creates and deletes an index, as we've seen before. 71 | 72 | 2. You only get what you asked for. That means if you don't ask for an index, no index will be created in meilisearch. 73 | So, if you are testing the creation of indexes, you can ask for a `Client` and a `String` and then create it yourself. 74 | The index won't be present in meilisearch. 75 | 3. You can put your parameters in the order you want it won't change anything. 76 | 4. Everything you use **must** be in scope directly. If you're using an `Index`, you must write `Index` in the parameters, 77 | not `meilisearch_rust::Index` or `crate::Index`. 78 | 5. And I think that's all, use and abuse it 🎉 79 | -------------------------------------------------------------------------------- /meilisearch-test-macro/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![recursion_limit = "4096"] 2 | 3 | extern crate proc_macro; 4 | 5 | use proc_macro::TokenStream; 6 | use proc_macro2::Span; 7 | use quote::quote; 8 | use syn::{ 9 | parse_macro_input, parse_quote, Expr, FnArg, Ident, Item, PatType, Path, Stmt, Type, TypePath, 10 | Visibility, 11 | }; 12 | 13 | #[proc_macro_attribute] 14 | pub fn meilisearch_test(params: TokenStream, input: TokenStream) -> TokenStream { 15 | assert!( 16 | params.is_empty(), 17 | "the #[async_test] attribute currently does not take parameters" 18 | ); 19 | 20 | let mut inner = parse_macro_input!(input as Item); 21 | let mut outer = inner.clone(); 22 | if let (&mut Item::Fn(ref mut inner_fn), &mut Item::Fn(ref mut outer_fn)) = 23 | (&mut inner, &mut outer) 24 | { 25 | #[derive(Debug, PartialEq, Eq)] 26 | enum Param { 27 | Client, 28 | Index, 29 | String, 30 | } 31 | 32 | inner_fn.sig.ident = Ident::new( 33 | &("_inner_meilisearch_test_macro_".to_string() + &inner_fn.sig.ident.to_string()), 34 | Span::call_site(), 35 | ); 36 | let inner_ident = &inner_fn.sig.ident; 37 | inner_fn.vis = Visibility::Inherited; 38 | inner_fn.attrs.clear(); 39 | assert!( 40 | outer_fn.sig.asyncness.take().is_some(), 41 | "#[meilisearch_test] can only be applied to async functions" 42 | ); 43 | 44 | let mut params = Vec::new(); 45 | 46 | let parameters = &inner_fn.sig.inputs; 47 | for param in parameters { 48 | match param { 49 | FnArg::Typed(PatType { ty, .. }) => match &**ty { 50 | Type::Path(TypePath { path: Path { segments, .. }, .. } ) if segments.last().unwrap().ident == "String" => { 51 | params.push(Param::String); 52 | } 53 | Type::Path(TypePath { path: Path { segments, .. }, .. } ) if segments.last().unwrap().ident == "Index" => { 54 | params.push(Param::Index); 55 | } 56 | Type::Path(TypePath { path: Path { segments, .. }, .. } ) if segments.last().unwrap().ident == "Client" => { 57 | params.push(Param::Client); 58 | } 59 | // TODO: throw this error while pointing to the specific token 60 | ty => panic!( 61 | "#[meilisearch_test] can only receive Client, Index or String as parameters but received {ty:?}" 62 | ), 63 | }, 64 | // TODO: throw this error while pointing to the specific token 65 | // Used `self` as a parameter 66 | FnArg::Receiver(_) => panic!( 67 | "#[meilisearch_test] can only receive Client, Index or String as parameters" 68 | ), 69 | } 70 | } 71 | 72 | // if a `Client` or an `Index` was asked for the test we must create a meilisearch `Client`. 73 | let use_client = params 74 | .iter() 75 | .any(|param| matches!(param, Param::Client | Param::Index)); 76 | // if a `String` or an `Index` was asked then we need to extract the name of the test function. 77 | let use_name = params 78 | .iter() 79 | .any(|param| matches!(param, Param::String | Param::Index)); 80 | let use_index = params.contains(&Param::Index); 81 | 82 | // Now we are going to build the body of the outer function 83 | let mut outer_block: Vec = Vec::new(); 84 | 85 | // First we need to check if a client will be used and create it if it’s the case 86 | if use_client { 87 | outer_block.push(parse_quote!( 88 | let meilisearch_url = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); 89 | )); 90 | outer_block.push(parse_quote!( 91 | let meilisearch_api_key = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); 92 | )); 93 | outer_block.push(parse_quote!( 94 | let client = Client::new(meilisearch_url, Some(meilisearch_api_key)).unwrap(); 95 | )); 96 | } 97 | 98 | // Now we do the same for the index name 99 | if use_name { 100 | let fn_name = &outer_fn.sig.ident; 101 | // the name we're going to return is the complete path to the function i.e., something like that; 102 | // `indexes::tests::test_fetch_info` but since the `::` are not allowed by meilisearch as an index 103 | // name we're going to rename that to `indexes-tests-test_fetch_info`. 104 | outer_block.push(parse_quote!( 105 | let name = format!("{}::{}", std::module_path!(), stringify!(#fn_name)).replace("::", "-"); 106 | )); 107 | } 108 | 109 | // And finally if an index was asked, we delete it, and we (re)create it and wait until meilisearch confirm its creation. 110 | if use_index { 111 | outer_block.push(parse_quote!({ 112 | let res = client 113 | .delete_index(&name) 114 | .await 115 | .expect("Network issue while sending the delete index task") 116 | .wait_for_completion(&client, None, None) 117 | .await 118 | .expect("Network issue while waiting for the index deletion"); 119 | if res.is_failure() { 120 | let error = res.unwrap_failure(); 121 | assert_eq!( 122 | error.error_code, 123 | crate::errors::ErrorCode::IndexNotFound, 124 | "{:?}", 125 | error 126 | ); 127 | } 128 | })); 129 | 130 | outer_block.push(parse_quote!( 131 | let index = client 132 | .create_index(&name, None) 133 | .await 134 | .expect("Network issue while sending the create index task") 135 | .wait_for_completion(&client, None, None) 136 | .await 137 | .expect("Network issue while waiting for the index creation") 138 | .try_make_index(&client) 139 | .expect("Could not create the index out of the create index task"); 140 | )); 141 | } 142 | 143 | // Create a list of params separated by comma with the name we defined previously. 144 | let params: Vec = params 145 | .into_iter() 146 | .map(|param| match param { 147 | Param::Client => parse_quote!(client), 148 | Param::Index => parse_quote!(index), 149 | Param::String => parse_quote!(name), 150 | }) 151 | .collect(); 152 | 153 | // Now we can call the user code with our parameters :tada: 154 | outer_block.push(parse_quote!( 155 | let result = #inner_ident(#(#params.clone()),*).await; 156 | )); 157 | 158 | // And right before the end, if an index was created and the tests successfully executed we delete it. 159 | if use_index { 160 | outer_block.push(parse_quote!( 161 | index 162 | .delete() 163 | .await 164 | .expect("Network issue while sending the last delete index task"); 165 | // we early exit the test here and let meilisearch handle the deletion asynchronously 166 | )); 167 | } 168 | 169 | // Finally, for the great finish we just return the result the user gave us. 170 | outer_block.push(parse_quote!(return result;)); 171 | 172 | outer_fn.sig.inputs.clear(); 173 | outer_fn.sig.asyncness = inner_fn.sig.asyncness; 174 | outer_fn.attrs.push(parse_quote!(#[tokio::test])); 175 | outer_fn.block.stmts = outer_block; 176 | } else { 177 | panic!("#[meilisearch_test] can only be applied to async functions") 178 | } 179 | quote!( 180 | #inner 181 | #outer 182 | ) 183 | .into() 184 | } 185 | -------------------------------------------------------------------------------- /scripts/check-readme.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Checking that cargo is installed 4 | command -v cargo > /dev/null 2>&1 5 | if [ "$?" -ne 0 ]; then 6 | echo 'You must install cargo to make this script working.' 7 | echo 'See https://doc.rust-lang.org/cargo/getting-started/installation.html' 8 | exit 1 9 | fi 10 | 11 | # Installing cargo-readme if it's not installed yet 12 | cargo install cargo-readme 13 | 14 | # Comparing the generated README and the current one 15 | current_readme="README.md" 16 | generated_readme="README.md_tmp" 17 | cargo readme > "$generated_readme" 18 | 19 | # Exiting with the right message 20 | echo '' 21 | diff "$current_readme" "$generated_readme" > /dev/null 2>&1 22 | if [ "$?" = 0 ]; then 23 | echo "OK" 24 | rm -f "$generated_readme" 25 | exit 0 26 | else 27 | echo "The current README.md is not up-to-date with the template." 28 | 29 | # Displaying the diff if the --diff flag is activated 30 | if [ "$1" = '--diff' ]; then 31 | echo 'Diff found:' 32 | diff "$current_readme" "$generated_readme" 33 | else 34 | echo 'To see the diff, run:' 35 | echo ' $ sh scripts/check-readme.sh --diff' 36 | echo 'To update the README, run:' 37 | echo ' $ sh scripts/update-readme.sh' 38 | fi 39 | 40 | rm -f "$generated_readme" 41 | exit 1 42 | fi 43 | -------------------------------------------------------------------------------- /scripts/update-readme.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Checking that cargo is installed 4 | command -v cargo > /dev/null 2>&1 5 | if [ "$?" -ne 0 ]; then 6 | echo 'You must install cargo to make this script working.' 7 | echo 'See https://doc.rust-lang.org/cargo/getting-started/installation.html' 8 | exit 9 | fi 10 | 11 | # Installing cargo-readme if it's not installed yet 12 | cargo install cargo-readme 13 | 14 | # Generating the README.md file 15 | cargo readme > README.md 16 | -------------------------------------------------------------------------------- /scripts/update_macro_versions.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | new_version=$(grep '^version = ' Cargo.toml) 3 | 4 | # Updates the versions in meilisearch-rust and meilisearch-index-setting-macro of the latter, with the latest meilisearch-rust version. 5 | 6 | old_index_macro_version=$(grep '^version = ' ./meilisearch-index-setting-macro/Cargo.toml) 7 | old_macro_in_sdk_version=$(grep '{ path = "meilisearch-index-setting-macro", version =' ./Cargo.toml) 8 | 9 | sed -i '' -e "s/^$old_index_macro_version/$new_version/g" './meilisearch-index-setting-macro/Cargo.toml' 10 | sed -i '' -e "s/$old_macro_in_sdk_version/meilisearch-index-setting-macro = { path = \"meilisearch-index-setting-macro\", $new_version }/g" './Cargo.toml' 11 | -------------------------------------------------------------------------------- /src/documents.rs: -------------------------------------------------------------------------------- 1 | use async_trait::async_trait; 2 | use serde::{de::DeserializeOwned, Deserialize, Serialize}; 3 | 4 | /// Derive the [`IndexConfig`] trait. 5 | /// 6 | /// ## Field attribute 7 | /// Use the `#[index_config(..)]` field attribute to generate the correct settings 8 | /// for each field. The available parameters are: 9 | /// - `primary_key` (can only be used once) 10 | /// - `distinct` (can only be used once) 11 | /// - `searchable` 12 | /// - `displayed` 13 | /// - `filterable` 14 | /// - `sortable` 15 | /// 16 | /// ## Index name 17 | /// The name of the index will be the name of the struct converted to snake case. 18 | /// 19 | /// ## Sample usage: 20 | /// ``` 21 | /// use serde::{Serialize, Deserialize}; 22 | /// use meilisearch_sdk::documents::IndexConfig; 23 | /// use meilisearch_sdk::settings::Settings; 24 | /// use meilisearch_sdk::indexes::Index; 25 | /// use meilisearch_sdk::client::Client; 26 | /// 27 | /// #[derive(Serialize, Deserialize, IndexConfig)] 28 | /// struct Movie { 29 | /// #[index_config(primary_key)] 30 | /// movie_id: u64, 31 | /// #[index_config(displayed, searchable)] 32 | /// title: String, 33 | /// #[index_config(displayed)] 34 | /// description: String, 35 | /// #[index_config(filterable, sortable, displayed)] 36 | /// release_date: String, 37 | /// #[index_config(filterable, displayed)] 38 | /// genres: Vec, 39 | /// } 40 | /// 41 | /// async fn usage(client: Client) { 42 | /// // Default settings with the distinct, searchable, displayed, filterable, and sortable fields set correctly. 43 | /// let settings: Settings = Movie::generate_settings(); 44 | /// // Index created with the name `movie` and the primary key set to `movie_id` 45 | /// let index: Index = Movie::generate_index(&client).await.unwrap(); 46 | /// } 47 | /// ``` 48 | pub use meilisearch_index_setting_macro::IndexConfig; 49 | 50 | use crate::client::Client; 51 | use crate::request::HttpClient; 52 | use crate::settings::Settings; 53 | use crate::task_info::TaskInfo; 54 | use crate::tasks::Task; 55 | use crate::{errors::Error, indexes::Index}; 56 | 57 | #[async_trait(?Send)] 58 | pub trait IndexConfig { 59 | const INDEX_STR: &'static str; 60 | 61 | #[must_use] 62 | fn index(client: &Client) -> Index { 63 | client.index(Self::INDEX_STR) 64 | } 65 | fn generate_settings() -> Settings; 66 | async fn generate_index(client: &Client) -> Result, Task>; 67 | } 68 | 69 | #[derive(Debug, Clone, Deserialize)] 70 | pub struct DocumentsResults { 71 | pub results: Vec, 72 | pub limit: u32, 73 | pub offset: u32, 74 | pub total: u32, 75 | } 76 | 77 | #[derive(Debug, Clone, Serialize)] 78 | pub struct DocumentQuery<'a, Http: HttpClient> { 79 | #[serde(skip_serializing)] 80 | pub index: &'a Index, 81 | 82 | /// The fields that should appear in the documents. By default, all of the fields are present. 83 | #[serde(skip_serializing_if = "Option::is_none")] 84 | pub fields: Option>, 85 | } 86 | 87 | impl<'a, Http: HttpClient> DocumentQuery<'a, Http> { 88 | #[must_use] 89 | pub fn new(index: &Index) -> DocumentQuery { 90 | DocumentQuery { 91 | index, 92 | fields: None, 93 | } 94 | } 95 | 96 | /// Specify the fields to return in the document. 97 | /// 98 | /// # Example 99 | /// 100 | /// ``` 101 | /// # use meilisearch_sdk::{client::*, indexes::*, documents::*}; 102 | /// # 103 | /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); 104 | /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); 105 | /// # 106 | /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); 107 | /// let index = client.index("document_query_with_fields"); 108 | /// let mut document_query = DocumentQuery::new(&index); 109 | /// 110 | /// document_query.with_fields(["title"]); 111 | /// ``` 112 | pub fn with_fields( 113 | &mut self, 114 | fields: impl IntoIterator, 115 | ) -> &mut DocumentQuery<'a, Http> { 116 | self.fields = Some(fields.into_iter().collect()); 117 | self 118 | } 119 | 120 | /// Execute the get document query. 121 | /// 122 | /// # Example 123 | /// 124 | /// ``` 125 | /// # use meilisearch_sdk::{client::*, indexes::*, documents::*}; 126 | /// # use serde::{Deserialize, Serialize}; 127 | /// # 128 | /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); 129 | /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); 130 | /// # 131 | /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); 132 | /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { 133 | /// #[derive(Debug, Serialize, Deserialize, PartialEq)] 134 | /// struct MyObject { 135 | /// id: String, 136 | /// kind: String, 137 | /// } 138 | /// 139 | /// #[derive(Debug, Serialize, Deserialize, PartialEq)] 140 | /// struct MyObjectReduced { 141 | /// id: String, 142 | /// } 143 | /// # let index = client.index("document_query_execute"); 144 | /// # index.add_or_replace(&[MyObject{id:"1".to_string(), kind:String::from("a kind")},MyObject{id:"2".to_string(), kind:String::from("some kind")}], None).await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); 145 | /// 146 | /// let document = DocumentQuery::new(&index).with_fields(["id"]) 147 | /// .execute::("1") 148 | /// .await 149 | /// .unwrap(); 150 | /// 151 | /// assert_eq!( 152 | /// document, 153 | /// MyObjectReduced { id: "1".to_string() } 154 | /// ); 155 | /// # index.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); 156 | /// # }); 157 | pub async fn execute( 158 | &self, 159 | document_id: &str, 160 | ) -> Result { 161 | self.index.get_document_with::(document_id, self).await 162 | } 163 | } 164 | 165 | #[derive(Debug, Clone, Serialize)] 166 | pub struct DocumentsQuery<'a, Http: HttpClient> { 167 | #[serde(skip_serializing)] 168 | pub index: &'a Index, 169 | 170 | /// The number of documents to skip. 171 | /// 172 | /// If the value of the parameter `offset` is `n`, the `n` first documents will not be returned. 173 | /// This is helpful for pagination. 174 | /// 175 | /// Example: If you want to skip the first document, set offset to `1`. 176 | #[serde(skip_serializing_if = "Option::is_none")] 177 | pub offset: Option, 178 | 179 | /// The maximum number of documents returned. 180 | /// If the value of the parameter `limit` is `n`, there will never be more than `n` documents in the response. 181 | /// This is helpful for pagination. 182 | /// 183 | /// Example: If you don't want to get more than two documents, set limit to `2`. 184 | /// 185 | /// **Default: `20`** 186 | #[serde(skip_serializing_if = "Option::is_none")] 187 | pub limit: Option, 188 | 189 | /// The fields that should appear in the documents. By default, all of the fields are present. 190 | #[serde(skip_serializing_if = "Option::is_none")] 191 | pub fields: Option>, 192 | 193 | /// Filters to apply. 194 | /// 195 | /// Available since v1.2 of Meilisearch 196 | /// Read the [dedicated guide](https://www.meilisearch.com/docs/learn/fine_tuning_results/filtering#filter-basics) to learn the syntax. 197 | #[serde(skip_serializing_if = "Option::is_none")] 198 | pub filter: Option<&'a str>, 199 | } 200 | 201 | impl<'a, Http: HttpClient> DocumentsQuery<'a, Http> { 202 | #[must_use] 203 | pub fn new(index: &Index) -> DocumentsQuery { 204 | DocumentsQuery { 205 | index, 206 | offset: None, 207 | limit: None, 208 | fields: None, 209 | filter: None, 210 | } 211 | } 212 | 213 | /// Specify the offset. 214 | /// 215 | /// # Example 216 | /// 217 | /// ``` 218 | /// # use meilisearch_sdk::{client::*, indexes::*, documents::*}; 219 | /// # 220 | /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); 221 | /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); 222 | /// # 223 | /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); 224 | /// let index = client.index("my_index"); 225 | /// 226 | /// let mut documents_query = DocumentsQuery::new(&index).with_offset(1); 227 | /// ``` 228 | pub fn with_offset(&mut self, offset: usize) -> &mut DocumentsQuery<'a, Http> { 229 | self.offset = Some(offset); 230 | self 231 | } 232 | 233 | /// Specify the limit. 234 | /// 235 | /// # Example 236 | /// 237 | /// ``` 238 | /// # use meilisearch_sdk::{client::*, indexes::*, documents::*}; 239 | /// # 240 | /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); 241 | /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); 242 | /// # 243 | /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); 244 | /// let index = client.index("my_index"); 245 | /// 246 | /// let mut documents_query = DocumentsQuery::new(&index); 247 | /// 248 | /// documents_query.with_limit(1); 249 | /// ``` 250 | pub fn with_limit(&mut self, limit: usize) -> &mut DocumentsQuery<'a, Http> { 251 | self.limit = Some(limit); 252 | self 253 | } 254 | 255 | /// Specify the fields to return in the documents. 256 | /// 257 | /// # Example 258 | /// 259 | /// ``` 260 | /// # use meilisearch_sdk::{client::*, indexes::*, documents::*}; 261 | /// # 262 | /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); 263 | /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); 264 | /// # 265 | /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); 266 | /// let index = client.index("my_index"); 267 | /// 268 | /// let mut documents_query = DocumentsQuery::new(&index); 269 | /// 270 | /// documents_query.with_fields(["title"]); 271 | /// ``` 272 | pub fn with_fields( 273 | &mut self, 274 | fields: impl IntoIterator, 275 | ) -> &mut DocumentsQuery<'a, Http> { 276 | self.fields = Some(fields.into_iter().collect()); 277 | self 278 | } 279 | 280 | pub fn with_filter<'b>(&'b mut self, filter: &'a str) -> &'b mut DocumentsQuery<'a, Http> { 281 | self.filter = Some(filter); 282 | self 283 | } 284 | 285 | /// Execute the get documents query. 286 | /// 287 | /// # Example 288 | /// 289 | /// ``` 290 | /// # use meilisearch_sdk::{client::*, indexes::*, documents::*}; 291 | /// # use serde::{Deserialize, Serialize}; 292 | /// # 293 | /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); 294 | /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); 295 | /// # 296 | /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); 297 | /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { 298 | /// # let index = client.create_index("documents_query_execute", None).await.unwrap().wait_for_completion(&client, None, None).await.unwrap().try_make_index(&client).unwrap(); 299 | /// #[derive(Debug, Serialize, Deserialize, PartialEq)] 300 | /// struct MyObject { 301 | /// id: Option, 302 | /// kind: String, 303 | /// } 304 | /// let index = client.index("documents_query_execute"); 305 | /// 306 | /// let document = DocumentsQuery::new(&index) 307 | /// .with_offset(1) 308 | /// .execute::() 309 | /// .await 310 | /// .unwrap(); 311 | /// 312 | /// # index.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); 313 | /// # }); 314 | /// ``` 315 | pub async fn execute( 316 | &self, 317 | ) -> Result, Error> { 318 | self.index.get_documents_with::(self).await 319 | } 320 | } 321 | 322 | #[derive(Debug, Clone, Serialize)] 323 | pub struct DocumentDeletionQuery<'a, Http: HttpClient> { 324 | #[serde(skip_serializing)] 325 | pub index: &'a Index, 326 | 327 | /// Filters to apply. 328 | /// 329 | /// Read the [dedicated guide](https://www.meilisearch.com/docs/learn/fine_tuning_results/filtering#filter-basics) to learn the syntax. 330 | pub filter: Option<&'a str>, 331 | } 332 | 333 | impl<'a, Http: HttpClient> DocumentDeletionQuery<'a, Http> { 334 | #[must_use] 335 | pub fn new(index: &Index) -> DocumentDeletionQuery { 336 | DocumentDeletionQuery { 337 | index, 338 | filter: None, 339 | } 340 | } 341 | 342 | pub fn with_filter<'b>( 343 | &'b mut self, 344 | filter: &'a str, 345 | ) -> &'b mut DocumentDeletionQuery<'a, Http> { 346 | self.filter = Some(filter); 347 | self 348 | } 349 | 350 | pub async fn execute(&self) -> Result { 351 | self.index.delete_documents_with(self).await 352 | } 353 | } 354 | 355 | #[cfg(test)] 356 | mod tests { 357 | use super::*; 358 | use crate::{client::Client, errors::*, indexes::*}; 359 | use meilisearch_test_macro::meilisearch_test; 360 | use serde::{Deserialize, Serialize}; 361 | 362 | #[derive(Debug, Serialize, Deserialize, PartialEq)] 363 | struct MyObject { 364 | id: Option, 365 | kind: String, 366 | } 367 | 368 | #[allow(unused)] 369 | #[derive(IndexConfig)] 370 | struct MovieClips { 371 | #[index_config(primary_key)] 372 | movie_id: u64, 373 | #[index_config(distinct)] 374 | owner: String, 375 | #[index_config(displayed, searchable)] 376 | title: String, 377 | #[index_config(displayed)] 378 | description: String, 379 | #[index_config(filterable, sortable, displayed)] 380 | release_date: String, 381 | #[index_config(filterable, displayed)] 382 | genres: Vec, 383 | } 384 | 385 | #[allow(unused)] 386 | #[derive(IndexConfig)] 387 | struct VideoClips { 388 | video_id: u64, 389 | } 390 | 391 | async fn setup_test_index(client: &Client, index: &Index) -> Result<(), Error> { 392 | let t0 = index 393 | .add_documents( 394 | &[ 395 | MyObject { 396 | id: Some(0), 397 | kind: "text".into(), 398 | }, 399 | MyObject { 400 | id: Some(1), 401 | kind: "text".into(), 402 | }, 403 | MyObject { 404 | id: Some(2), 405 | kind: "title".into(), 406 | }, 407 | MyObject { 408 | id: Some(3), 409 | kind: "title".into(), 410 | }, 411 | ], 412 | None, 413 | ) 414 | .await?; 415 | 416 | t0.wait_for_completion(client, None, None).await?; 417 | 418 | Ok(()) 419 | } 420 | 421 | #[meilisearch_test] 422 | async fn test_get_documents_with_execute(client: Client, index: Index) -> Result<(), Error> { 423 | setup_test_index(&client, &index).await?; 424 | let documents = DocumentsQuery::new(&index) 425 | .with_limit(1) 426 | .with_offset(1) 427 | .with_fields(["kind"]) 428 | .execute::() 429 | .await 430 | .unwrap(); 431 | 432 | assert_eq!(documents.limit, 1); 433 | assert_eq!(documents.offset, 1); 434 | assert_eq!(documents.results.len(), 1); 435 | 436 | Ok(()) 437 | } 438 | 439 | #[meilisearch_test] 440 | async fn test_delete_documents_with(client: Client, index: Index) -> Result<(), Error> { 441 | setup_test_index(&client, &index).await?; 442 | index 443 | .set_filterable_attributes(["id"]) 444 | .await? 445 | .wait_for_completion(&client, None, None) 446 | .await?; 447 | 448 | let mut query = DocumentDeletionQuery::new(&index); 449 | query.with_filter("id = 1"); 450 | index 451 | .delete_documents_with(&query) 452 | .await? 453 | .wait_for_completion(&client, None, None) 454 | .await?; 455 | let document_result = index.get_document::("1").await; 456 | 457 | match document_result { 458 | Ok(_) => panic!("The test was expecting no documents to be returned but got one."), 459 | Err(e) => match e { 460 | Error::Meilisearch(err) => { 461 | assert_eq!(err.error_code, ErrorCode::DocumentNotFound); 462 | } 463 | _ => panic!("The error was expected to be a Meilisearch error, but it was not."), 464 | }, 465 | } 466 | 467 | Ok(()) 468 | } 469 | 470 | #[meilisearch_test] 471 | async fn test_delete_documents_with_filter_not_filterable( 472 | client: Client, 473 | index: Index, 474 | ) -> Result<(), Error> { 475 | setup_test_index(&client, &index).await?; 476 | 477 | let mut query = DocumentDeletionQuery::new(&index); 478 | query.with_filter("id = 1"); 479 | let error = index 480 | .delete_documents_with(&query) 481 | .await? 482 | .wait_for_completion(&client, None, None) 483 | .await?; 484 | 485 | let error = error.unwrap_failure(); 486 | 487 | assert!(matches!( 488 | error, 489 | MeilisearchError { 490 | error_code: ErrorCode::InvalidDocumentFilter, 491 | error_type: ErrorType::InvalidRequest, 492 | .. 493 | } 494 | )); 495 | 496 | Ok(()) 497 | } 498 | 499 | #[meilisearch_test] 500 | async fn test_get_documents_with_only_one_param( 501 | client: Client, 502 | index: Index, 503 | ) -> Result<(), Error> { 504 | setup_test_index(&client, &index).await?; 505 | // let documents = index.get_documents(None, None, None).await.unwrap(); 506 | let documents = DocumentsQuery::new(&index) 507 | .with_limit(1) 508 | .execute::() 509 | .await 510 | .unwrap(); 511 | 512 | assert_eq!(documents.limit, 1); 513 | assert_eq!(documents.offset, 0); 514 | assert_eq!(documents.results.len(), 1); 515 | 516 | Ok(()) 517 | } 518 | 519 | #[meilisearch_test] 520 | async fn test_get_documents_with_filter(client: Client, index: Index) -> Result<(), Error> { 521 | setup_test_index(&client, &index).await?; 522 | 523 | index 524 | .set_filterable_attributes(["id"]) 525 | .await 526 | .unwrap() 527 | .wait_for_completion(&client, None, None) 528 | .await 529 | .unwrap(); 530 | 531 | let documents = DocumentsQuery::new(&index) 532 | .with_filter("id = 1") 533 | .execute::() 534 | .await?; 535 | 536 | assert_eq!(documents.results.len(), 1); 537 | 538 | Ok(()) 539 | } 540 | 541 | #[meilisearch_test] 542 | async fn test_get_documents_with_error_hint() -> Result<(), Error> { 543 | let meilisearch_url = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); 544 | let client = Client::new(format!("{meilisearch_url}/hello"), Some("masterKey")).unwrap(); 545 | let index = client.index("test_get_documents_with_filter_wrong_ms_version"); 546 | 547 | let documents = DocumentsQuery::new(&index) 548 | .with_filter("id = 1") 549 | .execute::() 550 | .await; 551 | 552 | let error = documents.unwrap_err(); 553 | 554 | let message = Some("Hint: It might not be working because you're not up to date with the Meilisearch version that updated the get_documents_with method.".to_string()); 555 | let url = format!( 556 | "{meilisearch_url}/hello/indexes/test_get_documents_with_filter_wrong_ms_version/documents/fetch" 557 | ); 558 | let status_code = 404; 559 | let displayed_error = format!("MeilisearchCommunicationError: The server responded with a 404. Hint: It might not be working because you're not up to date with the Meilisearch version that updated the get_documents_with method.\nurl: {meilisearch_url}/hello/indexes/test_get_documents_with_filter_wrong_ms_version/documents/fetch"); 560 | 561 | match &error { 562 | Error::MeilisearchCommunication(error) => { 563 | assert_eq!(error.status_code, status_code); 564 | assert_eq!(error.message, message); 565 | assert_eq!(error.url, url); 566 | } 567 | _ => panic!("The error was expected to be a MeilisearchCommunicationError error, but it was not."), 568 | }; 569 | assert_eq!(format!("{error}"), displayed_error); 570 | 571 | Ok(()) 572 | } 573 | 574 | #[meilisearch_test] 575 | async fn test_get_documents_with_error_hint_meilisearch_api_error( 576 | index: Index, 577 | client: Client, 578 | ) -> Result<(), Error> { 579 | setup_test_index(&client, &index).await?; 580 | 581 | let error = DocumentsQuery::new(&index) 582 | .with_filter("id = 1") 583 | .execute::() 584 | .await 585 | .unwrap_err(); 586 | 587 | let message = "Attribute `id` is not filterable. This index does not have configured filterable attributes. 588 | 1:3 id = 1 589 | Hint: It might not be working because you're not up to date with the Meilisearch version that updated the get_documents_with method.".to_string(); 590 | let displayed_error = "Meilisearch invalid_request: invalid_document_filter: Attribute `id` is not filterable. This index does not have configured filterable attributes. 591 | 1:3 id = 1 592 | Hint: It might not be working because you're not up to date with the Meilisearch version that updated the get_documents_with method.. https://docs.meilisearch.com/errors#invalid_document_filter"; 593 | 594 | match &error { 595 | Error::Meilisearch(error) => { 596 | assert_eq!(error.error_message, message); 597 | } 598 | _ => panic!("The error was expected to be a MeilisearchCommunicationError error, but it was not."), 599 | }; 600 | assert_eq!(format!("{error}"), displayed_error); 601 | 602 | Ok(()) 603 | } 604 | 605 | #[meilisearch_test] 606 | async fn test_get_documents_with_invalid_filter( 607 | client: Client, 608 | index: Index, 609 | ) -> Result<(), Error> { 610 | setup_test_index(&client, &index).await?; 611 | 612 | // Does not work because `id` is not filterable 613 | let error = DocumentsQuery::new(&index) 614 | .with_filter("id = 1") 615 | .execute::() 616 | .await 617 | .unwrap_err(); 618 | 619 | assert!(matches!( 620 | error, 621 | Error::Meilisearch(MeilisearchError { 622 | error_code: ErrorCode::InvalidDocumentFilter, 623 | error_type: ErrorType::InvalidRequest, 624 | .. 625 | }) 626 | )); 627 | 628 | Ok(()) 629 | } 630 | 631 | #[meilisearch_test] 632 | async fn test_settings_generated_by_macro(client: Client, index: Index) -> Result<(), Error> { 633 | setup_test_index(&client, &index).await?; 634 | 635 | let movie_settings: Settings = MovieClips::generate_settings(); 636 | let video_settings: Settings = VideoClips::generate_settings(); 637 | 638 | assert_eq!(movie_settings.searchable_attributes.unwrap(), ["title"]); 639 | assert!(video_settings.searchable_attributes.unwrap().is_empty()); 640 | 641 | assert_eq!( 642 | movie_settings.displayed_attributes.unwrap(), 643 | ["title", "description", "release_date", "genres"] 644 | ); 645 | assert!(video_settings.displayed_attributes.unwrap().is_empty()); 646 | 647 | assert_eq!( 648 | movie_settings.filterable_attributes.unwrap(), 649 | ["release_date", "genres"] 650 | ); 651 | assert!(video_settings.filterable_attributes.unwrap().is_empty()); 652 | 653 | assert_eq!( 654 | movie_settings.sortable_attributes.unwrap(), 655 | ["release_date"] 656 | ); 657 | assert!(video_settings.sortable_attributes.unwrap().is_empty()); 658 | 659 | Ok(()) 660 | } 661 | 662 | #[meilisearch_test] 663 | async fn test_generate_index(client: Client) -> Result<(), Error> { 664 | let index: Index = MovieClips::generate_index(&client).await.unwrap(); 665 | 666 | assert_eq!(index.uid, "movie_clips"); 667 | 668 | index 669 | .delete() 670 | .await? 671 | .wait_for_completion(&client, None, None) 672 | .await?; 673 | 674 | Ok(()) 675 | } 676 | #[derive(Serialize, Deserialize, IndexConfig)] 677 | struct Movie { 678 | #[index_config(primary_key)] 679 | movie_id: u64, 680 | #[index_config(displayed, searchable)] 681 | title: String, 682 | #[index_config(displayed)] 683 | description: String, 684 | #[index_config(filterable, sortable, displayed)] 685 | release_date: String, 686 | #[index_config(filterable, displayed)] 687 | genres: Vec, 688 | } 689 | } 690 | -------------------------------------------------------------------------------- /src/dumps.rs: -------------------------------------------------------------------------------- 1 | //! The `dumps` module allows the creation of database dumps. 2 | //! 3 | //! - Dumps are `.dump` files that can be used to launch Meilisearch. 4 | //! 5 | //! - Dumps are compatible between Meilisearch versions. 6 | //! 7 | //! - Creating a dump is also referred to as exporting it, whereas launching Meilisearch with a dump is referred to as importing it. 8 | //! 9 | //! - During a [dump export](crate::client::Client::create_dump), all [indexes](crate::indexes::Index) of the current instance are exported—together with their documents and settings—and saved as a single `.dump` file. 10 | //! 11 | //! - During a dump import, all indexes contained in the indicated `.dump` file are imported along with their associated documents and [settings](crate::settings::Settings). 12 | //! Any existing [index](crate::indexes::Index) with the same uid as an index in the dump file will be overwritten. 13 | //! 14 | //! - Dump imports are [performed at launch](https://www.meilisearch.com/docs/learn/configuration/instance_options#import-dump) using an option. 15 | //! 16 | //! # Example 17 | //! 18 | //! ``` 19 | //! # use meilisearch_sdk::{client::*, errors::*, dumps::*, dumps::*, task_info::*, tasks::*}; 20 | //! # use futures_await_test::async_test; 21 | //! # use std::{thread::sleep, time::Duration}; 22 | //! # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { 23 | //! # 24 | //! # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); 25 | //! # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); 26 | //! # 27 | //! # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); 28 | //! 29 | //! // Create a dump 30 | //! let task_info = client.create_dump().await.unwrap(); 31 | //! assert!(matches!( 32 | //! task_info, 33 | //! TaskInfo { 34 | //! update_type: TaskType::DumpCreation { .. }, 35 | //! .. 36 | //! } 37 | //! )); 38 | //! # }); 39 | //! ``` 40 | 41 | use crate::{client::Client, errors::Error, request::*, task_info::TaskInfo}; 42 | 43 | /// Dump related methods. 44 | /// See the [dumps](crate::dumps) module. 45 | impl Client { 46 | /// Triggers a dump creation process. 47 | /// 48 | /// Once the process is complete, a dump is created in the [dumps directory](https://www.meilisearch.com/docs/learn/configuration/instance_options#dump-directory). 49 | /// If the dumps directory does not exist yet, it will be created. 50 | /// 51 | /// # Example 52 | /// 53 | /// ``` 54 | /// # use meilisearch_sdk::{client::*, errors::*, dumps::*, dumps::*, task_info::*, tasks::*}; 55 | /// # use futures_await_test::async_test; 56 | /// # use std::{thread::sleep, time::Duration}; 57 | /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { 58 | /// # 59 | /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); 60 | /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); 61 | /// # 62 | /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); 63 | /// # 64 | /// let task_info = client.create_dump().await.unwrap(); 65 | /// 66 | /// assert!(matches!( 67 | /// task_info, 68 | /// TaskInfo { 69 | /// update_type: TaskType::DumpCreation { .. }, 70 | /// .. 71 | /// } 72 | /// )); 73 | /// # }); 74 | /// ``` 75 | pub async fn create_dump(&self) -> Result { 76 | self.http_client 77 | .request::<(), (), TaskInfo>( 78 | &format!("{}/dumps", self.host), 79 | Method::Post { 80 | query: (), 81 | body: (), 82 | }, 83 | 202, 84 | ) 85 | .await 86 | } 87 | } 88 | 89 | /// Alias for [`create_dump`](Client::create_dump). 90 | pub async fn create_dump(client: &Client) -> Result { 91 | client.create_dump().await 92 | } 93 | 94 | #[cfg(test)] 95 | mod tests { 96 | use super::*; 97 | use crate::{client::*, tasks::*}; 98 | use meilisearch_test_macro::meilisearch_test; 99 | use std::time::Duration; 100 | 101 | #[meilisearch_test] 102 | async fn test_dumps_success_creation(client: Client) -> Result<(), Error> { 103 | let task = client 104 | .create_dump() 105 | .await? 106 | .wait_for_completion( 107 | &client, 108 | Some(Duration::from_millis(1)), 109 | Some(Duration::from_millis(6000)), 110 | ) 111 | .await?; 112 | 113 | assert!(matches!(task, Task::Succeeded { .. })); 114 | Ok(()) 115 | } 116 | 117 | #[meilisearch_test] 118 | async fn test_dumps_correct_update_type(client: Client) -> Result<(), Error> { 119 | let task_info = client.create_dump().await.unwrap(); 120 | 121 | assert!(matches!( 122 | task_info, 123 | TaskInfo { 124 | update_type: TaskType::DumpCreation { .. }, 125 | .. 126 | } 127 | )); 128 | Ok(()) 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/errors.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use thiserror::Error; 3 | 4 | /// An enum representing the errors that can occur. 5 | 6 | #[derive(Debug, Error)] 7 | #[non_exhaustive] 8 | pub enum Error { 9 | /// The exhaustive list of Meilisearch errors: 10 | /// 11 | /// Also check out: 12 | #[error(transparent)] 13 | Meilisearch(#[from] MeilisearchError), 14 | 15 | #[error(transparent)] 16 | MeilisearchCommunication(#[from] MeilisearchCommunicationError), 17 | /// The Meilisearch server returned an invalid JSON for a request. 18 | #[error("Error parsing response JSON: {}", .0)] 19 | ParseError(#[from] serde_json::Error), 20 | 21 | /// A timeout happened while waiting for an update to complete. 22 | #[error("A task did not succeed in time.")] 23 | Timeout, 24 | /// This Meilisearch SDK generated an invalid request (which was not sent). 25 | /// 26 | /// It probably comes from an invalid API key resulting in an invalid HTTP header. 27 | #[error("Unable to generate a valid HTTP request. It probably comes from an invalid API key.")] 28 | InvalidRequest, 29 | 30 | /// Can't call this method without setting an api key in the client. 31 | #[error("You need to provide an api key to use the `{0}` method.")] 32 | CantUseWithoutApiKey(String), 33 | /// It is not possible to generate a tenant token with an invalid api key. 34 | /// 35 | /// Empty strings or with less than 8 characters are considered invalid. 36 | #[error("The provided api_key is invalid.")] 37 | TenantTokensInvalidApiKey, 38 | /// It is not possible to generate an already expired tenant token. 39 | #[error("The provided expires_at is already expired.")] 40 | TenantTokensExpiredSignature, 41 | 42 | /// When jsonwebtoken cannot generate the token successfully. 43 | #[cfg(not(target_arch = "wasm32"))] 44 | #[error("Impossible to generate the token, jsonwebtoken encountered an error: {}", .0)] 45 | InvalidTenantToken(#[from] jsonwebtoken::errors::Error), 46 | 47 | /// The http client encountered an error. 48 | #[cfg(feature = "reqwest")] 49 | #[error("HTTP request failed: {}", .0)] 50 | HttpError(#[from] reqwest::Error), 51 | 52 | /// The library formatting the query parameters encountered an error. 53 | #[error("Internal Error: could not parse the query parameters: {}", .0)] 54 | Yaup(#[from] yaup::Error), 55 | 56 | /// The library validating the format of an uuid. 57 | #[cfg(not(target_arch = "wasm32"))] 58 | #[error("The uid of the token has bit an uuid4 format: {}", .0)] 59 | Uuid(#[from] uuid::Error), 60 | 61 | /// Error thrown in case the version of the Uuid is not v4. 62 | #[error("The uid provided to the token is not of version uuidv4")] 63 | InvalidUuid4Version, 64 | 65 | #[error(transparent)] 66 | Other(Box), 67 | } 68 | 69 | #[derive(Debug, Clone, Deserialize, Error)] 70 | #[serde(rename_all = "camelCase")] 71 | pub struct MeilisearchCommunicationError { 72 | pub status_code: u16, 73 | pub message: Option, 74 | pub url: String, 75 | } 76 | 77 | impl std::fmt::Display for MeilisearchCommunicationError { 78 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 79 | write!( 80 | f, 81 | "MeilisearchCommunicationError: The server responded with a {}.", 82 | self.status_code 83 | )?; 84 | if let Some(message) = &self.message { 85 | write!(f, " {message}")?; 86 | } 87 | write!(f, "\nurl: {}", self.url)?; 88 | Ok(()) 89 | } 90 | } 91 | 92 | #[derive(Debug, Clone, Deserialize, Error)] 93 | #[serde(rename_all = "camelCase")] 94 | #[error("Meilisearch {}: {}: {}. {}", .error_type, .error_code, .error_message, .error_link)] 95 | pub struct MeilisearchError { 96 | /// The human readable error message 97 | #[serde(rename = "message")] 98 | pub error_message: String, 99 | /// The error code of the error. Officially documented at 100 | /// . 101 | #[serde(rename = "code")] 102 | pub error_code: ErrorCode, 103 | /// The type of error (invalid request, internal error, or authentication error) 104 | #[serde(rename = "type")] 105 | pub error_type: ErrorType, 106 | /// A link to the Meilisearch documentation for an error. 107 | #[serde(rename = "link")] 108 | pub error_link: String, 109 | } 110 | 111 | /// The type of error that was encountered. 112 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] 113 | #[serde(rename_all = "snake_case")] 114 | #[non_exhaustive] 115 | pub enum ErrorType { 116 | /// The submitted request was invalid. 117 | InvalidRequest, 118 | /// The Meilisearch instance encountered an internal error. 119 | Internal, 120 | /// Authentication was either incorrect or missing. 121 | Auth, 122 | 123 | /// That's unexpected. Please open a GitHub issue after ensuring you are 124 | /// using the supported version of the Meilisearch server. 125 | #[serde(other)] 126 | Unknown, 127 | } 128 | 129 | impl std::fmt::Display for ErrorType { 130 | fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { 131 | write!( 132 | fmt, 133 | "{}", 134 | // this can't fail 135 | serde_json::to_value(self).unwrap().as_str().unwrap() 136 | ) 137 | } 138 | } 139 | 140 | /// The error code. 141 | /// 142 | /// Officially documented at . 143 | #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] 144 | #[serde(rename_all = "snake_case")] 145 | #[non_exhaustive] 146 | pub enum ErrorCode { 147 | IndexCreationFailed, 148 | IndexAlreadyExists, 149 | IndexNotFound, 150 | InvalidIndexUid, 151 | InvalidState, 152 | PrimaryKeyInferenceFailed, 153 | IndexPrimaryKeyAlreadyPresent, 154 | InvalidStoreFile, 155 | MaxFieldsLimitExceeded, 156 | MissingDocumentId, 157 | InvalidDocumentId, 158 | BadParameter, 159 | BadRequest, 160 | DatabaseSizeLimitReached, 161 | DocumentNotFound, 162 | InternalError, 163 | InvalidApiKey, 164 | MissingAuthorizationHeader, 165 | TaskNotFound, 166 | DumpNotFound, 167 | MissingMasterKey, 168 | NoSpaceLeftOnDevice, 169 | PayloadTooLarge, 170 | UnretrievableDocument, 171 | SearchError, 172 | UnsupportedMediaType, 173 | DumpAlreadyProcessing, 174 | DumpProcessFailed, 175 | MissingContentType, 176 | MalformedPayload, 177 | InvalidContentType, 178 | MissingPayload, 179 | InvalidApiKeyDescription, 180 | InvalidApiKeyActions, 181 | InvalidApiKeyIndexes, 182 | InvalidApiKeyExpiresAt, 183 | ApiKeyNotFound, 184 | MissingTaskFilters, 185 | MissingIndexUid, 186 | InvalidIndexOffset, 187 | InvalidIndexLimit, 188 | InvalidIndexPrimaryKey, 189 | InvalidDocumentFilter, 190 | MissingDocumentFilter, 191 | InvalidDocumentFields, 192 | InvalidDocumentLimit, 193 | InvalidDocumentOffset, 194 | InvalidDocumentGeoField, 195 | InvalidSearchQ, 196 | InvalidSearchOffset, 197 | InvalidSearchLimit, 198 | InvalidSearchPage, 199 | InvalidSearchHitsPerPage, 200 | InvalidSearchAttributesToRetrieve, 201 | InvalidSearchAttributesToCrop, 202 | InvalidSearchCropLength, 203 | InvalidSearchAttributesToHighlight, 204 | InvalidSearchShowMatchesPosition, 205 | InvalidSearchFilter, 206 | InvalidSearchSort, 207 | InvalidSearchFacets, 208 | InvalidSearchHighlightPreTag, 209 | InvalidSearchHighlightPostTag, 210 | InvalidSearchCropMarker, 211 | InvalidSearchMatchingStrategy, 212 | ImmutableApiKeyUid, 213 | ImmutableApiKeyActions, 214 | ImmutableApiKeyIndexes, 215 | ImmutableExpiresAt, 216 | ImmutableCreatedAt, 217 | ImmutableUpdatedAt, 218 | InvalidSwapDuplicateIndexFound, 219 | InvalidSwapIndexes, 220 | MissingSwapIndexes, 221 | InvalidTaskTypes, 222 | InvalidTaskUids, 223 | InvalidTaskStatuses, 224 | InvalidTaskLimit, 225 | InvalidTaskFrom, 226 | InvalidTaskCanceledBy, 227 | InvalidTaskFilters, 228 | TooManyOpenFiles, 229 | IoError, 230 | InvalidTaskIndexUids, 231 | ImmutableIndexUid, 232 | ImmutableIndexCreatedAt, 233 | ImmutableIndexUpdatedAt, 234 | InvalidSettingsDisplayedAttributes, 235 | InvalidSettingsSearchableAttributes, 236 | InvalidSettingsFilterableAttributes, 237 | InvalidSettingsSortableAttributes, 238 | InvalidSettingsRankingRules, 239 | InvalidSettingsStopWords, 240 | InvalidSettingsSynonyms, 241 | InvalidSettingsDistinctAttributes, 242 | InvalidSettingsTypoTolerance, 243 | InvalidSettingsFaceting, 244 | InvalidSettingsDictionary, 245 | InvalidSettingsPagination, 246 | InvalidTaskBeforeEnqueuedAt, 247 | InvalidTaskAfterEnqueuedAt, 248 | InvalidTaskBeforeStartedAt, 249 | InvalidTaskAfterStartedAt, 250 | InvalidTaskBeforeFinishedAt, 251 | InvalidTaskAfterFinishedAt, 252 | MissingApiKeyActions, 253 | MissingApiKeyIndexes, 254 | MissingApiKeyExpiresAt, 255 | InvalidApiKeyLimit, 256 | InvalidApiKeyOffset, 257 | 258 | /// That's unexpected. Please open a GitHub issue after ensuring you are 259 | /// using the supported version of the Meilisearch server. 260 | #[serde(other)] 261 | Unknown, 262 | } 263 | 264 | pub const MEILISEARCH_VERSION_HINT: &str = "Hint: It might not be working because you're not up to date with the Meilisearch version that updated the get_documents_with method"; 265 | 266 | impl std::fmt::Display for ErrorCode { 267 | fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { 268 | write!( 269 | fmt, 270 | "{}", 271 | // this can't fail 272 | serde_json::to_value(self).unwrap().as_str().unwrap() 273 | ) 274 | } 275 | } 276 | 277 | #[cfg(test)] 278 | mod test { 279 | use super::*; 280 | 281 | use jsonwebtoken::errors::ErrorKind::InvalidToken; 282 | use meilisearch_test_macro::meilisearch_test; 283 | use uuid::Uuid; 284 | 285 | #[meilisearch_test] 286 | async fn test_meilisearch_error() { 287 | let error: MeilisearchError = serde_json::from_str( 288 | r#" 289 | { 290 | "message": "The cool error message.", 291 | "code": "index_creation_failed", 292 | "type": "internal", 293 | "link": "https://the best link ever" 294 | }"#, 295 | ) 296 | .unwrap(); 297 | 298 | assert_eq!(error.error_message, "The cool error message."); 299 | assert_eq!(error.error_code, ErrorCode::IndexCreationFailed); 300 | assert_eq!(error.error_type, ErrorType::Internal); 301 | assert_eq!(error.error_link, "https://the best link ever"); 302 | 303 | let error: MeilisearchError = serde_json::from_str( 304 | r#" 305 | { 306 | "message": "", 307 | "code": "An unknown error", 308 | "type": "An unknown type", 309 | "link": "" 310 | }"#, 311 | ) 312 | .unwrap(); 313 | 314 | assert_eq!(error.error_code, ErrorCode::Unknown); 315 | assert_eq!(error.error_type, ErrorType::Unknown); 316 | } 317 | 318 | #[meilisearch_test] 319 | async fn test_error_message_parsing() { 320 | let error: MeilisearchError = serde_json::from_str( 321 | r#" 322 | { 323 | "message": "The cool error message.", 324 | "code": "index_creation_failed", 325 | "type": "internal", 326 | "link": "https://the best link ever" 327 | }"#, 328 | ) 329 | .unwrap(); 330 | 331 | assert_eq!(error.to_string(), "Meilisearch internal: index_creation_failed: The cool error message.. https://the best link ever"); 332 | 333 | let error: MeilisearchCommunicationError = MeilisearchCommunicationError { 334 | status_code: 404, 335 | message: Some("Hint: something.".to_string()), 336 | url: "http://localhost:7700/something".to_string(), 337 | }; 338 | 339 | assert_eq!( 340 | error.to_string(), 341 | "MeilisearchCommunicationError: The server responded with a 404. Hint: something.\nurl: http://localhost:7700/something" 342 | ); 343 | 344 | let error: MeilisearchCommunicationError = MeilisearchCommunicationError { 345 | status_code: 404, 346 | message: None, 347 | url: "http://localhost:7700/something".to_string(), 348 | }; 349 | 350 | assert_eq!( 351 | error.to_string(), 352 | "MeilisearchCommunicationError: The server responded with a 404.\nurl: http://localhost:7700/something" 353 | ); 354 | 355 | let error = Error::Timeout; 356 | assert_eq!(error.to_string(), "A task did not succeed in time."); 357 | 358 | let error = Error::InvalidRequest; 359 | assert_eq!( 360 | error.to_string(), 361 | "Unable to generate a valid HTTP request. It probably comes from an invalid API key." 362 | ); 363 | 364 | let error = Error::TenantTokensInvalidApiKey; 365 | assert_eq!(error.to_string(), "The provided api_key is invalid."); 366 | 367 | let error = Error::TenantTokensExpiredSignature; 368 | assert_eq!( 369 | error.to_string(), 370 | "The provided expires_at is already expired." 371 | ); 372 | 373 | let error = Error::InvalidUuid4Version; 374 | assert_eq!( 375 | error.to_string(), 376 | "The uid provided to the token is not of version uuidv4" 377 | ); 378 | 379 | let error = Error::Uuid(Uuid::parse_str("67e55044").unwrap_err()); 380 | assert_eq!(error.to_string(), "The uid of the token has bit an uuid4 format: invalid length: expected length 32 for simple format, found 8"); 381 | 382 | let data = r#" 383 | { 384 | "name": "John Doe" 385 | "age": 43, 386 | }"#; 387 | 388 | let error = Error::ParseError(serde_json::from_str::(data).unwrap_err()); 389 | assert_eq!( 390 | error.to_string(), 391 | "Error parsing response JSON: invalid type: map, expected a string at line 2 column 8" 392 | ); 393 | 394 | let error = Error::HttpError( 395 | reqwest::Client::new() 396 | .execute(reqwest::Request::new( 397 | reqwest::Method::POST, 398 | // there will never be a `meilisearch.gouv.fr` addr since these domain name are controlled by the state of france 399 | reqwest::Url::parse("https://meilisearch.gouv.fr").unwrap(), 400 | )) 401 | .await 402 | .unwrap_err(), 403 | ); 404 | assert_eq!( 405 | error.to_string(), 406 | "HTTP request failed: error sending request for url (https://meilisearch.gouv.fr/)" 407 | ); 408 | 409 | let error = Error::InvalidTenantToken(jsonwebtoken::errors::Error::from(InvalidToken)); 410 | assert_eq!( 411 | error.to_string(), 412 | "Impossible to generate the token, jsonwebtoken encountered an error: InvalidToken" 413 | ); 414 | 415 | let error = Error::Yaup(yaup::Error::Custom("Test yaup error".to_string())); 416 | assert_eq!( 417 | error.to_string(), 418 | "Internal Error: could not parse the query parameters: Test yaup error" 419 | ); 420 | } 421 | } 422 | -------------------------------------------------------------------------------- /src/features.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | client::Client, 3 | errors::Error, 4 | request::{HttpClient, Method}, 5 | }; 6 | use serde::{Deserialize, Serialize}; 7 | 8 | /// Struct representing the experimental features result from the API. 9 | #[derive(Clone, Debug, Deserialize)] 10 | #[serde(rename_all = "camelCase")] 11 | pub struct ExperimentalFeaturesResult { 12 | pub metrics: bool, 13 | pub logs_route: bool, 14 | pub contains_filter: bool, 15 | pub network: bool, 16 | pub edit_documents_by_function: bool, 17 | } 18 | 19 | /// Struct representing the experimental features request. 20 | /// 21 | /// You can build this struct using the builder pattern. 22 | /// 23 | /// # Example 24 | /// 25 | /// ``` 26 | /// # use meilisearch_sdk::{client::Client, features::ExperimentalFeatures}; 27 | /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); 28 | /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); 29 | /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); 30 | /// let mut features = ExperimentalFeatures::new(&client); 31 | /// ``` 32 | #[derive(Debug, Serialize)] 33 | #[serde(rename_all = "camelCase")] 34 | pub struct ExperimentalFeatures<'a, Http: HttpClient> { 35 | #[serde(skip_serializing)] 36 | client: &'a Client, 37 | 38 | #[serde(skip_serializing_if = "Option::is_none")] 39 | pub metrics: Option, 40 | #[serde(skip_serializing_if = "Option::is_none")] 41 | pub contains_filter: Option, 42 | #[serde(skip_serializing_if = "Option::is_none")] 43 | pub logs_route: Option, 44 | #[serde(skip_serializing_if = "Option::is_none")] 45 | pub network: Option, 46 | #[serde(skip_serializing_if = "Option::is_none")] 47 | pub edit_documents_by_function: Option, 48 | } 49 | 50 | impl<'a, Http: HttpClient> ExperimentalFeatures<'a, Http> { 51 | #[must_use] 52 | pub fn new(client: &'a Client) -> Self { 53 | ExperimentalFeatures { 54 | client, 55 | metrics: None, 56 | logs_route: None, 57 | network: None, 58 | contains_filter: None, 59 | edit_documents_by_function: None, 60 | } 61 | } 62 | 63 | /// Get all the experimental features 64 | /// 65 | /// # Example 66 | /// 67 | /// ``` 68 | /// # use meilisearch_sdk::{client::Client, features::ExperimentalFeatures}; 69 | /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); 70 | /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); 71 | /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); 72 | /// tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { 73 | /// let features = ExperimentalFeatures::new(&client); 74 | /// features.get().await.unwrap(); 75 | /// }); 76 | /// ``` 77 | pub async fn get(&self) -> Result { 78 | self.client 79 | .http_client 80 | .request::<(), (), ExperimentalFeaturesResult>( 81 | &format!("{}/experimental-features", self.client.host), 82 | Method::Get { query: () }, 83 | 200, 84 | ) 85 | .await 86 | } 87 | 88 | /// Update the experimental features 89 | /// 90 | /// # Example 91 | /// 92 | /// ``` 93 | /// # use meilisearch_sdk::{client::Client, features::ExperimentalFeatures}; 94 | /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); 95 | /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); 96 | /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); 97 | /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { 98 | /// let features = ExperimentalFeatures::new(&client); 99 | /// features.update().await.unwrap(); 100 | /// # }); 101 | /// ``` 102 | pub async fn update(&self) -> Result { 103 | self.client 104 | .http_client 105 | .request::<(), &Self, ExperimentalFeaturesResult>( 106 | &format!("{}/experimental-features", self.client.host), 107 | Method::Patch { 108 | query: (), 109 | body: self, 110 | }, 111 | 200, 112 | ) 113 | .await 114 | } 115 | 116 | pub fn set_metrics(&mut self, metrics: bool) -> &mut Self { 117 | self.metrics = Some(metrics); 118 | self 119 | } 120 | 121 | pub fn set_logs_route(&mut self, logs_route: bool) -> &mut Self { 122 | self.logs_route = Some(logs_route); 123 | self 124 | } 125 | 126 | pub fn set_contains_filter(&mut self, contains_filter: bool) -> &mut Self { 127 | self.contains_filter = Some(contains_filter); 128 | self 129 | } 130 | 131 | pub fn set_edit_documents_by_function( 132 | &mut self, 133 | edit_documents_by_function: bool, 134 | ) -> &mut Self { 135 | self.edit_documents_by_function = Some(edit_documents_by_function); 136 | self 137 | } 138 | 139 | pub fn set_network(&mut self, network: bool) -> &mut Self { 140 | self.network = Some(network); 141 | self 142 | } 143 | } 144 | 145 | #[cfg(test)] 146 | mod tests { 147 | use super::*; 148 | use meilisearch_test_macro::meilisearch_test; 149 | 150 | #[meilisearch_test] 151 | async fn test_experimental_features_set_metrics(client: Client) { 152 | let mut features = ExperimentalFeatures::new(&client); 153 | features.set_metrics(true); 154 | let _ = features.update().await.unwrap(); 155 | 156 | let res = features.get().await.unwrap(); 157 | assert!(res.metrics) 158 | } 159 | 160 | #[meilisearch_test] 161 | async fn test_experimental_features_set_logs_route(client: Client) { 162 | let mut features = ExperimentalFeatures::new(&client); 163 | features.set_logs_route(true); 164 | let _ = features.update().await.unwrap(); 165 | 166 | let res = features.get().await.unwrap(); 167 | assert!(res.logs_route) 168 | } 169 | 170 | #[meilisearch_test] 171 | async fn test_experimental_features_set_contains_filter(client: Client) { 172 | let mut features = ExperimentalFeatures::new(&client); 173 | features.set_contains_filter(true); 174 | let _ = features.update().await.unwrap(); 175 | 176 | let res = features.get().await.unwrap(); 177 | assert!(res.contains_filter) 178 | } 179 | 180 | #[meilisearch_test] 181 | async fn test_experimental_features_set_network(client: Client) { 182 | let mut features = ExperimentalFeatures::new(&client); 183 | features.set_network(true); 184 | let _ = features.update().await.unwrap(); 185 | 186 | let res = features.get().await.unwrap(); 187 | assert!(res.network) 188 | } 189 | 190 | #[meilisearch_test] 191 | async fn test_experimental_features_set_edit_documents_by_function(client: Client) { 192 | let mut features = ExperimentalFeatures::new(&client); 193 | features.set_edit_documents_by_function(true); 194 | let _ = features.update().await.unwrap(); 195 | 196 | let res = features.get().await.unwrap(); 197 | assert!(res.edit_documents_by_function) 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! # 🚀 Getting started 2 | //! 3 | //! ### Add Documents 4 | //! 5 | //! ``` 6 | //! use meilisearch_sdk::client::*; 7 | //! use serde::{Serialize, Deserialize}; 8 | //! use futures::executor::block_on; 9 | //! 10 | //! #[derive(Serialize, Deserialize, Debug)] 11 | //! struct Movie { 12 | //! id: usize, 13 | //! title: String, 14 | //! genres: Vec, 15 | //! } 16 | //! 17 | //! 18 | //! #[tokio::main(flavor = "current_thread")] 19 | //! async fn main() { 20 | //! # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); 21 | //! # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); 22 | //! // Create a client (without sending any request so that can't fail) 23 | //! let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); 24 | //! 25 | //! # let index = client.create_index("movies", None).await.unwrap().wait_for_completion(&client, None, None).await.unwrap().try_make_index(&client).unwrap(); 26 | //! // An index is where the documents are stored. 27 | //! let movies = client.index("movies"); 28 | //! 29 | //! // Add some movies in the index. If the index 'movies' does not exist, Meilisearch creates it when you first add the documents. 30 | //! movies.add_documents(&[ 31 | //! Movie { id: 1, title: String::from("Carol"), genres: vec!["Romance".to_string(), "Drama".to_string()] }, 32 | //! Movie { id: 2, title: String::from("Wonder Woman"), genres: vec!["Action".to_string(), "Adventure".to_string()] }, 33 | //! Movie { id: 3, title: String::from("Life of Pi"), genres: vec!["Adventure".to_string(), "Drama".to_string()] }, 34 | //! Movie { id: 4, title: String::from("Mad Max"), genres: vec!["Adventure".to_string(), "Science Fiction".to_string()] }, 35 | //! Movie { id: 5, title: String::from("Moana"), genres: vec!["Fantasy".to_string(), "Action".to_string()] }, 36 | //! Movie { id: 6, title: String::from("Philadelphia"), genres: vec!["Drama".to_string()] }, 37 | //! ], Some("id")).await.unwrap(); 38 | //! # index.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); 39 | //! } 40 | //! ``` 41 | //! 42 | //! With the `uid`, you can check the status (`enqueued`, `canceled`, `processing`, `succeeded` or `failed`) of your documents addition using the [task](https://www.meilisearch.com/docs/reference/api/tasks#get-task). 43 | //! 44 | //! ### Basic Search 45 | //! 46 | //! ``` 47 | //! # use meilisearch_sdk::client::*; 48 | //! # use serde::{Serialize, Deserialize}; 49 | //! # #[derive(Serialize, Deserialize, Debug)] 50 | //! # struct Movie { 51 | //! # id: usize, 52 | //! # title: String, 53 | //! # genres: Vec, 54 | //! # } 55 | //! # fn main() { tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { 56 | //! # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); 57 | //! # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); 58 | //! # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); 59 | //! # let movies = client.create_index("movies_2", None).await.unwrap().wait_for_completion(&client, None, None).await.unwrap().try_make_index(&client).unwrap(); 60 | //! // Meilisearch is typo-tolerant: 61 | //! println!("{:?}", client.index("movies_2").search().with_query("caorl").execute::().await.unwrap().hits); 62 | //! # movies.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); 63 | //! # })} 64 | //! ``` 65 | //! 66 | //! Output: 67 | //! ```text 68 | //! [Movie { id: 1, title: String::from("Carol"), genres: vec!["Romance", "Drama"] }] 69 | //! ``` 70 | //! 71 | //! Json output: 72 | //! ```json 73 | //! { 74 | //! "hits": [{ 75 | //! "id": 1, 76 | //! "title": "Carol", 77 | //! "genres": ["Romance", "Drama"] 78 | //! }], 79 | //! "offset": 0, 80 | //! "limit": 10, 81 | //! "processingTimeMs": 1, 82 | //! "query": "caorl" 83 | //! } 84 | //! ``` 85 | //! 86 | //! ### Custom Search 87 | //! 88 | //! ``` 89 | //! # use meilisearch_sdk::{client::*, search::*}; 90 | //! # use serde::{Serialize, Deserialize}; 91 | //! # #[derive(Serialize, Deserialize, Debug)] 92 | //! # struct Movie { 93 | //! # id: usize, 94 | //! # title: String, 95 | //! # genres: Vec, 96 | //! # } 97 | //! # fn main() { tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { 98 | //! # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); 99 | //! # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); 100 | //! # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); 101 | //! # let movies = client.create_index("movies_3", None).await.unwrap().wait_for_completion(&client, None, None).await.unwrap().try_make_index(&client).unwrap(); 102 | //! let search_result = client.index("movies_3") 103 | //! .search() 104 | //! .with_query("phil") 105 | //! .with_attributes_to_highlight(Selectors::Some(&["*"])) 106 | //! .execute::() 107 | //! .await 108 | //! .unwrap(); 109 | //! println!("{:?}", search_result.hits); 110 | //! # movies.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); 111 | //! # })} 112 | //! ``` 113 | //! 114 | //! Json output: 115 | //! ```json 116 | //! { 117 | //! "hits": [ 118 | //! { 119 | //! "id": 6, 120 | //! "title": "Philadelphia", 121 | //! "_formatted": { 122 | //! "id": 6, 123 | //! "title": "Philadelphia", 124 | //! "genre": ["Drama"] 125 | //! } 126 | //! } 127 | //! ], 128 | //! "offset": 0, 129 | //! "limit": 20, 130 | //! "processingTimeMs": 0, 131 | //! "query": "phil" 132 | //! } 133 | //! ``` 134 | //! 135 | //! ### Custom Search With Filters 136 | //! 137 | //! If you want to enable filtering, you must add your attributes to the `filterableAttributes` 138 | //! index setting. 139 | //! 140 | //! ``` 141 | //! # use meilisearch_sdk::{client::*}; 142 | //! # use serde::{Serialize, Deserialize}; 143 | //! # fn main() { tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { 144 | //! # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); 145 | //! # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); 146 | //! # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); 147 | //! # let movies = client.create_index("movies_4", None).await.unwrap().wait_for_completion(&client, None, None).await.unwrap().try_make_index(&client).unwrap(); 148 | //! let filterable_attributes = [ 149 | //! "id", 150 | //! "genres", 151 | //! ]; 152 | //! client.index("movies_4").set_filterable_attributes(&filterable_attributes).await.unwrap(); 153 | //! # movies.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); 154 | //! # })} 155 | //! ``` 156 | //! 157 | //! You only need to perform this operation once. 158 | //! 159 | //! Note that Meilisearch will rebuild your index whenever you update `filterableAttributes`. Depending on the size of your dataset, this might take time. You can track the process using the [tasks](https://www.meilisearch.com/docs/reference/api/tasks#get-task). 160 | //! 161 | //! Then, you can perform the search: 162 | //! 163 | //! ``` 164 | //! # use meilisearch_sdk::{client::*, search::*}; 165 | //! # use serde::{Serialize, Deserialize}; 166 | //! # #[derive(Serialize, Deserialize, Debug)] 167 | //! # struct Movie { 168 | //! # id: usize, 169 | //! # title: String, 170 | //! # genres: Vec, 171 | //! # } 172 | //! # fn main() { tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { 173 | //! # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); 174 | //! # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); 175 | //! # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); 176 | //! # let movies = client.create_index("movies_5", None).await.unwrap().wait_for_completion(&client, None, None).await.unwrap().try_make_index(&client).unwrap(); 177 | //! # let filterable_attributes = [ 178 | //! # "id", 179 | //! # "genres" 180 | //! # ]; 181 | //! # movies.set_filterable_attributes(&filterable_attributes).await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); 182 | //! # movies.add_documents(&[ 183 | //! # Movie { id: 1, title: String::from("Carol"), genres: vec!["Romance".to_string(), "Drama".to_string()] }, 184 | //! # Movie { id: 2, title: String::from("Wonder Woman"), genres: vec!["Action".to_string(), "Adventure".to_string()] }, 185 | //! # Movie { id: 3, title: String::from("Life of Pi"), genres: vec!["Adventure".to_string(), "Drama".to_string()] }, 186 | //! # Movie { id: 4, title: String::from("Mad Max"), genres: vec!["Adventure".to_string(), "Science Fiction".to_string()] }, 187 | //! # Movie { id: 5, title: String::from("Moana"), genres: vec!["Fantasy".to_string(), "Action".to_string()] }, 188 | //! # Movie { id: 6, title: String::from("Philadelphia"), genres: vec!["Drama".to_string()] }, 189 | //! # ], Some("id")).await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); 190 | //! let search_result = client.index("movies_5") 191 | //! .search() 192 | //! .with_query("wonder") 193 | //! .with_filter("id > 1 AND genres = Action") 194 | //! .execute::() 195 | //! .await 196 | //! .unwrap(); 197 | //! println!("{:?}", search_result.hits); 198 | //! # movies.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); 199 | //! # })} 200 | //! ``` 201 | //! 202 | //! Json output: 203 | //! ```json 204 | //! { 205 | //! "hits": [ 206 | //! { 207 | //! "id": 2, 208 | //! "title": "Wonder Woman", 209 | //! "genres": ["Action", "Adventure"] 210 | //! } 211 | //! ], 212 | //! "offset": 0, 213 | //! "limit": 20, 214 | //! "estimatedTotalHits": 1, 215 | //! "processingTimeMs": 0, 216 | //! "query": "wonder" 217 | //! } 218 | //! ``` 219 | //! 220 | //! ### Customize the `HttpClient` 221 | //! 222 | //! By default, the SDK uses [`reqwest`](https://docs.rs/reqwest/latest/reqwest/) to make http calls. 223 | //! The SDK lets you customize the http client by implementing the `HttpClient` trait yourself and 224 | //! initializing the `Client` with the `new_with_client` method. 225 | //! You may be interested by the `futures-unsend` feature which lets you specify a non-Send http client. 226 | //! 227 | //! ### Wasm support 228 | //! 229 | //! The SDK supports wasm through reqwest. You'll need to enable the `futures-unsend` feature while importing it, though. 230 | #![warn(clippy::all)] 231 | #![allow(clippy::needless_doctest_main)] 232 | 233 | /// Module containing the [`Client`](client::Client) struct. 234 | pub mod client; 235 | /// Module representing the [documents] structures. 236 | pub mod documents; 237 | /// Module containing the [dumps] trait. 238 | pub mod dumps; 239 | /// Module containing the [`errors::Error`] struct. 240 | pub mod errors; 241 | /// Module related to runtime and instance features. 242 | pub mod features; 243 | /// Module containing the Index struct. 244 | pub mod indexes; 245 | /// Module containing the [`Key`](key::Key) struct. 246 | pub mod key; 247 | pub mod request; 248 | /// Module related to search queries and results. 249 | pub mod search; 250 | /// Module containing [`Settings`](settings::Settings). 251 | pub mod settings; 252 | /// Module containing the [snapshots](snapshots::create_snapshot)-feature. 253 | pub mod snapshots; 254 | /// Module representing the [`TaskInfo`](task_info::TaskInfo)s. 255 | pub mod task_info; 256 | /// Module representing the [`Task`](tasks::Task)s. 257 | pub mod tasks; 258 | /// Module that generates tenant tokens. 259 | #[cfg(not(target_arch = "wasm32"))] 260 | mod tenant_tokens; 261 | /// Module containing utilizes functions. 262 | mod utils; 263 | 264 | #[cfg(feature = "reqwest")] 265 | pub mod reqwest; 266 | 267 | #[cfg(feature = "reqwest")] 268 | pub type DefaultHttpClient = reqwest::ReqwestClient; 269 | 270 | #[cfg(not(feature = "reqwest"))] 271 | pub type DefaultHttpClient = std::convert::Infallible; 272 | 273 | #[cfg(test)] 274 | /// Support for the `IndexConfig` derive proc macro in the crate's tests. 275 | extern crate self as meilisearch_sdk; 276 | /// Can't assume that the user of proc_macro will have access to `async_trait` crate. So exporting the `async-trait` crate from `meilisearch_sdk` in a hidden module. 277 | #[doc(hidden)] 278 | pub mod macro_helper { 279 | pub use async_trait::async_trait; 280 | } 281 | -------------------------------------------------------------------------------- /src/request.rs: -------------------------------------------------------------------------------- 1 | use std::convert::Infallible; 2 | 3 | use async_trait::async_trait; 4 | use log::{error, trace, warn}; 5 | use serde::{de::DeserializeOwned, Serialize}; 6 | use serde_json::{from_str, to_vec}; 7 | 8 | use crate::errors::{Error, MeilisearchCommunicationError, MeilisearchError}; 9 | 10 | #[derive(Debug)] 11 | pub enum Method { 12 | Get { query: Q }, 13 | Post { query: Q, body: B }, 14 | Patch { query: Q, body: B }, 15 | Put { query: Q, body: B }, 16 | Delete { query: Q }, 17 | } 18 | 19 | impl Method { 20 | pub fn map_body(self, f: impl Fn(B) -> B2) -> Method { 21 | match self { 22 | Method::Get { query } => Method::Get { query }, 23 | Method::Delete { query } => Method::Delete { query }, 24 | Method::Post { query, body } => Method::Post { 25 | query, 26 | body: f(body), 27 | }, 28 | Method::Patch { query, body } => Method::Patch { 29 | query, 30 | body: f(body), 31 | }, 32 | Method::Put { query, body } => Method::Put { 33 | query, 34 | body: f(body), 35 | }, 36 | } 37 | } 38 | 39 | pub fn query(&self) -> &Q { 40 | match self { 41 | Method::Get { query } => query, 42 | Method::Delete { query } => query, 43 | Method::Post { query, .. } => query, 44 | Method::Put { query, .. } => query, 45 | Method::Patch { query, .. } => query, 46 | } 47 | } 48 | 49 | pub fn body(&self) -> Option<&B> { 50 | match self { 51 | Method::Get { query: _ } | Method::Delete { query: _ } => None, 52 | Method::Post { body, query: _ } => Some(body), 53 | Method::Put { body, query: _ } => Some(body), 54 | Method::Patch { body, query: _ } => Some(body), 55 | } 56 | } 57 | 58 | pub fn into_body(self) -> Option { 59 | match self { 60 | Method::Get { query: _ } | Method::Delete { query: _ } => None, 61 | Method::Post { body, query: _ } => Some(body), 62 | Method::Put { body, query: _ } => Some(body), 63 | Method::Patch { body, query: _ } => Some(body), 64 | } 65 | } 66 | } 67 | 68 | #[cfg_attr(feature = "futures-unsend", async_trait(?Send))] 69 | #[cfg_attr(not(feature = "futures-unsend"), async_trait)] 70 | pub trait HttpClient: Clone + Send + Sync { 71 | async fn request( 72 | &self, 73 | url: &str, 74 | method: Method, 75 | expected_status_code: u16, 76 | ) -> Result 77 | where 78 | Query: Serialize + Send + Sync, 79 | Body: Serialize + Send + Sync, 80 | Output: DeserializeOwned + 'static + Send, 81 | { 82 | use futures::io::Cursor; 83 | 84 | self.stream_request( 85 | url, 86 | method.map_body(|body| Cursor::new(to_vec(&body).unwrap())), 87 | "application/json", 88 | expected_status_code, 89 | ) 90 | .await 91 | } 92 | 93 | async fn stream_request< 94 | Query: Serialize + Send + Sync, 95 | Body: futures_io::AsyncRead + Send + Sync + 'static, 96 | Output: DeserializeOwned + 'static, 97 | >( 98 | &self, 99 | url: &str, 100 | method: Method, 101 | content_type: &str, 102 | expected_status_code: u16, 103 | ) -> Result; 104 | } 105 | 106 | pub fn parse_response( 107 | status_code: u16, 108 | expected_status_code: u16, 109 | body: &str, 110 | url: String, 111 | ) -> Result { 112 | if status_code == expected_status_code { 113 | return match from_str::(body) { 114 | Ok(output) => { 115 | trace!("Request succeed"); 116 | Ok(output) 117 | } 118 | Err(e) => { 119 | error!("Request succeeded but failed to parse response"); 120 | Err(Error::ParseError(e)) 121 | } 122 | }; 123 | } 124 | 125 | warn!( 126 | "Expected response code {}, got {}", 127 | expected_status_code, status_code 128 | ); 129 | 130 | match from_str::(body) { 131 | Ok(e) => Err(Error::from(e)), 132 | Err(e) => { 133 | if status_code >= 400 { 134 | return Err(Error::MeilisearchCommunication( 135 | MeilisearchCommunicationError { 136 | status_code, 137 | message: None, 138 | url, 139 | }, 140 | )); 141 | } 142 | Err(Error::ParseError(e)) 143 | } 144 | } 145 | } 146 | 147 | #[cfg_attr(feature = "futures-unsend", async_trait(?Send))] 148 | #[cfg_attr(not(feature = "futures-unsend"), async_trait)] 149 | impl HttpClient for Infallible { 150 | async fn request( 151 | &self, 152 | _url: &str, 153 | _method: Method, 154 | _expected_status_code: u16, 155 | ) -> Result 156 | where 157 | Query: Serialize + Send + Sync, 158 | Body: Serialize + Send + Sync, 159 | Output: DeserializeOwned + 'static + Send, 160 | { 161 | unreachable!() 162 | } 163 | 164 | async fn stream_request< 165 | Query: Serialize + Send + Sync, 166 | Body: futures_io::AsyncRead + Send + Sync + 'static, 167 | Output: DeserializeOwned + 'static, 168 | >( 169 | &self, 170 | _url: &str, 171 | _method: Method, 172 | _content_type: &str, 173 | _expected_status_code: u16, 174 | ) -> Result { 175 | unreachable!() 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /src/reqwest.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | pin::Pin, 3 | task::{Context, Poll}, 4 | }; 5 | 6 | use async_trait::async_trait; 7 | use bytes::{Bytes, BytesMut}; 8 | use futures::{AsyncRead, Stream}; 9 | use pin_project_lite::pin_project; 10 | use serde::{de::DeserializeOwned, Serialize}; 11 | 12 | use crate::{ 13 | errors::Error, 14 | request::{parse_response, HttpClient, Method}, 15 | }; 16 | 17 | #[derive(Debug, Clone, Default)] 18 | pub struct ReqwestClient { 19 | client: reqwest::Client, 20 | } 21 | 22 | impl ReqwestClient { 23 | pub fn new(api_key: Option<&str>) -> Result { 24 | use reqwest::{header, ClientBuilder}; 25 | 26 | let builder = ClientBuilder::new(); 27 | let mut headers = header::HeaderMap::new(); 28 | #[cfg(not(target_arch = "wasm32"))] 29 | headers.insert( 30 | header::USER_AGENT, 31 | header::HeaderValue::from_str(&qualified_version()).unwrap(), 32 | ); 33 | #[cfg(target_arch = "wasm32")] 34 | headers.insert( 35 | header::HeaderName::from_static("x-meilisearch-client"), 36 | header::HeaderValue::from_str(&qualified_version()).unwrap(), 37 | ); 38 | 39 | if let Some(api_key) = api_key { 40 | headers.insert( 41 | header::AUTHORIZATION, 42 | header::HeaderValue::from_str(&format!("Bearer {api_key}")).unwrap(), 43 | ); 44 | } 45 | 46 | let builder = builder.default_headers(headers); 47 | let client = builder.build()?; 48 | 49 | Ok(ReqwestClient { client }) 50 | } 51 | } 52 | 53 | #[cfg_attr(feature = "futures-unsend", async_trait(?Send))] 54 | #[cfg_attr(not(feature = "futures-unsend"), async_trait)] 55 | impl HttpClient for ReqwestClient { 56 | async fn stream_request< 57 | Query: Serialize + Send + Sync, 58 | Body: futures_io::AsyncRead + Send + Sync + 'static, 59 | Output: DeserializeOwned + 'static, 60 | >( 61 | &self, 62 | url: &str, 63 | method: Method, 64 | content_type: &str, 65 | expected_status_code: u16, 66 | ) -> Result { 67 | use reqwest::header; 68 | 69 | let query = method.query(); 70 | let query = yaup::to_string(query)?; 71 | 72 | let url = if query.is_empty() { 73 | url.to_string() 74 | } else { 75 | format!("{url}{query}") 76 | }; 77 | 78 | let mut request = self.client.request(verb(&method), &url); 79 | 80 | if let Some(body) = method.into_body() { 81 | // TODO: Currently reqwest doesn't support streaming data in wasm so we need to collect everything in RAM 82 | #[cfg(not(target_arch = "wasm32"))] 83 | { 84 | let stream = ReaderStream::new(body); 85 | let body = reqwest::Body::wrap_stream(stream); 86 | 87 | request = request 88 | .header(header::CONTENT_TYPE, content_type) 89 | .body(body); 90 | } 91 | #[cfg(target_arch = "wasm32")] 92 | { 93 | use futures::{pin_mut, AsyncReadExt}; 94 | 95 | let mut buf = Vec::new(); 96 | pin_mut!(body); 97 | body.read_to_end(&mut buf) 98 | .await 99 | .map_err(|err| Error::Other(Box::new(err)))?; 100 | request = request.header(header::CONTENT_TYPE, content_type).body(buf); 101 | } 102 | } 103 | 104 | let response = self.client.execute(request.build()?).await?; 105 | let status = response.status().as_u16(); 106 | let mut body = response.text().await?; 107 | 108 | if body.is_empty() { 109 | body = "null".to_string(); 110 | } 111 | 112 | parse_response(status, expected_status_code, &body, url.to_string()) 113 | } 114 | } 115 | 116 | fn verb(method: &Method) -> reqwest::Method { 117 | match method { 118 | Method::Get { .. } => reqwest::Method::GET, 119 | Method::Delete { .. } => reqwest::Method::DELETE, 120 | Method::Post { .. } => reqwest::Method::POST, 121 | Method::Put { .. } => reqwest::Method::PUT, 122 | Method::Patch { .. } => reqwest::Method::PATCH, 123 | } 124 | } 125 | 126 | pub fn qualified_version() -> String { 127 | const VERSION: Option<&str> = option_env!("CARGO_PKG_VERSION"); 128 | 129 | format!("Meilisearch Rust (v{})", VERSION.unwrap_or("unknown")) 130 | } 131 | 132 | pin_project! { 133 | #[derive(Debug)] 134 | pub struct ReaderStream { 135 | #[pin] 136 | reader: R, 137 | buf: BytesMut, 138 | capacity: usize, 139 | } 140 | } 141 | 142 | impl ReaderStream { 143 | pub fn new(reader: R) -> Self { 144 | Self { 145 | reader, 146 | buf: BytesMut::new(), 147 | // 8KiB of capacity, the default capacity used by `BufReader` in the std 148 | capacity: 8 * 1024 * 1024, 149 | } 150 | } 151 | } 152 | 153 | impl Stream for ReaderStream { 154 | type Item = std::io::Result; 155 | 156 | fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 157 | let this = self.as_mut().project(); 158 | 159 | if this.buf.capacity() == 0 { 160 | this.buf.resize(*this.capacity, 0); 161 | } 162 | 163 | match AsyncRead::poll_read(this.reader, cx, this.buf) { 164 | Poll::Pending => Poll::Pending, 165 | Poll::Ready(Err(err)) => Poll::Ready(Some(Err(err))), 166 | Poll::Ready(Ok(0)) => Poll::Ready(None), 167 | Poll::Ready(Ok(i)) => { 168 | let chunk = this.buf.split_to(i); 169 | Poll::Ready(Some(Ok(chunk.freeze()))) 170 | } 171 | } 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /src/snapshots.rs: -------------------------------------------------------------------------------- 1 | //! The `snapshots` module allows the creation of database snapshots. 2 | //! 3 | //! - snapshots are `.snapshots` files that can be used to launch Meilisearch. 4 | //! 5 | //! - snapshots are not compatible between Meilisearch versions. 6 | //! 7 | //! # Example 8 | //! 9 | //! ``` 10 | //! # use meilisearch_sdk::{client::*, errors::*, snapshots::*, snapshots::*, task_info::*, tasks::*}; 11 | //! # use futures_await_test::async_test; 12 | //! # use std::{thread::sleep, time::Duration}; 13 | //! # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { 14 | //! # 15 | //! # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); 16 | //! # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); 17 | //! # 18 | //! # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); 19 | //! 20 | //! // Create a snapshot 21 | //! let task_info = client.create_snapshot().await.unwrap(); 22 | //! assert!(matches!( 23 | //! task_info, 24 | //! TaskInfo { 25 | //! update_type: TaskType::SnapshotCreation { .. }, 26 | //! .. 27 | //! } 28 | //! )); 29 | //! # }); 30 | //! ``` 31 | 32 | use crate::{client::Client, errors::Error, request::*, task_info::TaskInfo}; 33 | 34 | /// Snapshots related methods. 35 | /// See the [snapshots](crate::snapshots) module. 36 | impl Client { 37 | /// Triggers a snapshots creation process. 38 | /// 39 | /// Once the process is complete, a snapshots is created in the [snapshots directory]. 40 | /// If the snapshots directory does not exist yet, it will be created. 41 | /// 42 | /// # Example 43 | /// 44 | /// ``` 45 | /// # use meilisearch_sdk::{client::*, errors::*, snapshots::*, snapshots::*, task_info::*, tasks::*}; 46 | /// # use futures_await_test::async_test; 47 | /// # use std::{thread::sleep, time::Duration}; 48 | /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { 49 | /// # 50 | /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); 51 | /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); 52 | /// # 53 | /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); 54 | /// # 55 | /// let task_info = client.create_snapshot().await.unwrap(); 56 | /// 57 | /// assert!(matches!( 58 | /// task_info, 59 | /// TaskInfo { 60 | /// update_type: TaskType::SnapshotCreation { .. }, 61 | /// .. 62 | /// } 63 | /// )); 64 | /// # }); 65 | /// ``` 66 | pub async fn create_snapshot(&self) -> Result { 67 | self.http_client 68 | .request::<(), (), TaskInfo>( 69 | &format!("{}/snapshots", self.host), 70 | Method::Post { 71 | query: (), 72 | body: (), 73 | }, 74 | 202, 75 | ) 76 | .await 77 | } 78 | } 79 | 80 | /// Alias for [`create_snapshot`](Client::create_snapshot). 81 | pub async fn create_snapshot(client: &Client) -> Result { 82 | client.create_snapshot().await 83 | } 84 | 85 | #[cfg(test)] 86 | mod tests { 87 | use super::*; 88 | use crate::{client::*, tasks::*}; 89 | use meilisearch_test_macro::meilisearch_test; 90 | 91 | #[meilisearch_test] 92 | async fn test_snapshot_success_creation(client: Client) -> Result<(), Error> { 93 | let task = client 94 | .create_snapshot() 95 | .await? 96 | .wait_for_completion(&client, None, None) 97 | .await?; 98 | 99 | assert!(matches!(task, Task::Succeeded { .. })); 100 | Ok(()) 101 | } 102 | 103 | #[meilisearch_test] 104 | async fn test_snapshot_correct_update_type(client: Client) -> Result<(), Error> { 105 | let task_info = client.create_snapshot().await.unwrap(); 106 | 107 | assert!(matches!( 108 | task_info, 109 | TaskInfo { 110 | update_type: TaskType::SnapshotCreation { .. }, 111 | .. 112 | } 113 | )); 114 | Ok(()) 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/task_info.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | use std::time::Duration; 3 | use time::OffsetDateTime; 4 | 5 | use crate::{client::Client, errors::Error, request::HttpClient, tasks::*}; 6 | 7 | #[derive(Debug, Clone, Deserialize)] 8 | #[serde(rename_all = "camelCase")] 9 | pub struct TaskInfo { 10 | #[serde(with = "time::serde::rfc3339")] 11 | pub enqueued_at: OffsetDateTime, 12 | pub index_uid: Option, 13 | pub status: String, 14 | #[serde(flatten)] 15 | pub update_type: TaskType, 16 | pub task_uid: u32, 17 | } 18 | 19 | impl AsRef for TaskInfo { 20 | fn as_ref(&self) -> &u32 { 21 | &self.task_uid 22 | } 23 | } 24 | 25 | impl TaskInfo { 26 | #[must_use] 27 | pub fn get_task_uid(&self) -> u32 { 28 | self.task_uid 29 | } 30 | 31 | /// Wait until Meilisearch processes a task provided by [`TaskInfo`], and get its status. 32 | /// 33 | /// `interval` = The frequency at which the server should be polled. **Default = 50ms** 34 | /// 35 | /// `timeout` = The maximum time to wait for processing to complete. **Default = 5000ms** 36 | /// 37 | /// If the waited time exceeds `timeout` then an [`Error::Timeout`] will be returned. 38 | /// 39 | /// See also [`Client::wait_for_task`, `Index::wait_for_task`]. 40 | /// 41 | /// # Example 42 | /// 43 | /// ``` 44 | /// # use meilisearch_sdk::{client::*, indexes::*, tasks::*}; 45 | /// # use serde::{Serialize, Deserialize}; 46 | /// # 47 | /// # #[derive(Debug, Serialize, Deserialize, PartialEq)] 48 | /// # struct Document { 49 | /// # id: usize, 50 | /// # value: String, 51 | /// # kind: String, 52 | /// # } 53 | /// # 54 | /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); 55 | /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); 56 | /// # 57 | /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { 58 | /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); 59 | /// let movies = client.index("movies_wait_for_completion"); 60 | /// 61 | /// let status = movies.add_documents(&[ 62 | /// Document { id: 0, kind: "title".into(), value: "The Social Network".to_string() }, 63 | /// Document { id: 1, kind: "title".into(), value: "Harry Potter and the Sorcerer's Stone".to_string() }, 64 | /// ], None) 65 | /// .await 66 | /// .unwrap() 67 | /// .wait_for_completion(&client, None, None) 68 | /// .await 69 | /// .unwrap(); 70 | /// 71 | /// assert!(matches!(status, Task::Succeeded { .. })); 72 | /// # movies.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); 73 | /// # }); 74 | /// ``` 75 | pub async fn wait_for_completion( 76 | self, 77 | client: &Client, 78 | interval: Option, 79 | timeout: Option, 80 | ) -> Result { 81 | client.wait_for_task(self, interval, timeout).await 82 | } 83 | } 84 | 85 | #[cfg(test)] 86 | mod test { 87 | use super::*; 88 | use crate::{ 89 | client::*, 90 | errors::{ErrorCode, ErrorType}, 91 | indexes::Index, 92 | }; 93 | use big_s::S; 94 | use meilisearch_test_macro::meilisearch_test; 95 | use serde::{Deserialize, Serialize}; 96 | use std::time::Duration; 97 | 98 | #[derive(Debug, Serialize, Deserialize, PartialEq)] 99 | struct Document { 100 | id: usize, 101 | value: String, 102 | kind: String, 103 | } 104 | 105 | #[test] 106 | fn test_deserialize_task_info() { 107 | let datetime = OffsetDateTime::parse( 108 | "2022-02-03T13:02:38.369634Z", 109 | &time::format_description::well_known::Rfc3339, 110 | ) 111 | .unwrap(); 112 | 113 | let task_info: TaskInfo = serde_json::from_str( 114 | r#" 115 | { 116 | "enqueuedAt": "2022-02-03T13:02:38.369634Z", 117 | "indexUid": "meili", 118 | "status": "enqueued", 119 | "type": "documentAdditionOrUpdate", 120 | "taskUid": 12 121 | }"#, 122 | ) 123 | .unwrap(); 124 | 125 | assert!(matches!( 126 | task_info, 127 | TaskInfo { 128 | enqueued_at, 129 | index_uid: Some(index_uid), 130 | task_uid: 12, 131 | update_type: TaskType::DocumentAdditionOrUpdate { details: None }, 132 | status, 133 | } 134 | if enqueued_at == datetime && index_uid == "meili" && status == "enqueued")); 135 | } 136 | 137 | #[meilisearch_test] 138 | async fn test_wait_for_task_with_args(client: Client, movies: Index) -> Result<(), Error> { 139 | let task_info = movies 140 | .add_documents( 141 | &[ 142 | Document { 143 | id: 0, 144 | kind: "title".into(), 145 | value: S("The Social Network"), 146 | }, 147 | Document { 148 | id: 1, 149 | kind: "title".into(), 150 | value: S("Harry Potter and the Sorcerer's Stone"), 151 | }, 152 | ], 153 | None, 154 | ) 155 | .await?; 156 | 157 | let task = client 158 | .get_task(task_info) 159 | .await? 160 | .wait_for_completion( 161 | &client, 162 | Some(Duration::from_millis(1)), 163 | Some(Duration::from_millis(6000)), 164 | ) 165 | .await?; 166 | 167 | assert!(matches!(task, Task::Succeeded { .. })); 168 | Ok(()) 169 | } 170 | 171 | #[meilisearch_test] 172 | async fn test_failing_task(client: Client, index: Index) -> Result<(), Error> { 173 | let task_info = client.create_index(index.uid, None).await.unwrap(); 174 | let task = client.wait_for_task(task_info, None, None).await?; 175 | 176 | let error = task.unwrap_failure(); 177 | assert_eq!(error.error_code, ErrorCode::IndexAlreadyExists); 178 | assert_eq!(error.error_type, ErrorType::InvalidRequest); 179 | Ok(()) 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /src/tenant_tokens.rs: -------------------------------------------------------------------------------- 1 | use crate::errors::Error; 2 | use jsonwebtoken::{encode, EncodingKey, Header}; 3 | use serde::{Deserialize, Serialize}; 4 | use serde_json::Value; 5 | use time::OffsetDateTime; 6 | #[cfg(not(target_arch = "wasm32"))] 7 | use uuid::Uuid; 8 | 9 | #[derive(Debug, Serialize, Deserialize)] 10 | #[serde(rename_all = "camelCase")] 11 | struct TenantTokenClaim { 12 | api_key_uid: String, 13 | search_rules: Value, 14 | #[serde(with = "time::serde::timestamp::option")] 15 | exp: Option, 16 | } 17 | 18 | pub fn generate_tenant_token( 19 | api_key_uid: String, 20 | search_rules: Value, 21 | api_key: impl AsRef, 22 | expires_at: Option, 23 | ) -> Result { 24 | // Validate uuid format 25 | let uid = Uuid::try_parse(&api_key_uid)?; 26 | 27 | // Validate uuid version 28 | if uid.get_version_num() != 4 { 29 | return Err(Error::InvalidUuid4Version); 30 | } 31 | 32 | if expires_at.is_some_and(|expires_at| OffsetDateTime::now_utc() > expires_at) { 33 | return Err(Error::TenantTokensExpiredSignature); 34 | } 35 | 36 | let claims = TenantTokenClaim { 37 | api_key_uid, 38 | exp: expires_at, 39 | search_rules, 40 | }; 41 | 42 | let token = encode( 43 | &Header::default(), 44 | &claims, 45 | &EncodingKey::from_secret(api_key.as_ref().as_bytes()), 46 | ); 47 | 48 | Ok(token?) 49 | } 50 | 51 | #[cfg(test)] 52 | mod tests { 53 | use crate::tenant_tokens::*; 54 | use big_s::S; 55 | use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation}; 56 | use serde_json::json; 57 | use std::collections::HashSet; 58 | 59 | const SEARCH_RULES: [&str; 1] = ["*"]; 60 | const VALID_KEY: &str = "a19b6ec84ee31324efa560cd1f7e6939"; 61 | 62 | fn build_validation() -> Validation { 63 | let mut validation = Validation::new(Algorithm::HS256); 64 | validation.validate_exp = false; 65 | validation.required_spec_claims = HashSet::new(); 66 | 67 | validation 68 | } 69 | 70 | #[test] 71 | fn test_generate_token_with_given_key() { 72 | let api_key_uid = S("76cf8b87-fd12-4688-ad34-260d930ca4f4"); 73 | let token = 74 | generate_tenant_token(api_key_uid, json!(SEARCH_RULES), VALID_KEY, None).unwrap(); 75 | 76 | let valid_key = decode::( 77 | &token, 78 | &DecodingKey::from_secret(VALID_KEY.as_ref()), 79 | &build_validation(), 80 | ); 81 | let invalid_key = decode::( 82 | &token, 83 | &DecodingKey::from_secret("not-the-same-key".as_ref()), 84 | &build_validation(), 85 | ); 86 | 87 | assert!(valid_key.is_ok()); 88 | assert!(invalid_key.is_err()); 89 | } 90 | 91 | #[test] 92 | fn test_generate_token_without_uid() { 93 | let api_key_uid = S(""); 94 | let key = S(""); 95 | let token = generate_tenant_token(api_key_uid, json!(SEARCH_RULES), key, None); 96 | 97 | assert!(token.is_err()); 98 | } 99 | 100 | #[test] 101 | fn test_generate_token_with_expiration() { 102 | let api_key_uid = S("76cf8b87-fd12-4688-ad34-260d930ca4f4"); 103 | let exp = OffsetDateTime::now_utc() + time::Duration::HOUR; 104 | let token = 105 | generate_tenant_token(api_key_uid, json!(SEARCH_RULES), VALID_KEY, Some(exp)).unwrap(); 106 | 107 | let decoded = decode::( 108 | &token, 109 | &DecodingKey::from_secret(VALID_KEY.as_ref()), 110 | &Validation::new(Algorithm::HS256), 111 | ); 112 | 113 | assert!(decoded.is_ok()); 114 | } 115 | 116 | #[test] 117 | fn test_generate_token_with_expires_at_in_the_past() { 118 | let api_key_uid = S("76cf8b87-fd12-4688-ad34-260d930ca4f4"); 119 | let exp = OffsetDateTime::now_utc() - time::Duration::HOUR; 120 | let token = generate_tenant_token(api_key_uid, json!(SEARCH_RULES), VALID_KEY, Some(exp)); 121 | 122 | assert!(token.is_err()); 123 | } 124 | 125 | #[test] 126 | fn test_generate_token_contains_claims() { 127 | let api_key_uid = S("76cf8b87-fd12-4688-ad34-260d930ca4f4"); 128 | let token = 129 | generate_tenant_token(api_key_uid.clone(), json!(SEARCH_RULES), VALID_KEY, None) 130 | .unwrap(); 131 | 132 | let decoded = decode::( 133 | &token, 134 | &DecodingKey::from_secret(VALID_KEY.as_ref()), 135 | &build_validation(), 136 | ) 137 | .expect("Cannot decode the token"); 138 | 139 | assert_eq!(decoded.claims.api_key_uid, api_key_uid); 140 | assert_eq!(decoded.claims.search_rules, json!(SEARCH_RULES)); 141 | } 142 | 143 | #[test] 144 | fn test_generate_token_with_multi_byte_chars() { 145 | let api_key_uid = S("76cf8b87-fd12-4688-ad34-260d930ca4f4"); 146 | let key = "Ëa1ทt9bVcL-vãUทtP3OpXW5qPc%bWH5ทvw09"; 147 | let token = 148 | generate_tenant_token(api_key_uid.clone(), json!(SEARCH_RULES), key, None).unwrap(); 149 | 150 | let decoded = decode::( 151 | &token, 152 | &DecodingKey::from_secret(key.as_ref()), 153 | &build_validation(), 154 | ) 155 | .expect("Cannot decode the token"); 156 | 157 | assert_eq!(decoded.claims.api_key_uid, api_key_uid); 158 | } 159 | 160 | #[test] 161 | fn test_generate_token_with_wrongly_formatted_uid() { 162 | let api_key_uid = S("xxx"); 163 | let key = "Ëa1ทt9bVcL-vãUทtP3OpXW5qPc%bWH5ทvw09"; 164 | let token = generate_tenant_token(api_key_uid, json!(SEARCH_RULES), key, None); 165 | 166 | assert!(token.is_err()); 167 | } 168 | 169 | #[test] 170 | fn test_generate_token_with_wrong_uid_version() { 171 | let api_key_uid = S("6a11eb96-2485-11ed-861d-0242ac120002"); 172 | let key = "Ëa1ทt9bVcL-vãUทtP3OpXW5qPc%bWH5ทvw09"; 173 | let token = generate_tenant_token(api_key_uid, json!(SEARCH_RULES), key, None); 174 | 175 | assert!(token.is_err()); 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /src/utils.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | #[cfg(not(target_arch = "wasm32"))] 4 | pub(crate) async fn async_sleep(interval: Duration) { 5 | let (sender, receiver) = futures::channel::oneshot::channel::<()>(); 6 | std::thread::spawn(move || { 7 | std::thread::sleep(interval); 8 | let _ = sender.send(()); 9 | }); 10 | let _ = receiver.await; 11 | } 12 | 13 | #[cfg(target_arch = "wasm32")] 14 | pub(crate) async fn async_sleep(interval: Duration) { 15 | use std::convert::TryInto; 16 | use wasm_bindgen_futures::JsFuture; 17 | 18 | JsFuture::from(web_sys::js_sys::Promise::new(&mut |yes, _| { 19 | web_sys::window() 20 | .unwrap() 21 | .set_timeout_with_callback_and_timeout_and_arguments_0( 22 | &yes, 23 | interval.as_millis().try_into().unwrap(), 24 | ) 25 | .unwrap(); 26 | })) 27 | .await 28 | .unwrap(); 29 | } 30 | 31 | #[cfg(test)] 32 | mod test { 33 | use super::*; 34 | use meilisearch_test_macro::meilisearch_test; 35 | 36 | #[meilisearch_test] 37 | async fn test_async_sleep() { 38 | let sleep_duration = Duration::from_millis(10); 39 | let now = std::time::Instant::now(); 40 | 41 | async_sleep(sleep_duration).await; 42 | 43 | assert!(now.elapsed() >= sleep_duration); 44 | } 45 | } 46 | --------------------------------------------------------------------------------