diff --git a/backend/Cargo.lock b/backend/Cargo.lock index babc3251d..8c1bd77e2 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -1460,8 +1460,6 @@ dependencies = [ [[package]] name = "meilisearch-index-setting-macro" version = "0.27.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "056e8c0652af81cc6525e0d9c0e1037ea7bcd77955dcd4aef1a1441be7ad7e55" dependencies = [ "convert_case", "proc-macro2", @@ -1473,8 +1471,6 @@ dependencies = [ [[package]] name = "meilisearch-sdk" version = "0.27.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66958255878d712b4f2dece377a8661b41dc976ff15f564b91bfce8b4a619304" dependencies = [ "async-trait", "bytes", @@ -2780,6 +2776,7 @@ dependencies = [ name = "tobira" version = "2.13.0" dependencies = [ + "ahash", "anyhow", "base64 0.22.1", "bincode", diff --git a/backend/Cargo.toml b/backend/Cargo.toml index d9ae0ba40..22f62fd8a 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -17,6 +17,7 @@ embed-in-debug = ["reinda/always-prod"] [dependencies] +ahash = "0.8" anyhow = { version = "1.0.71", features = ["backtrace"] } base64 = "0.22.1" bincode = "1.3.3" @@ -42,7 +43,7 @@ hyper-rustls = { version = "0.27.3", default-features = false, features = ["http hyper-util = { version = "0.1.3", features = ["client", "server", "http1", "http2"] } iso8601 = "0.6.1" juniper = { version = "0.16.1", default-features = false, features = ["chrono", "schema-language", "anyhow", "backtrace"] } -meilisearch-sdk = "0.27.1" +meilisearch-sdk = { path = "vendor/meilisearch-sdk" } mime_guess = { version = "2", default-features = false } nu-ansi-term = "0.50.1" ogrim = "0.1.1" diff --git a/backend/src/api/model/search/event.rs b/backend/src/api/model/search/event.rs index 05b902702..793f049a1 100644 --- a/backend/src/api/model/search/event.rs +++ b/backend/src/api/model/search/event.rs @@ -1,6 +1,8 @@ +use std::collections::HashMap; + use chrono::{DateTime, Utc}; use juniper::GraphQLObject; -use meilisearch_sdk::search::SearchResult; +use meilisearch_sdk::search::MatchRange; use crate::{ api::{Context, Id, Node, NodeValue}, @@ -39,7 +41,13 @@ pub struct SearchEventMatches { title: Vec, description: Vec, series_title: Vec, - // TODO: creators + creators: Vec, +} + +#[derive(Debug, GraphQLObject)] +pub struct ArrayMatch { + index: i32, + span: ByteSpan, } /// A match inside an event's texts while searching. @@ -75,10 +83,11 @@ impl SearchEvent { Self::new_inner(src, vec![], SearchEventMatches::default(), user_can_read) } - pub(crate) fn new(hit: SearchResult, context: &Context) -> Self { - let match_positions = hit.matches_position.as_ref(); - let src = hit.result; - + pub(crate) fn new( + src: search::Event, + match_positions: Option<&HashMap>>, + context: &Context, + ) -> Self { let mut text_matches = Vec::new(); let read_roles = decode_acl(&src.read_roles); let user_can_read = context.auth.overlaps_roles(read_roles); @@ -99,6 +108,16 @@ impl SearchEvent { title: field_matches_for(match_positions, "title"), description: field_matches_for(match_positions, "description"), series_title: field_matches_for(match_positions, "series_title"), + creators: match_ranges_for(match_positions, "creators") + .iter() + .filter_map(|m| { + m.indices.as_ref().and_then(|v| v.get(0)).map(|index| ArrayMatch { + span: ByteSpan { start: m.start as i32, len: m.length as i32 }, + index: *index as i32, + }) + }) + .take(8) + .collect(), }; Self::new_inner(src, text_matches, matches, user_can_read) diff --git a/backend/src/api/model/search/mod.rs b/backend/src/api/model/search/mod.rs index e626f97c6..b5afc2649 100644 --- a/backend/src/api/model/search/mod.rs +++ b/backend/src/api/model/search/mod.rs @@ -1,6 +1,6 @@ use chrono::{DateTime, Utc}; use juniper::GraphQLObject; -use meilisearch_sdk::search::MatchRange; +use meilisearch_sdk::search::{FederationOptions, MatchRange, QueryFederationOptions}; use once_cell::sync::Lazy; use regex::Regex; use std::{borrow::Cow, collections::HashMap, fmt, time::Instant}; @@ -204,97 +204,72 @@ pub(crate) async fn perform( ]).to_string(); let event_query = context.search.event_index.search() .with_query(user_query) - .with_limit(15) .with_show_matches_position(true) .with_filter(&filter) - .with_show_ranking_score(true) .build(); - // Prepare the series search let series_query = context.search.series_index.search() .with_query(user_query) .with_show_matches_position(true) .with_filter("listed = true") - .with_limit(15) - .with_show_ranking_score(true) + .with_federation_options(QueryFederationOptions { + weight: Some(1.1), + }) .build(); - // Prepare the realm search let realm_query = context.search.realm_index.search() .with_query(user_query) - .with_limit(10) .with_filter("is_user_realm = false") .with_show_matches_position(true) - .with_show_ranking_score(true) .build(); - - // Perform the searches - let res = tokio::try_join!( - event_query.execute::(), - series_query.execute::(), - realm_query.execute::(), - ); - let (event_results, series_results, realm_results) = handle_search_result!(res, SearchOutcome); - - // Merge results according to Meilis score. - // - // TODO: Comparing scores of different indices is not well defined right now. - // We can either use score details or adding dummy searchable fields to the - // realm index. See this discussion for more info: - // https://github.com/orgs/meilisearch/discussions/489#discussioncomment-6160361 - let events = event_results.hits.into_iter().map(|result| { - let score = result.ranking_score; - (NodeValue::from(SearchEvent::new(result, &context)), score) - }); - let series = series_results.hits.into_iter().map(|result| { - let score = result.ranking_score; - (NodeValue::from(SearchSeries::new(result, context)), score) - }); - let realms = realm_results.hits.into_iter().map(|result| { - let score = result.ranking_score; - (NodeValue::from(SearchRealm::new(result)), score) + let mut multi_search = context.search.client.multi_search(); + if matches!(filters.item_type, None | Some(ItemType::Event)) { + multi_search.with_search_query(event_query); + } + if matches!(filters.item_type, None | Some(ItemType::Series)) { + multi_search.with_search_query(series_query); + } + if matches!(filters.item_type, None | Some(ItemType::Realm)) { + multi_search.with_search_query(realm_query); + } + let multi_search = multi_search.with_federation(FederationOptions { + limit: Some(30), + offset: Some(0), // TODO: pagination + ..Default::default() }); - let mut merged: Vec<(NodeValue, Option)> = Vec::new(); - let total_hits: usize; - - match filters.item_type { - Some(ItemType::Event) => { - merged.extend(events); - total_hits = event_results.estimated_total_hits.unwrap_or(0); - }, - Some(ItemType::Series) => { - merged.extend(series); - total_hits = series_results.estimated_total_hits.unwrap_or(0); - }, - Some(ItemType::Realm) => { - merged.extend(realms); - total_hits = realm_results.estimated_total_hits.unwrap_or(0); - }, - None => { - merged.extend(events); - merged.extend(series); - merged.extend(realms); - total_hits = [ - event_results.estimated_total_hits, - series_results.estimated_total_hits, - realm_results.estimated_total_hits, - ] - .iter() - .filter_map(|&x| x) - .sum(); - }, - } - merged.sort_unstable_by(|(_, score0), (_, score1)| score1.unwrap().total_cmp(&score0.unwrap())); + #[derive(serde::Deserialize)] + #[serde(untagged)] + enum MultiSearchItem { + Event(search::Event), + Series(search::Series), + Realm(search::Realm), + } - let items = merged.into_iter().map(|(node, _)| node).collect(); + // TODO: Check if sort order makes sense. That's because comparing scores of + // different indices is not well defined right now. We can either use score + // details or adding dummy searchable fields to the realm index. See this + // discussion for more info: + // https://github.com/orgs/meilisearch/discussions/489#discussioncomment-6160361 + let res = handle_search_result!(multi_search.execute::().await, SearchOutcome); + + let items = res.hits.into_iter() + .map(|res| { + let mp = res.matches_position.as_ref(); + match res.result { + MultiSearchItem::Event(event) => NodeValue::from(SearchEvent::new(event, mp, &context)), + MultiSearchItem::Series(series) => NodeValue::from(SearchSeries::new(series, mp, context)), + MultiSearchItem::Realm(realm) => NodeValue::from(SearchRealm::new(realm, mp)), + } + }) + .collect(); Ok(SearchOutcome::Results(SearchResults { items, - total_hits, + total_hits: res.estimated_total_hits, duration: elapsed_time(), })) } @@ -353,7 +328,9 @@ pub(crate) async fn all_events( } let res = query.execute::().await; let results = handle_search_result!(res, EventSearchOutcome); - let items = results.hits.into_iter().map(|h| SearchEvent::new(h, &context)).collect(); + let items = results.hits.into_iter() + .map(|h| SearchEvent::new(h.result, h.matches_position.as_ref(), &context)) + .collect(); let total_hits = results.estimated_total_hits.unwrap_or(0); Ok(EventSearchOutcome::Results(SearchResults { items, total_hits, duration: elapsed_time() })) @@ -405,7 +382,9 @@ pub(crate) async fn all_series( } let res = query.execute::().await; let results = handle_search_result!(res, SeriesSearchOutcome); - let items = results.hits.into_iter().map(|h| SearchSeries::new(h, context)).collect(); + let items = results.hits.into_iter() + .map(|h| SearchSeries::new(h.result, h.matches_position.as_ref(), context)) + .collect(); let total_hits = results.estimated_total_hits.unwrap_or(0); Ok(SeriesSearchOutcome::Results(SearchResults { items, total_hits, duration: elapsed_time() })) diff --git a/backend/src/api/model/search/realm.rs b/backend/src/api/model/search/realm.rs index 6a177ae5c..ebecc33a9 100644 --- a/backend/src/api/model/search/realm.rs +++ b/backend/src/api/model/search/realm.rs @@ -1,5 +1,7 @@ +use std::collections::HashMap; + use juniper::GraphQLObject; -use meilisearch_sdk::search::SearchResult; +use meilisearch_sdk::search::MatchRange; use crate::{ api::{Context, Node, Id, NodeValue}, @@ -36,12 +38,14 @@ impl SearchRealm { Self::new_inner(src, SearchRealmMatches::default()) } - pub(crate) fn new(hit: SearchResult) -> Self { - let match_positions = hit.matches_position.as_ref(); + pub(crate) fn new( + src: search::Realm, + match_positions: Option<&HashMap>>, + ) -> Self { let matches = SearchRealmMatches { name: field_matches_for(match_positions, "name"), }; - Self::new_inner(hit.result, matches) + Self::new_inner(src, matches) } fn new_inner(src: search::Realm, matches: SearchRealmMatches) -> Self { diff --git a/backend/src/api/model/search/series.rs b/backend/src/api/model/search/series.rs index fe85fa3ab..1fed007e0 100644 --- a/backend/src/api/model/search/series.rs +++ b/backend/src/api/model/search/series.rs @@ -1,5 +1,7 @@ +use std::collections::HashMap; + use juniper::GraphQLObject; -use meilisearch_sdk::search::SearchResult; +use meilisearch_sdk::search::MatchRange; use crate::{ api::{Context, Id, Node, NodeValue}, @@ -36,16 +38,15 @@ impl Node for SearchSeries { impl SearchSeries { pub(crate) fn new( - hit: SearchResult, + src: search::Series, + match_positions: Option<&HashMap>>, context: &Context, ) -> Self { - let match_positions = hit.matches_position.as_ref(); let matches = SearchSeriesMatches { title: field_matches_for(match_positions, "title"), description: field_matches_for(match_positions, "description"), }; - let src = hit.result; Self { id: Id::search_series(src.id.0), opencast_id: src.opencast_id, diff --git a/backend/src/search/event.rs b/backend/src/search/event.rs index 13859ed7b..70d2ad10d 100644 --- a/backend/src/search/event.rs +++ b/backend/src/search/event.rs @@ -18,7 +18,11 @@ use crate::{ util::{base64_decode, BASE64_DIGITS}, }; -use super::{realm::Realm, util::{self, FieldAbilities}, IndexItem, IndexItemKind, SearchId}; +use super::{ + realm::Realm, + util::{self, is_stop_word, FieldAbilities}, + IndexItem, IndexItemKind, SearchId, +}; @@ -366,6 +370,18 @@ impl TextSearchIndex { continue; } + // Get correct indices and the actual text snippet. Unfortunately, + // Meilisearch might sometimes return invalid indices that slice + // UTF-8 codepoints in half, so we need to protect against that. + let start = ceil_char_boundary(&self.texts, match_range.start); + let end = ceil_char_boundary(&self.texts, match_range.start + match_range.length); + let snippet = &self.texts[start..end]; + + // If the match is a single stop word, we ignore it. + if is_stop_word(snippet) { + continue; + } + let slot = self.lookup(match_range); let matches = entries.entry(slot as u32).or_insert_with(Vec::new); diff --git a/backend/src/search/mod.rs b/backend/src/search/mod.rs index ad75f15d2..c1512731e 100644 --- a/backend/src/search/mod.rs +++ b/backend/src/search/mod.rs @@ -336,7 +336,7 @@ pub(crate) async fn rebuild_if_necessary( for task in tasks { util::wait_on_task(task, meili).await?; } - info!("Completely rebuild search index"); + info!("Completely rebuilt search index"); meili.meta_index.add_or_replace(&[meta::Meta::current_clean()], None).await .context("failed to update index version document (clean)")?; diff --git a/backend/src/search/stop-words.txt b/backend/src/search/stop-words.txt new file mode 100644 index 000000000..d45d410eb --- /dev/null +++ b/backend/src/search/stop-words.txt @@ -0,0 +1,396 @@ +# Single latin letters +a +b +c +d +e +f +g +h +i +j +k +l +m +n +o +p +q +r +s +t +u +v +w +x +y +z + + +# English +# 'a' and 'i' are already covered by single letters above. +# Based on NLTK's list of english stopwords +about +above +#after -> German word +again +against +all +am +an +and +any +are +as +at +be +because +been +before +being +below +between +both +but +by +can +could +did +do +does +doing +dont +down +during +each +few +for +from +further +had +has +have +having +he +her +here +hers +herself +him +himself +his +how +however +if +in +into +is +it +its +itself +just +like +many +me +more +#most -> German word +must +my +myself +no +nor +not # -> German word, not super common as stand-alone word and very much a English stop word, so we keep it +now +of +off +on +once +only +or +other +our +ours +ourselves +out +over +own +said +same +she +should +so +some +such # -> German word, but probably fine to keep it a stop word +than +that +the +their +theirs +them +themselves +then +there +#these -> German word +they +this +those +through +to +too +under +until +up +using +very +was +we +were +what +when +where +which +while +who +whom +why +will +with +would +you +your +yours +yourself +yourselves + + +# German +aber +alle +allem +allen +aller +alles +als +also #-> English word but also kind of stop-wordy, so keeping it +am +an +ander +andere +anderem +anderen +anderer +anderes +anderm +andern +anderr +anders +auch +auf +aus +bei +#bin -> english word +bis +bist +da +damit +dann +der +den +des +dem +#die -> english word +das +dass +daß +dazu +dein +deine +deinem +deinen +deiner +deines +denn +derer +dessen +dich +dir +du +#dies -> english word +diese +diesem +diesen +dieser +dieses +doch +dort +durch +ein +eine +einem +einen +einer +eines +einig +einige +einigem +einigen +einiger +einiges +einmal +er +ihn +ihm +es +etwas +euer +eure +eurem +euren +eurer +eures +für +gab +gegen +gewesen +hab +habe +haben +#hat -> English word +hatte +hatten +hier +hin +hinter +ich +mich +mir +ihr +ihre +ihrem +ihren +ihrer +ihres +euch +im +in +indem +ins +ist +jede +jedem +jeden +jeder +jedes +jene +jenem +jenen +jener +jenes +jetzt +kam +kann +kein +keine +keinem +keinen +keiner +keines +konnte +können +könnte +machen +#man -> English word +manche +manchem +manchen +mancher +manches +mein +meine +meinem +meinen +meiner +meines +mit +muss +musste +nach +nicht +nichts +noch +#nun -> English word +nur +ob +oder +ohne +sehr +sein +seine +seinem +seinen +seiner +seines +selbst +sich +sie +ihnen +sind +so +solche +solchem +solchen +solcher +solches +soll +sollte +sondern +sonst +sowie +über +um +und +uns +unse +unsem +unsen +unser +unses +unter +viel +vom +von +vor +während +#war -> English word +waren +warst +was +weg +weil +weiter +welche +welchem +welchen +welcher +welches +wenn +werde +werden +wie +wieder +will +wir +wird +wirst +wo +wollen +wollte +wurde +wurden +würde +würden +zu +zum +zur +zwar +zwischen diff --git a/backend/src/search/util.rs b/backend/src/search/util.rs index 4292ce87a..d5b5c1587 100644 --- a/backend/src/search/util.rs +++ b/backend/src/search/util.rs @@ -1,5 +1,6 @@ -use std::time::Duration; +use std::{sync::LazyLock, time::Duration}; +use ahash::AHashSet; use meilisearch_sdk::{errors::{Error, ErrorCode}, indexes::Index, tasks::Task, task_info::TaskInfo}; use crate::{ @@ -42,6 +43,35 @@ pub(super) async fn lazy_set_special_attributes( Ok(()) } +// This might seem like a good use case for a perfect hash table, but that's not +// even faster than this solution with a really fast hash. See +// https://github.com/LukasKalbertodt/case-insensitive-small-set-bench +pub static STOP_WORDS: LazyLock> = LazyLock::new(|| { + const RAW: &str = include_str!("stop-words.txt"); + RAW.lines() + .map(|l| l.split('#').next().unwrap().trim()) + .filter(|s| !s.is_empty()) + .collect() +}); + +/// Returns `true` iff the given string is contained in our list of stop words. +/// The comparison ignores ASCII case. +/// +/// We do have a few stop words with non-ASCII chars, but those are only in the +/// middle of the word. And ASCII-lowercasing is much easier and therefore +/// faster than proper Unicode-lowercasing. +pub fn is_stop_word(s: &str) -> bool { + if s.bytes().all(|b| b.is_ascii_lowercase()) { + STOP_WORDS.contains(s) + } else { + // This string allocation seems like it would really hurt + // performance, but it's really not that bad. All in all, doing + // it like this is actually quite fast. See + // https://github.com/LukasKalbertodt/case-insensitive-small-set-bench + STOP_WORDS.contains(s.to_ascii_lowercase().as_str()) + } +} + /// Encodes roles inside an ACL (e.g. for an event) to be stored in the index. /// The roles are hex encoded to be filterable properly with Meili's /// case-insensitive filtering. Also, `ROLE_ADMIN` is removed as an space diff --git a/backend/vendor/README.md b/backend/vendor/README.md new file mode 100644 index 000000000..4ded501e1 --- /dev/null +++ b/backend/vendor/README.md @@ -0,0 +1,17 @@ +# Vendored dependencies + +For the backend, we mainly use crates.io dependencies, which can be considered immutable. +Sometimes, we send patches to some of those libraries, which we want to use in Tobira before they are merged/published. +We could use Cargo git-dependencies, but this is problematic for reproducible builds, especially when a fork might be deleted in the future. +In those cases, we vendor these dependencies. +But this is always just temporary and we always want to switch to upstream versions of these dependencies ASAP. + +We also document the exact version used in vendored dependencies in this document: + +## `meilisearch-sdk` + +Base is `40f94024cda09a90b2784121d3237585c7eb8513` with these two PRs applied on top: +- https://github.com/meilisearch/meilisearch-rust/pull/625 (head `40518902db64436778dee233125ebccc9b442bad`) +- https://github.com/meilisearch/meilisearch-rust/pull/632 (head `737b519ddc10561bb4905c706f7b1a8d6d509857`) + +I removed `examples/`, `.git`, `Cargo.lock` (not used anyway if used as library) and a bunch of Rust-unrelated files to shrink the size of this. diff --git a/backend/vendor/meilisearch-sdk/.gitignore b/backend/vendor/meilisearch-sdk/.gitignore new file mode 100644 index 000000000..066c9a27e --- /dev/null +++ b/backend/vendor/meilisearch-sdk/.gitignore @@ -0,0 +1,4 @@ +/target +Cargo.lock +examples/web_app/target/* +.vscode diff --git a/backend/vendor/meilisearch-sdk/CONTRIBUTING.md b/backend/vendor/meilisearch-sdk/CONTRIBUTING.md new file mode 100644 index 000000000..498f8b232 --- /dev/null +++ b/backend/vendor/meilisearch-sdk/CONTRIBUTING.md @@ -0,0 +1,215 @@ +# Contributing + +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. + +- [Assumptions](#assumptions) +- [How to Contribute](#how-to-contribute) +- [Development Workflow](#development-workflow) +- [Git Guidelines](#git-guidelines) +- [Release Process (for internal team only)](#release-process-for-internal-team-only) + + +## Assumptions + +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.** +2. **You've read the Meilisearch [documentation](https://www.meilisearch.com/docs) and the [README](/README.md).** +3. **You know about the [Meilisearch community](https://discord.com/invite/meilisearch). Please use this for help.** + +## How to Contribute + +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). +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. +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). +4. Review the [Development Workflow](#development-workflow) section that describes the steps to maintain the repository. +5. Make the changes on your branch. +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.
+ 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/). + +## Development Workflow + +You can set up your local environment natively or using `docker`, check out the [`docker-compose.yml`](/docker-compose.yml). + +Example of running all the checks with docker: +```bash +docker-compose run --rm package bash -c "cargo test" +``` + +To install dependencies: + +```bash +cargo build --release +``` + +To ensure the same dependency versions in all environments, for example the CI, update the dependencies by running: `cargo update`. + +### Tests + +To run the tests, run: + +```bash +# Tests +curl -L https://install.meilisearch.com | sh # download Meilisearch +./meilisearch --master-key=masterKey --no-analytics # run Meilisearch +cargo test +``` + +There are two kinds of tests, documentation tests and unit tests. +If you need to write or read the unit tests you should consider reading this +[readme](meilisearch-test-macro/README.md) about our custom testing macro. + +Also, the WASM example compilation should be checked: + +```bash +rustup target add wasm32-unknown-unknown +cargo check -p web_app --target wasm32-unknown-unknown +``` + +Each PR should pass the tests to be accepted. + +### Clippy + +Each PR should pass [`clippy`](https://github.com/rust-lang/rust-clippy) (the linter) to be accepted. + +```bash +cargo clippy -- -D warnings +``` + +If you don't have `clippy` installed on your machine yet, run: + +```bash +rustup update +rustup component add clippy +``` + +⚠️ Also, if you have installed `clippy` a long time ago, you might need to update it: + +```bash +rustup update +``` + +### Fmt + +Each PR should pass the format test to be accepted. + +Run the following to fix the formatting errors: + +``` +cargo fmt +``` + +and the following to test if the formatting is correct: +``` +cargo fmt --all -- --check +``` + +### Update the README + +The README is generated. Please do not update manually the `README.md` file. + +Instead, update the `README.tpl` and `src/lib.rs` files, and run: + +```sh +sh scripts/update-readme.sh +``` + +Then, push the changed files. + +You can check the current `README.md` is up-to-date by running: + +```sh +sh scripts/check-readme.sh +# To see the diff +sh scripts/check-readme.sh --diff +``` + +If it's not, the CI will fail on your PR. + +### Yaml lint + +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 .` + +## Git Guidelines + +### Git Branches + +All changes must be made in a branch and submitted as PR. +We do not enforce any branch naming style, but please use something descriptive of your changes. + +### Git Commits + +As minimal requirements, your commit message should: +- be capitalized +- not finished by a dot or any other punctuation character (!,?) +- start with a verb so that we can read your commit message this way: "This commit will ...", where "..." is the commit message. + e.g.: "Fix the home page button" or "Add more tests for create_index method" + +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/). + +### GitHub Pull Requests + +Some notes on GitHub PRs: + +- [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.
+ The draft PR can be very useful if you want to show that you are working on something and make your work visible. +- 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. +- All PRs must be reviewed and approved by at least one maintainer. +- 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/). + +## Release Process (for the internal team only) + +Meilisearch tools follow the [Semantic Versioning Convention](https://semver.org/). + +### Automation to Rebase and Merge the PRs + +This project integrates a bot that helps us manage pull requests merging.
+_[Read more about this](https://github.com/meilisearch/integration-guides/blob/main/resources/bors.md)._ + +### Automated Changelogs + +This project integrates a tool to create automated changelogs.
+_[Read more about this](https://github.com/meilisearch/integration-guides/blob/main/resources/release-drafter.md)._ + +### How to Publish the Release + +⚠️ 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). + +Make a PR modifying the file [`Cargo.toml`](/Cargo.toml): + +```toml +version = "X.X.X" +``` + +the [`README.tpl`](/README.tpl): + +```rust +//! meilisearch-sdk = "X.X.X" +``` + +and the [code-samples file](/.code-samples.meilisearch.yaml): + +```yml + meilisearch-sdk = "X.X.X" +``` + +with the right version. + + +After the changes on `Cargo.toml`, run the following command: + +``` +sh scripts/update_macro_versions.sh +``` + +After the changes on `lib.rs`, run the following command: + +```bash +sh scripts/update-readme.sh +``` + +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`. + +GitHub Actions will be triggered and push the package to [crates.io](https://crates.io/crates/meilisearch-sdk). + +
+ +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 ❤️ diff --git a/backend/vendor/meilisearch-sdk/Cargo.toml b/backend/vendor/meilisearch-sdk/Cargo.toml new file mode 100644 index 000000000..b100a1225 --- /dev/null +++ b/backend/vendor/meilisearch-sdk/Cargo.toml @@ -0,0 +1,61 @@ +[package] +name = "meilisearch-sdk" +version = "0.27.1" +authors = ["Mubelotix "] +edition = "2018" +description = "Rust wrapper for the Meilisearch API. Meilisearch is a powerful, fast, open-source, easy to use and deploy search engine." +license = "MIT" +readme = "README.md" +repository = "https://github.com/meilisearch/meilisearch-sdk" +resolver = "2" + +[workspace] +members = ["examples/*"] + +[dependencies] +async-trait = "0.1.51" +iso8601 = "0.6.1" +log = "0.4" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +time = { version = "0.3.7", features = ["serde-well-known", "formatting", "parsing"] } +yaup = "0.3.1" +either = { version = "1.8.0", features = ["serde"] } +thiserror = "1.0.37" +meilisearch-index-setting-macro = { path = "meilisearch-index-setting-macro", version = "0.27.1" } +pin-project-lite = { version = "0.2.13", optional = true } +reqwest = { version = "0.12.3", optional = true, default-features = false, features = ["rustls-tls", "http2", "stream"] } +bytes = { version = "1.6", optional = true } +uuid = { version = "1.1.2", features = ["v4"] } +futures-io = "0.3.30" +futures = "0.3" + +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +jsonwebtoken = { version = "9", default-features = false } + +[target.'cfg(target_arch = "wasm32")'.dependencies] +uuid = { version = "1.8.0", default-features = false, features = ["v4", "js"] } +web-sys = "0.3" +wasm-bindgen-futures = "0.4" + +[features] +default = ["reqwest"] +reqwest = ["dep:reqwest", "pin-project-lite", "bytes"] +futures-unsend = [] + +[dev-dependencies] +futures-await-test = "0.3" +futures = "0.3" +mockito = "1.0.0" +meilisearch-test-macro = { path = "meilisearch-test-macro" } +tokio = { version = "1", features = ["rt", "macros"] } + +# The following dependencies are required for examples +wasm-bindgen = "0.2" +wasm-bindgen-futures = "0.4" +yew = "0.21" +lazy_static = "1.4" +web-sys = "0.3" +console_error_panic_hook = "0.1" +big_s = "1.0.2" +insta = "1.38.0" diff --git a/backend/vendor/meilisearch-sdk/LICENSE b/backend/vendor/meilisearch-sdk/LICENSE new file mode 100644 index 000000000..1b9f856e3 --- /dev/null +++ b/backend/vendor/meilisearch-sdk/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020-2025 Meili SAS + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/backend/vendor/meilisearch-sdk/README.md b/backend/vendor/meilisearch-sdk/README.md new file mode 100644 index 000000000..f70c3f907 --- /dev/null +++ b/backend/vendor/meilisearch-sdk/README.md @@ -0,0 +1,258 @@ + + + + +

+ Meilisearch-Rust +

+ +

Meilisearch Rust SDK

+ +

+ Meilisearch | + Meilisearch Cloud | + Documentation | + Discord | + Roadmap | + Website | + FAQ +

+ +

+ crates.io + Tests + License + + Bors enabled +

+ +

⚡ The Meilisearch API client written for Rust 🦀

+ +**Meilisearch Rust** is the Meilisearch API client for Rust developers. + +**Meilisearch** is an open-source search engine. [Learn more about Meilisearch.](https://github.com/meilisearch/meilisearch) + +## Table of Contents + +- [📖 Documentation](#-documentation) +- [🔧 Installation](#-installation) +- [🚀 Getting started](#-getting-started) +- [🌐 Running in the Browser with WASM](#-running-in-the-browser-with-wasm) +- [🤖 Compatibility with Meilisearch](#-compatibility-with-meilisearch) +- [⚙️ Contributing](#️-contributing) + +## 📖 Documentation + +This readme contains all the documentation you need to start using this Meilisearch SDK. + +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). + +## 🔧 Installation + +To use `meilisearch-sdk`, add this to your `Cargo.toml`: + +```toml +[dependencies] +meilisearch-sdk = "0.27.1" +``` + +The following optional dependencies may also be useful: + +```toml +futures = "0.3" # To be able to block on async functions if you are not using an async runtime +serde = { version = "1.0", features = ["derive"] } +``` + +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). +You can enable the `sync` feature to make most structs `Sync`. It may be a bit slower. + +Using this crate is possible without [serde](https://crates.io/crates/serde), but a lot of features require serde. + +### Run Meilisearch + +⚡️ **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). + +🪨 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. + +## 🚀 Getting started + +#### Add Documents + +```rust +use meilisearch_sdk::client::*; +use serde::{Serialize, Deserialize}; +use futures::executor::block_on; + +#[derive(Serialize, Deserialize, Debug)] +struct Movie { + id: usize, + title: String, + genres: Vec, +} + + +#[tokio::main(flavor = "current_thread")] +async fn main() { + // Create a client (without sending any request so that can't fail) + let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + + // An index is where the documents are stored. + let movies = client.index("movies"); + + // Add some movies in the index. If the index 'movies' does not exist, Meilisearch creates it when you first add the documents. + movies.add_documents(&[ + Movie { id: 1, title: String::from("Carol"), genres: vec!["Romance".to_string(), "Drama".to_string()] }, + Movie { id: 2, title: String::from("Wonder Woman"), genres: vec!["Action".to_string(), "Adventure".to_string()] }, + Movie { id: 3, title: String::from("Life of Pi"), genres: vec!["Adventure".to_string(), "Drama".to_string()] }, + Movie { id: 4, title: String::from("Mad Max"), genres: vec!["Adventure".to_string(), "Science Fiction".to_string()] }, + Movie { id: 5, title: String::from("Moana"), genres: vec!["Fantasy".to_string(), "Action".to_string()] }, + Movie { id: 6, title: String::from("Philadelphia"), genres: vec!["Drama".to_string()] }, + ], Some("id")).await.unwrap(); +} +``` + +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). + +#### Basic Search + +```rust +// Meilisearch is typo-tolerant: +println!("{:?}", client.index("movies_2").search().with_query("caorl").execute::().await.unwrap().hits); +``` + +Output: +``` +[Movie { id: 1, title: String::from("Carol"), genres: vec!["Romance", "Drama"] }] +``` + +Json output: +```json +{ + "hits": [{ + "id": 1, + "title": "Carol", + "genres": ["Romance", "Drama"] + }], + "offset": 0, + "limit": 10, + "processingTimeMs": 1, + "query": "caorl" +} +``` + +#### Custom Search + +```rust +let search_result = client.index("movies_3") + .search() + .with_query("phil") + .with_attributes_to_highlight(Selectors::Some(&["*"])) + .execute::() + .await + .unwrap(); +println!("{:?}", search_result.hits); +``` + +Json output: +```json +{ + "hits": [ + { + "id": 6, + "title": "Philadelphia", + "_formatted": { + "id": 6, + "title": "Philadelphia", + "genre": ["Drama"] + } + } + ], + "offset": 0, + "limit": 20, + "processingTimeMs": 0, + "query": "phil" +} +``` + +#### Custom Search With Filters + +If you want to enable filtering, you must add your attributes to the `filterableAttributes` +index setting. + +```rust +let filterable_attributes = [ + "id", + "genres", +]; +client.index("movies_4").set_filterable_attributes(&filterable_attributes).await.unwrap(); +``` + +You only need to perform this operation once. + +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). + +Then, you can perform the search: + +```rust +let search_result = client.index("movies_5") + .search() + .with_query("wonder") + .with_filter("id > 1 AND genres = Action") + .execute::() + .await + .unwrap(); +println!("{:?}", search_result.hits); +``` + +Json output: +```json +{ + "hits": [ + { + "id": 2, + "title": "Wonder Woman", + "genres": ["Action", "Adventure"] + } + ], + "offset": 0, + "limit": 20, + "estimatedTotalHits": 1, + "processingTimeMs": 0, + "query": "wonder" +} +``` + +#### Customize the `HttpClient` + +By default, the SDK uses [`reqwest`](https://docs.rs/reqwest/latest/reqwest/) to make http calls. +The SDK lets you customize the http client by implementing the `HttpClient` trait yourself and +initializing the `Client` with the `new_with_client` method. +You may be interested by the `futures-unsend` feature which lets you specify a non-Send http client. + +#### Wasm support + +The SDK supports wasm through reqwest. You'll need to enable the `futures-unsend` feature while importing it, though. + +## 🌐 Running in the Browser with WASM + +This crate fully supports WASM. + +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. + +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). + +WARNING: `meilisearch-sdk` will panic if no Window is available (ex: Web extension). + +## 🤖 Compatibility with Meilisearch + +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. + +## ⚙️ Contributing + +Any new contribution is more than welcome in this project! + +If you want to know more about the development workflow or want to contribute, please visit our [contributing guidelines](/CONTRIBUTING.md) for detailed instructions! + +
+ +**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. diff --git a/backend/vendor/meilisearch-sdk/README.tpl b/backend/vendor/meilisearch-sdk/README.tpl new file mode 100644 index 000000000..6fec810f9 --- /dev/null +++ b/backend/vendor/meilisearch-sdk/README.tpl @@ -0,0 +1,101 @@ + + + + +

+ Meilisearch-Rust +

+ +

Meilisearch Rust SDK

+ +

+ Meilisearch | + Meilisearch Cloud | + Documentation | + Discord | + Roadmap | + Website | + FAQ +

+ +

+ crates.io + Tests + License + + Bors enabled +

+ +

⚡ The Meilisearch API client written for Rust 🦀

+ +**Meilisearch Rust** is the Meilisearch API client for Rust developers. + +**Meilisearch** is an open-source search engine. [Learn more about Meilisearch.](https://github.com/meilisearch/meilisearch) + +## Table of Contents + +- [📖 Documentation](#-documentation) +- [🔧 Installation](#-installation) +- [🚀 Getting started](#-getting-started) +- [🌐 Running in the Browser with WASM](#-running-in-the-browser-with-wasm) +- [🤖 Compatibility with Meilisearch](#-compatibility-with-meilisearch) +- [⚙️ Contributing](#️-contributing) + +## 📖 Documentation + +This readme contains all the documentation you need to start using this Meilisearch SDK. + +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). + +## 🔧 Installation + +To use `meilisearch-sdk`, add this to your `Cargo.toml`: + +```toml +[dependencies] +meilisearch-sdk = "0.27.1" +``` + +The following optional dependencies may also be useful: + +```toml +futures = "0.3" # To be able to block on async functions if you are not using an async runtime +serde = { version = "1.0", features = ["derive"] } +``` + +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). +You can enable the `sync` feature to make most structs `Sync`. It may be a bit slower. + +Using this crate is possible without [serde](https://crates.io/crates/serde), but a lot of features require serde. + +### Run Meilisearch + +⚡️ **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). + +🪨 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. + +{{readme}} + +## 🌐 Running in the Browser with WASM + +This crate fully supports WASM. + +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. + +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). + +WARNING: `meilisearch-sdk` will panic if no Window is available (ex: Web extension). + +## 🤖 Compatibility with Meilisearch + +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. + +## ⚙️ Contributing + +Any new contribution is more than welcome in this project! + +If you want to know more about the development workflow or want to contribute, please visit our [contributing guidelines](/CONTRIBUTING.md) for detailed instructions! + +
+ +**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. diff --git a/backend/vendor/meilisearch-sdk/bors.toml b/backend/vendor/meilisearch-sdk/bors.toml new file mode 100644 index 000000000..83ffb5041 --- /dev/null +++ b/backend/vendor/meilisearch-sdk/bors.toml @@ -0,0 +1,10 @@ +status = [ + 'integration-tests', + 'clippy-check', + 'rust-format', + 'readme-check', + 'wasm-build', + 'Yaml linting check' +] +# 1 hour timeout +timeout-sec = 3600 diff --git a/backend/vendor/meilisearch-sdk/docker-compose.yml b/backend/vendor/meilisearch-sdk/docker-compose.yml new file mode 100644 index 000000000..edd0bcf3c --- /dev/null +++ b/backend/vendor/meilisearch-sdk/docker-compose.yml @@ -0,0 +1,31 @@ +version: "3.8" + +# remove this line if you don't need a volume to map your dependencies +# Check how to cache the build +volumes: + cargo: + +services: + package: + image: rust:1 + tty: true + stdin_open: true + working_dir: /home/package + environment: + - MEILISEARCH_URL=http://meilisearch:7700 + - CARGO_HOME=/vendor/cargo + depends_on: + - meilisearch + links: + - meilisearch + volumes: + - ./:/home/package + - cargo:/vendor/cargo + + meilisearch: + image: getmeili/meilisearch:latest + ports: + - "7700" + environment: + - MEILI_MASTER_KEY=masterKey + - MEILI_NO_ANALYTICS=true diff --git a/backend/vendor/meilisearch-sdk/meilisearch-index-setting-macro/Cargo.toml b/backend/vendor/meilisearch-sdk/meilisearch-index-setting-macro/Cargo.toml new file mode 100644 index 000000000..4f921389a --- /dev/null +++ b/backend/vendor/meilisearch-sdk/meilisearch-index-setting-macro/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "meilisearch-index-setting-macro" +version = "0.27.1" +description = "Helper tool to generate settings of a Meilisearch index" +edition = "2021" +license = "MIT" +repository = "https://github.com/meilisearch/meilisearch-rust" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +proc-macro = true + +[dependencies] +syn = { version = "2.0.48", features = ["extra-traits"] } +quote = "1.0.21" +proc-macro2 = "1.0.46" +convert_case = "0.6.0" +structmeta = "0.3" diff --git a/backend/vendor/meilisearch-sdk/meilisearch-index-setting-macro/src/lib.rs b/backend/vendor/meilisearch-sdk/meilisearch-index-setting-macro/src/lib.rs new file mode 100644 index 000000000..23d89f065 --- /dev/null +++ b/backend/vendor/meilisearch-sdk/meilisearch-index-setting-macro/src/lib.rs @@ -0,0 +1,191 @@ +use convert_case::{Case, Casing}; +use proc_macro2::Ident; +use quote::quote; +use structmeta::{Flag, StructMeta}; +use syn::{parse_macro_input, spanned::Spanned}; + +#[derive(Clone, StructMeta, Default)] +struct FieldAttrs { + primary_key: Flag, + displayed: Flag, + searchable: Flag, + distinct: Flag, + filterable: Flag, + sortable: Flag, +} + +#[proc_macro_derive(IndexConfig, attributes(index_config))] +pub fn generate_index_settings(input: proc_macro::TokenStream) -> proc_macro::TokenStream { + let ast = parse_macro_input!(input as syn::DeriveInput); + + let fields: &syn::Fields = match ast.data { + syn::Data::Struct(ref data) => &data.fields, + _ => { + return proc_macro::TokenStream::from( + syn::Error::new(ast.ident.span(), "Applicable only to struct").to_compile_error(), + ); + } + }; + + let struct_ident = &ast.ident; + + let index_config_implementation = get_index_config_implementation(struct_ident, fields); + proc_macro::TokenStream::from(quote! { + #index_config_implementation + }) +} + +fn get_index_config_implementation( + struct_ident: &Ident, + fields: &syn::Fields, +) -> proc_macro2::TokenStream { + let mut primary_key_attribute = String::new(); + let mut distinct_key_attribute = String::new(); + let mut displayed_attributes = vec![]; + let mut searchable_attributes = vec![]; + let mut filterable_attributes = vec![]; + let mut sortable_attributes = vec![]; + + let index_name = struct_ident + .to_string() + .from_case(Case::UpperCamel) + .to_case(Case::Snake); + + let mut primary_key_found = false; + let mut distinct_found = false; + + for field in fields { + let attrs = field + .attrs + .iter() + .filter(|attr| attr.path().is_ident("index_config")) + .map(|attr| attr.parse_args::().unwrap()) + .collect::>() + .first() + .cloned() + .unwrap_or_default(); + + // Check if the primary key field is unique + if attrs.primary_key.value() { + if primary_key_found { + return syn::Error::new( + field.span(), + "Only one field can be marked as primary key", + ) + .to_compile_error(); + } + primary_key_attribute = field.ident.clone().unwrap().to_string(); + primary_key_found = true; + } + + // Check if the distinct field is unique + if attrs.distinct.value() { + if distinct_found { + return syn::Error::new(field.span(), "Only one field can be marked as distinct") + .to_compile_error(); + } + distinct_key_attribute = field.ident.clone().unwrap().to_string(); + distinct_found = true; + } + + if attrs.displayed.value() { + displayed_attributes.push(field.ident.clone().unwrap().to_string()); + } + + if attrs.searchable.value() { + searchable_attributes.push(field.ident.clone().unwrap().to_string()); + } + + if attrs.filterable.value() { + filterable_attributes.push(field.ident.clone().unwrap().to_string()); + } + + if attrs.sortable.value() { + sortable_attributes.push(field.ident.clone().unwrap().to_string()); + } + } + + let primary_key_token: proc_macro2::TokenStream = if primary_key_attribute.is_empty() { + quote! { + ::std::option::Option::None + } + } else { + quote! { + ::std::option::Option::Some(#primary_key_attribute) + } + }; + + let display_attr_tokens = + get_settings_token_for_list(&displayed_attributes, "with_displayed_attributes"); + let sortable_attr_tokens = + get_settings_token_for_list(&sortable_attributes, "with_sortable_attributes"); + let filterable_attr_tokens = + get_settings_token_for_list(&filterable_attributes, "with_filterable_attributes"); + let searchable_attr_tokens = + get_settings_token_for_list(&searchable_attributes, "with_searchable_attributes"); + let distinct_attr_token = get_settings_token_for_string_for_some_string( + &distinct_key_attribute, + "with_distinct_attribute", + ); + + quote! { + #[::meilisearch_sdk::macro_helper::async_trait(?Send)] + impl ::meilisearch_sdk::documents::IndexConfig for #struct_ident { + const INDEX_STR: &'static str = #index_name; + + fn generate_settings() -> ::meilisearch_sdk::settings::Settings { + ::meilisearch_sdk::settings::Settings::new() + #display_attr_tokens + #sortable_attr_tokens + #filterable_attr_tokens + #searchable_attr_tokens + #distinct_attr_token + } + + async fn generate_index(client: &::meilisearch_sdk::client::Client) -> std::result::Result<::meilisearch_sdk::indexes::Index, ::meilisearch_sdk::tasks::Task> { + return client.create_index(#index_name, #primary_key_token) + .await.unwrap() + .wait_for_completion(&client, ::std::option::Option::None, ::std::option::Option::None) + .await.unwrap() + .try_make_index(&client); + } + } + } +} + +fn get_settings_token_for_list( + field_name_list: &[String], + method_name: &str, +) -> proc_macro2::TokenStream { + let string_attributes = field_name_list.iter().map(|attr| { + quote! { + #attr + } + }); + let method_ident = Ident::new(method_name, proc_macro2::Span::call_site()); + + if field_name_list.is_empty() { + quote! { + .#method_ident(::std::iter::empty::<&str>()) + } + } else { + quote! { + .#method_ident([#(#string_attributes),*]) + } + } +} + +fn get_settings_token_for_string_for_some_string( + field_name: &String, + method_name: &str, +) -> proc_macro2::TokenStream { + let method_ident = Ident::new(method_name, proc_macro2::Span::call_site()); + + if field_name.is_empty() { + proc_macro2::TokenStream::new() + } else { + quote! { + .#method_ident(::std::option::Option::Some(#field_name)) + } + } +} diff --git a/backend/vendor/meilisearch-sdk/meilisearch-test-macro/Cargo.toml b/backend/vendor/meilisearch-sdk/meilisearch-test-macro/Cargo.toml new file mode 100644 index 000000000..08098d5d3 --- /dev/null +++ b/backend/vendor/meilisearch-sdk/meilisearch-test-macro/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "meilisearch-test-macro" +version = "0.0.0" +edition = "2021" +publish = false + +[lib] +proc-macro = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +proc-macro2 = "1.0.0" +quote = "1.0.0" +syn = { version = "2.0.48", features = ["clone-impls", "full", "parsing", "printing", "proc-macro"], default-features = false } diff --git a/backend/vendor/meilisearch-sdk/meilisearch-test-macro/README.md b/backend/vendor/meilisearch-sdk/meilisearch-test-macro/README.md new file mode 100644 index 000000000..1d794b690 --- /dev/null +++ b/backend/vendor/meilisearch-sdk/meilisearch-test-macro/README.md @@ -0,0 +1,78 @@ +# Meilisearch test macro + +This crate defines the `meilisearch_test` macro. + +Since the code is a little bit harsh to read, here is a complete explanation of how to use it. +The macro aims to ease the writing of tests by: + +1. Reducing the amount of code you need to write and maintain for each test. +2. Ensuring All your indexes as a unique name so they can all run in parallel. +3. Ensuring you never forget to delete your index if you need one. + +Before explaining its usage, we're going to see a simple test _before_ this macro: + +```rust +#[async_test] +async fn test_get_tasks() -> Result<(), Error> { + let client = Client::new(MEILISEARCH_URL, MEILISEARCH_API_KEY); + + let index = client + .create_index("test_get_tasks", None) + .await? + .wait_for_completion(&client, None, None) + .await? + .try_make_index(&client) + .unwrap(); + + let tasks = index.get_tasks().await?; + // The only task is the creation of the index + assert_eq!(status.results.len(), 1); + + index.delete() + .await? + .wait_for_completion(&client, None, None) + .await?; + Ok(()) +} +``` + +I have multiple problems with this test: + +- `let client = Client::new(MEILISEARCH_URL, MEILISEARCH_API_KEY);`: This line is always the same in every test. + And if you make a typo on the http addr or the master key, you'll have an error. +- `let index = client.create_index("test_get_tasks", None)...`: Each test needs to have an unique name. + This means we currently need to write the name of the test everywhere; it's not practical. +- There are 11 lines dedicated to the creation and deletion of the index; this is once again something that'll never change + 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 + the tests. + +--- + +With this macro, all these problems are solved. See a rewrite of this test: + +```rust +#[meilisearch_test] +async fn test_get_tasks(index: Index, client: Client) -> Result<(), Error> { + let tasks = index.get_tasks().await?; + // The only task is the creation of the index + assert_eq!(status.results.len(), 1); +} +``` + +So now you're probably seeing what happened. By using an index and a client in the parameter of +the test, the macro automatically did the same thing we've seen before. +There are a few rules, though: + +1. The macro only handles three types of arguments: + +- `String`: It returns the name of the test. +- `Client`: It creates a client like that: `Client::new("http://localhost:7700", "masterKey")`. +- `Index`: It creates and deletes an index, as we've seen before. + +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. + So, if you are testing the creation of indexes, you can ask for a `Client` and a `String` and then create it yourself. + The index won't be present in meilisearch. +3. You can put your parameters in the order you want it won't change anything. +4. Everything you use **must** be in scope directly. If you're using an `Index`, you must write `Index` in the parameters, + not `meilisearch_rust::Index` or `crate::Index`. +5. And I think that's all, use and abuse it 🎉 diff --git a/backend/vendor/meilisearch-sdk/meilisearch-test-macro/src/lib.rs b/backend/vendor/meilisearch-sdk/meilisearch-test-macro/src/lib.rs new file mode 100644 index 000000000..28d4a440e --- /dev/null +++ b/backend/vendor/meilisearch-sdk/meilisearch-test-macro/src/lib.rs @@ -0,0 +1,184 @@ +#![recursion_limit = "4096"] + +extern crate proc_macro; + +use proc_macro::TokenStream; +use proc_macro2::Span; +use quote::quote; +use syn::{ + parse_macro_input, parse_quote, Expr, FnArg, Ident, Item, PatType, Path, Stmt, Type, TypePath, + Visibility, +}; + +#[proc_macro_attribute] +pub fn meilisearch_test(params: TokenStream, input: TokenStream) -> TokenStream { + assert!( + params.is_empty(), + "the #[async_test] attribute currently does not take parameters" + ); + + let mut inner = parse_macro_input!(input as Item); + let mut outer = inner.clone(); + if let (&mut Item::Fn(ref mut inner_fn), &mut Item::Fn(ref mut outer_fn)) = + (&mut inner, &mut outer) + { + #[derive(Debug, PartialEq, Eq)] + enum Param { + Client, + Index, + String, + } + + inner_fn.sig.ident = Ident::new( + &("_inner_meilisearch_test_macro_".to_string() + &inner_fn.sig.ident.to_string()), + Span::call_site(), + ); + let inner_ident = &inner_fn.sig.ident; + inner_fn.vis = Visibility::Inherited; + inner_fn.attrs.clear(); + assert!( + outer_fn.sig.asyncness.take().is_some(), + "#[meilisearch_test] can only be applied to async functions" + ); + + let mut params = Vec::new(); + + let parameters = &inner_fn.sig.inputs; + for param in parameters { + match param { + FnArg::Typed(PatType { ty, .. }) => match &**ty { + Type::Path(TypePath { path: Path { segments, .. }, .. } ) if segments.last().unwrap().ident == "String" => { + params.push(Param::String); + } + Type::Path(TypePath { path: Path { segments, .. }, .. } ) if segments.last().unwrap().ident == "Index" => { + params.push(Param::Index); + } + Type::Path(TypePath { path: Path { segments, .. }, .. } ) if segments.last().unwrap().ident == "Client" => { + params.push(Param::Client); + } + // TODO: throw this error while pointing to the specific token + ty => panic!( + "#[meilisearch_test] can only receive Client, Index or String as parameters but received {ty:?}" + ), + }, + // TODO: throw this error while pointing to the specific token + // Used `self` as a parameter + FnArg::Receiver(_) => panic!( + "#[meilisearch_test] can only receive Client, Index or String as parameters" + ), + } + } + + // if a `Client` or an `Index` was asked for the test we must create a meilisearch `Client`. + let use_client = params + .iter() + .any(|param| matches!(param, Param::Client | Param::Index)); + // if a `String` or an `Index` was asked then we need to extract the name of the test function. + let use_name = params + .iter() + .any(|param| matches!(param, Param::String | Param::Index)); + let use_index = params.contains(&Param::Index); + + // Now we are going to build the body of the outer function + let mut outer_block: Vec = Vec::new(); + + // First we need to check if a client will be used and create it if it’s the case + if use_client { + outer_block.push(parse_quote!( + let meilisearch_url = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + )); + outer_block.push(parse_quote!( + let meilisearch_api_key = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + )); + outer_block.push(parse_quote!( + let client = Client::new(meilisearch_url, Some(meilisearch_api_key)).unwrap(); + )); + } + + // Now we do the same for the index name + if use_name { + let fn_name = &outer_fn.sig.ident; + // the name we're going to return is the complete path to the function i.e., something like that; + // `indexes::tests::test_fetch_info` but since the `::` are not allowed by meilisearch as an index + // name we're going to rename that to `indexes-tests-test_fetch_info`. + outer_block.push(parse_quote!( + let name = format!("{}::{}", std::module_path!(), stringify!(#fn_name)).replace("::", "-"); + )); + } + + // And finally if an index was asked, we delete it, and we (re)create it and wait until meilisearch confirm its creation. + if use_index { + outer_block.push(parse_quote!({ + let res = client + .delete_index(&name) + .await + .expect("Network issue while sending the delete index task") + .wait_for_completion(&client, None, None) + .await + .expect("Network issue while waiting for the index deletion"); + if res.is_failure() { + let error = res.unwrap_failure(); + assert_eq!( + error.error_code, + crate::errors::ErrorCode::IndexNotFound, + "{:?}", + error + ); + } + })); + + outer_block.push(parse_quote!( + let index = client + .create_index(&name, None) + .await + .expect("Network issue while sending the create index task") + .wait_for_completion(&client, None, None) + .await + .expect("Network issue while waiting for the index creation") + .try_make_index(&client) + .expect("Could not create the index out of the create index task"); + )); + } + + // Create a list of params separated by comma with the name we defined previously. + let params: Vec = params + .into_iter() + .map(|param| match param { + Param::Client => parse_quote!(client), + Param::Index => parse_quote!(index), + Param::String => parse_quote!(name), + }) + .collect(); + + // Now we can call the user code with our parameters :tada: + outer_block.push(parse_quote!( + let result = #inner_ident(#(#params.clone()),*).await; + )); + + // And right before the end, if an index was created and the tests successfully executed we delete it. + if use_index { + outer_block.push(parse_quote!( + index + .delete() + .await + .expect("Network issue while sending the last delete index task"); + // we early exit the test here and let meilisearch handle the deletion asynchronously + )); + } + + // Finally, for the great finish we just return the result the user gave us. + outer_block.push(parse_quote!(return result;)); + + outer_fn.sig.inputs.clear(); + outer_fn.sig.asyncness = inner_fn.sig.asyncness; + outer_fn.attrs.push(parse_quote!(#[tokio::test])); + outer_fn.block.stmts = outer_block; + } else { + panic!("#[meilisearch_test] can only be applied to async functions") + } + quote!( + #inner + #outer + ) + .into() +} diff --git a/backend/vendor/meilisearch-sdk/scripts/check-readme.sh b/backend/vendor/meilisearch-sdk/scripts/check-readme.sh new file mode 100644 index 000000000..bce41d4cd --- /dev/null +++ b/backend/vendor/meilisearch-sdk/scripts/check-readme.sh @@ -0,0 +1,42 @@ +#!/bin/sh + +# Checking that cargo is installed +command -v cargo > /dev/null 2>&1 +if [ "$?" -ne 0 ]; then + echo 'You must install cargo to make this script working.' + echo 'See https://doc.rust-lang.org/cargo/getting-started/installation.html' + exit 1 +fi + +# Installing cargo-readme if it's not installed yet +cargo install cargo-readme + +# Comparing the generated README and the current one +current_readme="README.md" +generated_readme="README.md_tmp" +cargo readme > "$generated_readme" + +# Exiting with the right message +echo '' +diff "$current_readme" "$generated_readme" > /dev/null 2>&1 +if [ "$?" = 0 ]; then + echo "OK" + rm -f "$generated_readme" + exit 0 +else + echo "The current README.md is not up-to-date with the template." + + # Displaying the diff if the --diff flag is activated + if [ "$1" = '--diff' ]; then + echo 'Diff found:' + diff "$current_readme" "$generated_readme" + else + echo 'To see the diff, run:' + echo ' $ sh scripts/check-readme.sh --diff' + echo 'To update the README, run:' + echo ' $ sh scripts/update-readme.sh' + fi + + rm -f "$generated_readme" + exit 1 +fi diff --git a/backend/vendor/meilisearch-sdk/scripts/update-readme.sh b/backend/vendor/meilisearch-sdk/scripts/update-readme.sh new file mode 100644 index 000000000..e2f6dd18e --- /dev/null +++ b/backend/vendor/meilisearch-sdk/scripts/update-readme.sh @@ -0,0 +1,15 @@ +#!/bin/sh + +# Checking that cargo is installed +command -v cargo > /dev/null 2>&1 +if [ "$?" -ne 0 ]; then + echo 'You must install cargo to make this script working.' + echo 'See https://doc.rust-lang.org/cargo/getting-started/installation.html' + exit +fi + +# Installing cargo-readme if it's not installed yet +cargo install cargo-readme + +# Generating the README.md file +cargo readme > README.md diff --git a/backend/vendor/meilisearch-sdk/scripts/update_macro_versions.sh b/backend/vendor/meilisearch-sdk/scripts/update_macro_versions.sh new file mode 100644 index 000000000..1c14eaba3 --- /dev/null +++ b/backend/vendor/meilisearch-sdk/scripts/update_macro_versions.sh @@ -0,0 +1,10 @@ +#!/bin/sh +new_version=$(grep '^version = ' Cargo.toml) + +# Updates the versions in meilisearch-rust and meilisearch-index-setting-macro of the latter, with the latest meilisearch-rust version. + +old_index_macro_version=$(grep '^version = ' ./meilisearch-index-setting-macro/Cargo.toml) +old_macro_in_sdk_version=$(grep '{ path = "meilisearch-index-setting-macro", version =' ./Cargo.toml) + +sed -i '' -e "s/^$old_index_macro_version/$new_version/g" './meilisearch-index-setting-macro/Cargo.toml' +sed -i '' -e "s/$old_macro_in_sdk_version/meilisearch-index-setting-macro = { path = \"meilisearch-index-setting-macro\", $new_version }/g" './Cargo.toml' diff --git a/backend/vendor/meilisearch-sdk/src/.client.rs.pending-snap b/backend/vendor/meilisearch-sdk/src/.client.rs.pending-snap new file mode 100644 index 000000000..6a7b7874e --- /dev/null +++ b/backend/vendor/meilisearch-sdk/src/.client.rs.pending-snap @@ -0,0 +1 @@ +{"run_id":"1736847111-898738061","line":1346,"new":{"module_name":"meilisearch_sdk__client__tests","snapshot_name":"_inner_meilisearch_test_macro_test_error_delete_key","metadata":{"source":"src/client.rs","assertion_line":1346,"expression":"error","snapshot_kind":"text"},"snapshot":"Meilisearch auth: invalid_api_key: The provided API key is invalid.. https://docs.meilisearch.com/errors#invalid_api_key"},"old":{"module_name":"meilisearch_sdk__client__tests","metadata":{"snapshot_kind":"text"},"snapshot":"Meilisearch invalid_request: api_key_not_found: API key `invalid_key` not found.. https://docs.meilisearch.com/errors#api_key_not_found"}} diff --git a/backend/vendor/meilisearch-sdk/src/client.rs b/backend/vendor/meilisearch-sdk/src/client.rs new file mode 100644 index 000000000..3bb578d1e --- /dev/null +++ b/backend/vendor/meilisearch-sdk/src/client.rs @@ -0,0 +1,1622 @@ +use serde::de::Error as SerdeError; +use serde::{de::DeserializeOwned, Deserialize, Serialize}; +use serde_json::{json, Value}; +use std::{collections::HashMap, time::Duration}; +use time::OffsetDateTime; + +use crate::{ + errors::*, + indexes::*, + key::{Key, KeyBuilder, KeyUpdater, KeysQuery, KeysResults}, + request::*, + search::*, + task_info::TaskInfo, + tasks::{Task, TasksCancelQuery, TasksDeleteQuery, TasksResults, TasksSearchQuery}, + utils::async_sleep, + DefaultHttpClient, +}; + +/// The top-level struct of the SDK, representing a client containing [indexes](../indexes/struct.Index.html). +#[derive(Debug, Clone)] +pub struct Client { + pub(crate) host: String, + pub(crate) api_key: Option, + pub(crate) http_client: Http, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SwapIndexes { + pub indexes: (String, String), +} + +#[cfg(feature = "reqwest")] +impl Client { + /// Create a client using the specified server. + /// + /// Don't put a '/' at the end of the host. + /// + /// In production mode, see [the documentation about authentication](https://www.meilisearch.com/docs/learn/security/master_api_keys#authentication). + /// + /// # Example + /// + /// ``` + /// # use meilisearch_sdk::{client::*, indexes::*}; + /// # + /// let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// + /// let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// ``` + pub fn new( + host: impl Into, + api_key: Option>, + ) -> Result { + let api_key = api_key.map(|key| key.into()); + let http_client = crate::reqwest::ReqwestClient::new(api_key.as_deref())?; + + Ok(Client { + host: host.into(), + api_key, + http_client, + }) + } +} + +impl Client { + // Create a client with a custom http client + pub fn new_with_client( + host: impl Into, + api_key: Option>, + http_client: Http, + ) -> Client { + Client { + host: host.into(), + api_key: api_key.map(|key| key.into()), + http_client, + } + } + + fn parse_indexes_results_from_value( + &self, + value: &Value, + ) -> Result, Error> { + let raw_indexes = value["results"] + .as_array() + .ok_or_else(|| serde_json::Error::custom("Missing or invalid 'results' field")) + .map_err(Error::ParseError)?; + + let limit = value["limit"] + .as_u64() + .ok_or_else(|| serde_json::Error::custom("Missing or invalid 'limit' field")) + .map_err(Error::ParseError)? as u32; + + let offset = value["offset"] + .as_u64() + .ok_or_else(|| serde_json::Error::custom("Missing or invalid 'offset' field")) + .map_err(Error::ParseError)? as u32; + + let total = value["total"] + .as_u64() + .ok_or_else(|| serde_json::Error::custom("Missing or invalid 'total' field")) + .map_err(Error::ParseError)? as u32; + + let results = raw_indexes + .iter() + .map(|raw_index| Index::from_value(raw_index.clone(), self.clone())) + .collect::>()?; + + let indexes_results = IndexesResults { + limit, + offset, + total, + results, + }; + + Ok(indexes_results) + } + + pub async fn execute_multi_search_query( + &self, + body: &MultiSearchQuery<'_, '_, Http>, + ) -> Result, Error> { + self.http_client + .request::<(), &MultiSearchQuery, MultiSearchResponse>( + &format!("{}/multi-search", &self.host), + Method::Post { body, query: () }, + 200, + ) + .await + } + + pub async fn execute_federated_multi_search_query< + T: 'static + DeserializeOwned + Send + Sync, + >( + &self, + body: &FederatedMultiSearchQuery<'_, '_, Http>, + ) -> Result, Error> { + self.http_client + .request::<(), &FederatedMultiSearchQuery, FederatedMultiSearchResponse>( + &format!("{}/multi-search", &self.host), + Method::Post { body, query: () }, + 200, + ) + .await + } + + /// Make multiple search requests. + /// + /// # Example + /// + /// ``` + /// # use serde::{Serialize, Deserialize}; + /// # use meilisearch_sdk::{client::*, indexes::*, search::*}; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// #[derive(Serialize, Deserialize, Debug)] + /// struct Movie { + /// name: String, + /// description: String, + /// } + /// + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// let mut movies = client.index("search"); + /// # // add some documents + /// # movies.add_or_replace(&[Movie{name:String::from("Interstellar"), description:String::from("Interstellar chronicles the adventures of a group of explorers who make use of a newly discovered wormhole to surpass the limitations on human space travel and conquer the vast distances involved in an interstellar voyage.")},Movie{name:String::from("Unknown"), description:String::from("Unknown")}], Some("name")).await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// + /// let search_query_1 = SearchQuery::new(&movies) + /// .with_query("Interstellar") + /// .build(); + /// let search_query_2 = SearchQuery::new(&movies) + /// .with_query("") + /// .build(); + /// + /// let response = client + /// .multi_search() + /// .with_search_query(search_query_1) + /// .with_search_query(search_query_2) + /// .execute::() + /// .await + /// .unwrap(); + /// + /// assert_eq!(response.results.len(), 2); + /// # movies.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// # }); + /// ``` + /// + /// # Federated Search + /// + /// You can use [`MultiSearchQuery::with_federation`] to perform a [federated + /// search][1] where results from different indexes are merged and returned as + /// one list. + /// + /// When executing a federated query, the type parameter `T` is less clear, + /// as the documents in the different indexes potentially have different + /// fields and you might have one Rust type per index. In most cases, you + /// either want to create an enum with one variant per index and `#[serde + /// (untagged)]` attribute, or if you need more control, just pass + /// `serde_json::Map` and then deserialize that + /// into the appropriate target types later. + /// + /// [1]: https://www.meilisearch.com/docs/learn/multi_search/multi_search_vs_federated_search#what-is-federated-search + #[must_use] + pub fn multi_search(&self) -> MultiSearchQuery { + MultiSearchQuery::new(self) + } + + /// Return the host associated with this index. + /// + /// # Example + /// + /// ``` + /// # use meilisearch_sdk::{client::*}; + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// let client = Client::new("http://doggo.dog", Some(MEILISEARCH_API_KEY)).unwrap(); + /// + /// assert_eq!(client.get_host(), "http://doggo.dog"); + /// ``` + #[must_use] + pub fn get_host(&self) -> &str { + &self.host + } + + /// Return the api key associated with this index. + /// + /// # Example + /// + /// ``` + /// # use meilisearch_sdk::{client::*}; + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// let client = Client::new(MEILISEARCH_URL, Some("doggo")).unwrap(); + /// + /// assert_eq!(client.get_api_key(), Some("doggo")); + /// ``` + #[must_use] + pub fn get_api_key(&self) -> Option<&str> { + self.api_key.as_deref() + } + + /// List all [Indexes](Index) with query parameters and return values as instances of [Index]. + /// + /// # Example + /// + /// ``` + /// # use meilisearch_sdk::{client::*, indexes::*}; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// let indexes: IndexesResults = client.list_all_indexes().await.unwrap(); + /// + /// let indexes: IndexesResults = client.list_all_indexes().await.unwrap(); + /// println!("{:?}", indexes); + /// # }); + /// ``` + pub async fn list_all_indexes(&self) -> Result, Error> { + let value = self.list_all_indexes_raw().await?; + let indexes_results = self.parse_indexes_results_from_value(&value)?; + Ok(indexes_results) + } + + /// List all [Indexes](Index) and returns values as instances of [Index]. + /// + /// # Example + /// + /// ``` + /// # use meilisearch_sdk::{client::*, indexes::*}; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// let mut query = IndexesQuery::new(&client); + /// query.with_limit(1); + /// + /// let indexes: IndexesResults = client.list_all_indexes_with(&query).await.unwrap(); + /// + /// assert_eq!(indexes.limit, 1); + /// # }); + /// ``` + pub async fn list_all_indexes_with( + &self, + indexes_query: &IndexesQuery<'_, Http>, + ) -> Result, Error> { + let value = self.list_all_indexes_raw_with(indexes_query).await?; + let indexes_results = self.parse_indexes_results_from_value(&value)?; + + Ok(indexes_results) + } + + /// List all [Indexes](Index) and returns as Json. + /// + /// # Example + /// + /// ``` + /// # use meilisearch_sdk::{client::*, indexes::*}; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// let json_indexes = client.list_all_indexes_raw().await.unwrap(); + /// + /// println!("{:?}", json_indexes); + /// # }); + /// ``` + pub async fn list_all_indexes_raw(&self) -> Result { + let json_indexes = self + .http_client + .request::<(), (), Value>( + &format!("{}/indexes", self.host), + Method::Get { query: () }, + 200, + ) + .await?; + + Ok(json_indexes) + } + + /// List all [Indexes](Index) with query parameters and returns as Json. + /// + /// # Example + /// + /// ``` + /// # use meilisearch_sdk::{client::*, indexes::*}; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// let mut query = IndexesQuery::new(&client); + /// query.with_limit(1); + /// + /// let json_indexes = client.list_all_indexes_raw_with(&query).await.unwrap(); + /// + /// println!("{:?}", json_indexes); + /// # }); + /// ``` + pub async fn list_all_indexes_raw_with( + &self, + indexes_query: &IndexesQuery<'_, Http>, + ) -> Result { + let json_indexes = self + .http_client + .request::<&IndexesQuery, (), Value>( + &format!("{}/indexes", self.host), + Method::Get { + query: indexes_query, + }, + 200, + ) + .await?; + + Ok(json_indexes) + } + + /// Get an [Index], this index should already exist. + /// + /// # Example + /// + /// ``` + /// # use meilisearch_sdk::{client::*, indexes::*}; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// # let index = client.create_index("get_index", None).await.unwrap().wait_for_completion(&client, None, None).await.unwrap().try_make_index(&client).unwrap(); + /// let index = client.get_index("get_index").await.unwrap(); + /// + /// assert_eq!(index.as_ref(), "get_index"); + /// # index.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// # }); + /// ``` + pub async fn get_index(&self, uid: impl AsRef) -> Result, Error> { + let mut idx = self.index(uid.as_ref()); + idx.fetch_info().await?; + Ok(idx) + } + + /// Get a raw JSON [Index], this index should already exist. + /// + /// If you use it directly from an [Index], you can use the method [`Index::fetch_info`], which is the equivalent method from an index. + /// + /// # Example + /// + /// ``` + /// # use meilisearch_sdk::{client::*, indexes::*}; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// # let index = client.create_index("get_raw_index", None).await.unwrap().wait_for_completion(&client, None, None).await.unwrap().try_make_index(&client).unwrap(); + /// let raw_index = client.get_raw_index("get_raw_index").await.unwrap(); + /// + /// assert_eq!(raw_index.get("uid").unwrap().as_str().unwrap(), "get_raw_index"); + /// # index.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// # }); + /// ``` + pub async fn get_raw_index(&self, uid: impl AsRef) -> Result { + self.http_client + .request::<(), (), Value>( + &format!("{}/indexes/{}", self.host, uid.as_ref()), + Method::Get { query: () }, + 200, + ) + .await + } + + /// Create a corresponding object of an [Index] without any check or doing an HTTP call. + pub fn index(&self, uid: impl Into) -> Index { + Index::new(uid, self.clone()) + } + + /// Create an [Index]. + /// + /// The second parameter will be used as the primary key of the new index. + /// If it is not specified, Meilisearch will **try** to infer the primary key. + /// + /// # Example + /// + /// ``` + /// # use meilisearch_sdk::{client::*, indexes::*}; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// // Create a new index called movies and access it + /// let task = client.create_index("create_index", None).await.unwrap(); + /// + /// // Wait for the task to complete + /// let task = task.wait_for_completion(&client, None, None).await.unwrap(); + /// + /// // Try to get the inner index if the task succeeded + /// let index = task.try_make_index(&client).unwrap(); + /// + /// assert_eq!(index.as_ref(), "create_index"); + /// # index.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// # }); + /// ``` + pub async fn create_index( + &self, + uid: impl AsRef, + primary_key: Option<&str>, + ) -> Result { + self.http_client + .request::<(), Value, TaskInfo>( + &format!("{}/indexes", self.host), + Method::Post { + query: (), + body: json!({ + "uid": uid.as_ref(), + "primaryKey": primary_key, + }), + }, + 202, + ) + .await + } + + /// Delete an index from its UID. + /// + /// To delete an [Index], use the [`Index::delete`] method. + pub async fn delete_index(&self, uid: impl AsRef) -> Result { + self.http_client + .request::<(), (), TaskInfo>( + &format!("{}/indexes/{}", self.host, uid.as_ref()), + Method::Delete { query: () }, + 202, + ) + .await + } + + /// Alias for [`Client::list_all_indexes`]. + pub async fn get_indexes(&self) -> Result, Error> { + self.list_all_indexes().await + } + + /// Alias for [`Client::list_all_indexes_with`]. + pub async fn get_indexes_with( + &self, + indexes_query: &IndexesQuery<'_, Http>, + ) -> Result, Error> { + self.list_all_indexes_with(indexes_query).await + } + + /// Alias for [`Client::list_all_indexes_raw`]. + pub async fn get_indexes_raw(&self) -> Result { + self.list_all_indexes_raw().await + } + + /// Alias for [`Client::list_all_indexes_raw_with`]. + pub async fn get_indexes_raw_with( + &self, + indexes_query: &IndexesQuery<'_, Http>, + ) -> Result { + self.list_all_indexes_raw_with(indexes_query).await + } + + /// Swaps a list of two [Indexes](Index). + /// + /// # Example + /// + /// ``` + /// # use meilisearch_sdk::{client::*, indexes::*}; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// let task_index_1 = client.create_index("swap_index_1", None).await.unwrap(); + /// let task_index_2 = client.create_index("swap_index_2", None).await.unwrap(); + /// + /// // Wait for the task to complete + /// task_index_2.wait_for_completion(&client, None, None).await.unwrap(); + /// + /// let task = client + /// .swap_indexes([&SwapIndexes { + /// indexes: ( + /// "swap_index_1".to_string(), + /// "swap_index_2".to_string(), + /// ), + /// }]) + /// .await + /// .unwrap(); + /// + /// client.index("swap_index_1").delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// client.index("swap_index_2").delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// # }); + /// ``` + pub async fn swap_indexes( + &self, + indexes: impl IntoIterator, + ) -> Result { + self.http_client + .request::<(), Vec<&SwapIndexes>, TaskInfo>( + &format!("{}/swap-indexes", self.host), + Method::Post { + query: (), + body: indexes.into_iter().collect(), + }, + 202, + ) + .await + } + + /// Get stats of all [Indexes](Index). + /// + /// # Example + /// + /// ``` + /// # use meilisearch_sdk::{client::*, indexes::*}; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// let stats = client.get_stats().await.unwrap(); + /// # }); + /// ``` + pub async fn get_stats(&self) -> Result { + self.http_client + .request::<(), (), ClientStats>( + &format!("{}/stats", self.host), + Method::Get { query: () }, + 200, + ) + .await + } + + /// Get health of Meilisearch server. + /// + /// # Example + /// + /// ``` + /// # use meilisearch_sdk::{client::*, errors::*}; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// let health = client.health().await.unwrap(); + /// + /// assert_eq!(health.status, "available"); + /// # }); + /// ``` + pub async fn health(&self) -> Result { + self.http_client + .request::<(), (), Health>( + &format!("{}/health", self.host), + Method::Get { query: () }, + 200, + ) + .await + } + + /// Get health of Meilisearch server. + /// + /// # Example + /// + /// ``` + /// # use meilisearch_sdk::client::*; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// let health = client.is_healthy().await; + /// + /// assert_eq!(health, true); + /// # }); + /// ``` + pub async fn is_healthy(&self) -> bool { + if let Ok(health) = self.health().await { + health.status.as_str() == "available" + } else { + false + } + } + + /// Get the API [Keys](Key) from Meilisearch with parameters. + /// + /// See [`Client::create_key`], [`Client::get_key`], and the [meilisearch documentation](https://www.meilisearch.com/docs/reference/api/keys#get-all-keys). + /// + /// # Example + /// + /// ``` + /// # use meilisearch_sdk::{client::*, errors::Error, key::KeysQuery}; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// let mut query = KeysQuery::new(); + /// query.with_limit(1); + /// + /// let keys = client.get_keys_with(&query).await.unwrap(); + /// + /// assert_eq!(keys.results.len(), 1); + /// # }); + /// ``` + pub async fn get_keys_with(&self, keys_query: &KeysQuery) -> Result { + let keys = self + .http_client + .request::<&KeysQuery, (), KeysResults>( + &format!("{}/keys", self.host), + Method::Get { query: keys_query }, + 200, + ) + .await?; + + Ok(keys) + } + + /// Get the API [Keys](Key) from Meilisearch. + /// + /// See [`Client::create_key`], [`Client::get_key`], and the [meilisearch documentation](https://www.meilisearch.com/docs/reference/api/keys#get-all-keys). + /// + /// # Example + /// + /// ``` + /// # use meilisearch_sdk::{client::*, errors::Error, key::KeyBuilder}; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// let keys = client.get_keys().await.unwrap(); + /// + /// assert_eq!(keys.limit, 20); + /// # }); + /// ``` + pub async fn get_keys(&self) -> Result { + let keys = self + .http_client + .request::<(), (), KeysResults>( + &format!("{}/keys", self.host), + Method::Get { query: () }, + 200, + ) + .await?; + + Ok(keys) + } + + /// Get one API [Key] from Meilisearch. + /// + /// See also [`Client::create_key`], [`Client::get_keys`], and the [meilisearch documentation](https://www.meilisearch.com/docs/reference/api/keys#get-one-key). + /// + /// # Example + /// + /// ``` + /// # use meilisearch_sdk::{client::*, errors::Error, key::KeyBuilder}; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// # let key = client.get_keys().await.unwrap().results.into_iter() + /// # .find(|k| k.name.as_ref().map_or(false, |name| name.starts_with("Default Search API Key"))) + /// # .expect("No default search key"); + /// let key = client.get_key(key).await.expect("Invalid key"); + /// + /// assert_eq!(key.name, Some("Default Search API Key".to_string())); + /// # }); + /// ``` + pub async fn get_key(&self, key: impl AsRef) -> Result { + self.http_client + .request::<(), (), Key>( + &format!("{}/keys/{}", self.host, key.as_ref()), + Method::Get { query: () }, + 200, + ) + .await + } + + /// Delete an API [Key] from Meilisearch. + /// + /// See also [`Client::create_key`], [`Client::update_key`], [`Client::get_key`], and the [meilisearch documentation](https://www.meilisearch.com/docs/reference/api/keys#delete-a-key). + /// + /// # Example + /// + /// ``` + /// # use meilisearch_sdk::{client::*, errors::Error, key::KeyBuilder}; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// let key = KeyBuilder::new(); + /// let key = client.create_key(key).await.unwrap(); + /// let inner_key = key.key.clone(); + /// + /// client.delete_key(key).await.unwrap(); + /// + /// let keys = client.get_keys().await.unwrap(); + /// + /// assert!(keys.results.iter().all(|key| key.key != inner_key)); + /// # }); + /// ``` + pub async fn delete_key(&self, key: impl AsRef) -> Result<(), Error> { + self.http_client + .request::<(), (), ()>( + &format!("{}/keys/{}", self.host, key.as_ref()), + Method::Delete { query: () }, + 204, + ) + .await + } + + /// Create an API [Key] in Meilisearch. + /// + /// See also [`Client::update_key`], [`Client::delete_key`], [`Client::get_key`], and the [meilisearch documentation](https://www.meilisearch.com/docs/reference/api/keys#create-a-key). + /// + /// # Example + /// + /// ``` + /// # use meilisearch_sdk::{client::*, errors::Error, key::*}; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// let name = "create_key".to_string(); + /// let mut key = KeyBuilder::new(); + /// key.with_name(&name); + /// + /// let key = client.create_key(key).await.unwrap(); + /// + /// assert_eq!(key.name, Some(name)); + /// # client.delete_key(key).await.unwrap(); + /// # }); + /// ``` + pub async fn create_key(&self, key: impl AsRef) -> Result { + self.http_client + .request::<(), &KeyBuilder, Key>( + &format!("{}/keys", self.host), + Method::Post { + query: (), + body: key.as_ref(), + }, + 201, + ) + .await + } + + /// Update an API [Key] in Meilisearch. + /// + /// See also [`Client::create_key`], [`Client::delete_key`], [`Client::get_key`], and the [meilisearch documentation](https://www.meilisearch.com/docs/reference/api/keys#update-a-key). + /// + /// # Example + /// + /// ``` + /// # use meilisearch_sdk::{client::*, errors::Error, key::*}; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// let new_key = KeyBuilder::new(); + /// let mut new_key = client.create_key(new_key).await.unwrap(); + /// let mut key_update = KeyUpdater::new(new_key); + /// + /// let name = "my name".to_string(); + /// key_update.with_name(&name); + /// + /// let key = client.update_key(key_update).await.unwrap(); + /// + /// assert_eq!(key.name, Some(name)); + /// # client.delete_key(key).await.unwrap(); + /// # }); + /// ``` + pub async fn update_key(&self, key: impl AsRef) -> Result { + self.http_client + .request::<(), &KeyUpdater, Key>( + &format!("{}/keys/{}", self.host, key.as_ref().key), + Method::Patch { + body: key.as_ref(), + query: (), + }, + 200, + ) + .await + } + + /// Get version of the Meilisearch server. + /// + /// # Example + /// + /// ``` + /// # use meilisearch_sdk::client::*; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// let version = client.get_version().await.unwrap(); + /// # }); + /// ``` + pub async fn get_version(&self) -> Result { + self.http_client + .request::<(), (), Version>( + &format!("{}/version", self.host), + Method::Get { query: () }, + 200, + ) + .await + } + + /// Wait until Meilisearch processes a [Task], and get its status. + /// + /// `interval` = The frequency at which the server should be polled. **Default = 50ms** + /// + /// `timeout` = The maximum time to wait for processing to complete. **Default = 5000ms** + /// + /// If the waited time exceeds `timeout` then an [`Error::Timeout`] will be returned. + /// + /// See also [`Index::wait_for_task`, `Task::wait_for_completion`, `TaskInfo::wait_for_completion`]. + /// + /// # Example + /// + /// ``` + /// # use meilisearch_sdk::{client::*, indexes::*, tasks::*}; + /// # use serde::{Serialize, Deserialize}; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # + /// # #[derive(Debug, Serialize, Deserialize, PartialEq)] + /// # struct Document { + /// # id: usize, + /// # value: String, + /// # kind: String, + /// # } + /// # + /// # + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// let movies = client.index("movies_client_wait_for_task"); + /// + /// let task = movies.add_documents(&[ + /// Document { id: 0, kind: "title".into(), value: "The Social Network".to_string() }, + /// Document { id: 1, kind: "title".into(), value: "Harry Potter and the Sorcerer's Stone".to_string() }, + /// ], None).await.unwrap(); + /// + /// let status = client.wait_for_task(task, None, None).await.unwrap(); + /// + /// assert!(matches!(status, Task::Succeeded { .. })); + /// # movies.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// # }); + /// ``` + pub async fn wait_for_task( + &self, + task_id: impl AsRef, + interval: Option, + timeout: Option, + ) -> Result { + let interval = interval.unwrap_or_else(|| Duration::from_millis(50)); + let timeout = timeout.unwrap_or_else(|| Duration::from_millis(5000)); + + let mut elapsed_time = Duration::new(0, 0); + let mut task_result: Result; + + while timeout > elapsed_time { + task_result = self.get_task(&task_id).await; + match task_result { + Ok(status) => match status { + Task::Failed { .. } | Task::Succeeded { .. } => { + return self.get_task(task_id).await; + } + Task::Enqueued { .. } | Task::Processing { .. } => { + elapsed_time += interval; + async_sleep(interval).await; + } + }, + Err(error) => return Err(error), + }; + } + + Err(Error::Timeout) + } + + /// Get a task from the server given a task id. + /// + /// # Example + /// + /// ``` + /// # use meilisearch_sdk::{client::*, tasks::*}; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// # let index = client.create_index("movies_get_task", None).await.unwrap().wait_for_completion(&client, None, None).await.unwrap().try_make_index(&client).unwrap(); + /// let task = index.delete_all_documents().await.unwrap(); + /// + /// let task = client.get_task(task).await.unwrap(); + /// # index.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// # }); + /// ``` + pub async fn get_task(&self, task_id: impl AsRef) -> Result { + self.http_client + .request::<(), (), Task>( + &format!("{}/tasks/{}", self.host, task_id.as_ref()), + Method::Get { query: () }, + 200, + ) + .await + } + + /// Get all tasks with query parameters from the server. + /// + /// # Example + /// + /// ``` + /// # use meilisearch_sdk::{client::*, tasks::*}; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// let mut query = TasksSearchQuery::new(&client); + /// query.with_index_uids(["get_tasks_with"]); + /// + /// let tasks = client.get_tasks_with(&query).await.unwrap(); + /// # }); + /// ``` + pub async fn get_tasks_with( + &self, + tasks_query: &TasksSearchQuery<'_, Http>, + ) -> Result { + let tasks = self + .http_client + .request::<&TasksSearchQuery, (), TasksResults>( + &format!("{}/tasks", self.host), + Method::Get { query: tasks_query }, + 200, + ) + .await?; + + Ok(tasks) + } + + /// Cancel tasks with filters [`TasksCancelQuery`]. + /// + /// # Example + /// + /// ``` + /// # use meilisearch_sdk::{client::*, tasks::*}; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// let mut query = TasksCancelQuery::new(&client); + /// query.with_index_uids(["movies"]); + /// + /// let res = client.cancel_tasks_with(&query).await.unwrap(); + /// # }); + /// ``` + pub async fn cancel_tasks_with( + &self, + filters: &TasksCancelQuery<'_, Http>, + ) -> Result { + let tasks = self + .http_client + .request::<&TasksCancelQuery, (), TaskInfo>( + &format!("{}/tasks/cancel", self.host), + Method::Post { + query: filters, + body: (), + }, + 200, + ) + .await?; + + Ok(tasks) + } + + /// Delete tasks with filters [`TasksDeleteQuery`]. + /// + /// # Example + /// + /// ``` + /// # use meilisearch_sdk::{client::*, tasks::*}; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// let mut query = TasksDeleteQuery::new(&client); + /// query.with_index_uids(["movies"]); + /// + /// let res = client.delete_tasks_with(&query).await.unwrap(); + /// # }); + /// ``` + pub async fn delete_tasks_with( + &self, + filters: &TasksDeleteQuery<'_, Http>, + ) -> Result { + let tasks = self + .http_client + .request::<&TasksDeleteQuery, (), TaskInfo>( + &format!("{}/tasks", self.host), + Method::Delete { query: filters }, + 200, + ) + .await?; + + Ok(tasks) + } + + /// Get all tasks from the server. + /// + /// # Example + /// + /// ``` + /// # use meilisearch_sdk::{client::*, tasks::*}; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// let tasks = client.get_tasks().await.unwrap(); + /// + /// assert!(tasks.results.len() > 0); + /// # }); + /// ``` + pub async fn get_tasks(&self) -> Result { + let tasks = self + .http_client + .request::<(), (), TasksResults>( + &format!("{}/tasks", self.host), + Method::Get { query: () }, + 200, + ) + .await?; + + Ok(tasks) + } + + /// Generates a new tenant token. + /// + /// # Example + /// + /// ``` + /// # use meilisearch_sdk::client::Client; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// let api_key_uid = "76cf8b87-fd12-4688-ad34-260d930ca4f4".to_string(); + /// let token = client.generate_tenant_token(api_key_uid, serde_json::json!(["*"]), None, None).unwrap(); + /// + /// let client = Client::new(MEILISEARCH_URL, Some(token)).unwrap(); + /// # }); + /// ``` + #[cfg(not(target_arch = "wasm32"))] + pub fn generate_tenant_token( + &self, + api_key_uid: String, + search_rules: Value, + api_key: Option<&str>, + expires_at: Option, + ) -> Result { + let api_key = match self.get_api_key() { + Some(key) => api_key.unwrap_or(key), + None => { + return Err(Error::CantUseWithoutApiKey( + "generate_tenant_token".to_string(), + )) + } + }; + + crate::tenant_tokens::generate_tenant_token(api_key_uid, search_rules, api_key, expires_at) + } +} + +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ClientStats { + pub database_size: usize, + #[serde(with = "time::serde::rfc3339::option")] + pub last_update: Option, + pub indexes: HashMap, +} + +/// Health of the Meilisearch server. +/// +/// # Example +/// +/// ``` +/// # use meilisearch_sdk::{client::*, indexes::*, errors::Error}; +/// Health { +/// status: "available".to_string(), +/// }; +/// ``` +#[derive(Debug, Clone, Deserialize)] +pub struct Health { + pub status: String, +} + +/// Version of a Meilisearch server. +/// +/// # Example +/// +/// ``` +/// # use meilisearch_sdk::{client::*, indexes::*, errors::Error}; +/// Version { +/// commit_sha: "b46889b5f0f2f8b91438a08a358ba8f05fc09fc1".to_string(), +/// commit_date: "2019-11-15T09:51:54.278247+00:00".to_string(), +/// pkg_version: "0.1.1".to_string(), +/// }; +/// ``` +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Version { + pub commit_sha: String, + pub commit_date: String, + pub pkg_version: String, +} + +#[cfg(test)] +mod tests { + use big_s::S; + use time::OffsetDateTime; + + use meilisearch_test_macro::meilisearch_test; + + use crate::{client::*, key::Action, reqwest::qualified_version}; + + #[derive(Debug, Serialize, Deserialize, PartialEq)] + struct Document { + id: String, + } + + #[meilisearch_test] + async fn test_swapping_two_indexes(client: Client) { + let index_1 = client.index("test_swapping_two_indexes_1"); + let index_2 = client.index("test_swapping_two_indexes_2"); + + let t0 = index_1 + .add_documents( + &[Document { + id: "1".to_string(), + }], + None, + ) + .await + .unwrap(); + + index_2 + .add_documents( + &[Document { + id: "2".to_string(), + }], + None, + ) + .await + .unwrap(); + + t0.wait_for_completion(&client, None, None).await.unwrap(); + + let task = client + .swap_indexes([&SwapIndexes { + indexes: ( + "test_swapping_two_indexes_1".to_string(), + "test_swapping_two_indexes_2".to_string(), + ), + }]) + .await + .unwrap(); + task.wait_for_completion(&client, None, None).await.unwrap(); + + let document = index_1.get_document("2").await.unwrap(); + + assert_eq!( + Document { + id: "2".to_string() + }, + document + ); + } + + #[meilisearch_test] + async fn test_methods_has_qualified_version_as_header() { + let mut s = mockito::Server::new_async().await; + let mock_server_url = s.url(); + let path = "/hello"; + let address = &format!("{mock_server_url}{path}"); + let user_agent = &*qualified_version(); + let client = Client::new(mock_server_url, None::).unwrap(); + + let assertions = vec![ + ( + s.mock("GET", path) + .match_header("User-Agent", user_agent) + .create_async() + .await, + client + .http_client + .request::<(), (), ()>(address, Method::Get { query: () }, 200), + ), + ( + s.mock("POST", path) + .match_header("User-Agent", user_agent) + .create_async() + .await, + client.http_client.request::<(), (), ()>( + address, + Method::Post { + query: (), + body: {}, + }, + 200, + ), + ), + ( + s.mock("DELETE", path) + .match_header("User-Agent", user_agent) + .create_async() + .await, + client.http_client.request::<(), (), ()>( + address, + Method::Delete { query: () }, + 200, + ), + ), + ( + s.mock("PUT", path) + .match_header("User-Agent", user_agent) + .create_async() + .await, + client.http_client.request::<(), (), ()>( + address, + Method::Put { + query: (), + body: (), + }, + 200, + ), + ), + ( + s.mock("PATCH", path) + .match_header("User-Agent", user_agent) + .create_async() + .await, + client.http_client.request::<(), (), ()>( + address, + Method::Patch { + query: (), + body: (), + }, + 200, + ), + ), + ]; + + for (m, req) in assertions { + let _ = req.await; + + m.assert_async().await; + } + } + + #[meilisearch_test] + async fn test_get_tasks(client: Client) { + let tasks = client.get_tasks().await.unwrap(); + assert_eq!(tasks.limit, 20); + } + + #[meilisearch_test] + async fn test_get_tasks_with_params(client: Client) { + let query = TasksSearchQuery::new(&client); + let tasks = client.get_tasks_with(&query).await.unwrap(); + + assert_eq!(tasks.limit, 20); + } + + #[meilisearch_test] + async fn test_get_keys(client: Client) { + let keys = client.get_keys().await.unwrap(); + + assert!(keys.results.len() >= 2); + } + + #[meilisearch_test] + async fn test_delete_key(client: Client, name: String) { + let mut key = KeyBuilder::new(); + key.with_name(&name); + let key = client.create_key(key).await.unwrap(); + + client.delete_key(&key).await.unwrap(); + let keys = KeysQuery::new() + .with_limit(10000) + .execute(&client) + .await + .unwrap(); + + assert!(keys.results.iter().all(|k| k.key != key.key)); + } + + #[meilisearch_test] + async fn test_error_delete_key(client: Client, name: String) { + // ==> accessing a key that does not exist + let error = client.delete_key("invalid_key").await.unwrap_err(); + insta::assert_snapshot!(error, @"Meilisearch invalid_request: api_key_not_found: API key `invalid_key` not found.. https://docs.meilisearch.com/errors#api_key_not_found"); + + // ==> executing the action without enough right + let mut key = KeyBuilder::new(); + + key.with_name(&name); + let key = client.create_key(key).await.unwrap(); + let master_key = client.api_key.clone(); + + // create a new client with no right + let client = Client::new(client.host, Some(key.key.clone())).unwrap(); + // with a wrong key + let error = client.delete_key("invalid_key").await.unwrap_err(); + insta::assert_snapshot!(error, @"Meilisearch auth: invalid_api_key: The provided API key is invalid.. https://docs.meilisearch.com/errors#invalid_api_key"); + assert!(matches!( + error, + Error::Meilisearch(MeilisearchError { + error_code: ErrorCode::InvalidApiKey, + error_type: ErrorType::Auth, + .. + }) + )); + // with a good key + let error = client.delete_key(&key.key).await.unwrap_err(); + insta::assert_snapshot!(error, @"Meilisearch auth: invalid_api_key: The provided API key is invalid.. https://docs.meilisearch.com/errors#invalid_api_key"); + assert!(matches!( + error, + Error::Meilisearch(MeilisearchError { + error_code: ErrorCode::InvalidApiKey, + error_type: ErrorType::Auth, + .. + }) + )); + + // cleanup + let client = Client::new(client.host, master_key).unwrap(); + client.delete_key(key).await.unwrap(); + } + + #[meilisearch_test] + async fn test_create_key(client: Client, name: String) { + let expires_at = OffsetDateTime::now_utc() + time::Duration::HOUR; + let mut key = KeyBuilder::new(); + key.with_action(Action::DocumentsAdd) + .with_name(&name) + .with_expires_at(expires_at) + .with_description("a description") + .with_index("*"); + let key = client.create_key(key).await.unwrap(); + + assert_eq!(key.actions, vec![Action::DocumentsAdd]); + assert_eq!(&key.name, &Some(name)); + // We can't compare the two timestamps directly because of some nanoseconds imprecision with the floats + assert_eq!( + key.expires_at.unwrap().unix_timestamp(), + expires_at.unix_timestamp() + ); + assert_eq!(key.indexes, vec![S("*")]); + + client.delete_key(key).await.unwrap(); + } + + #[meilisearch_test] + async fn test_error_create_key(client: Client, name: String) { + // ==> Invalid index name + /* TODO: uncomment once meilisearch fix this bug: https://github.com/meilisearch/meilisearch/issues/2158 + let mut key = KeyBuilder::new(); + key.with_index("invalid index # / \\name with spaces"); + let error = client.create_key(key).await.unwrap_err(); + + assert!(matches!( + error, + Error::MeilisearchError { + error_code: ErrorCode::InvalidApiKeyIndexes, + error_type: ErrorType::InvalidRequest, + .. + } + )); + */ + // ==> executing the action without enough right + let mut no_right_key = KeyBuilder::new(); + no_right_key.with_name(format!("{name}_1")); + let no_right_key = client.create_key(no_right_key).await.unwrap(); + + // backup the master key for cleanup at the end of the test + let master_client = client.clone(); + let client = Client::new(&master_client.host, Some(no_right_key.key.clone())).unwrap(); + + let mut key = KeyBuilder::new(); + key.with_name(format!("{name}_2")); + let error = client.create_key(key).await.unwrap_err(); + + assert!(matches!( + error, + Error::Meilisearch(MeilisearchError { + error_code: ErrorCode::InvalidApiKey, + error_type: ErrorType::Auth, + .. + }) + )); + + // cleanup + master_client + .delete_key(client.api_key.unwrap()) + .await + .unwrap(); + } + + #[meilisearch_test] + async fn test_update_key(client: Client, description: String) { + let mut key = KeyBuilder::new(); + key.with_name("test_update_key"); + let mut key = client.create_key(key).await.unwrap(); + + let name = S("new name"); + key.with_description(&description); + key.with_name(&name); + + let key = key.update(&client).await.unwrap(); + + assert_eq!(key.description, Some(description)); + assert_eq!(key.name, Some(name)); + + client.delete_key(key).await.unwrap(); + } + + #[meilisearch_test] + async fn test_get_index(client: Client, index_uid: String) -> Result<(), Error> { + let task = client.create_index(&index_uid, None).await?; + let index = client + .wait_for_task(task, None, None) + .await? + .try_make_index(&client) + .unwrap(); + + assert_eq!(index.uid, index_uid); + index + .delete() + .await? + .wait_for_completion(&client, None, None) + .await?; + Ok(()) + } + + #[meilisearch_test] + async fn test_error_create_index(client: Client, index: Index) -> Result<(), Error> { + let error = client + .create_index("Wrong index name", None) + .await + .unwrap_err(); + + assert!(matches!( + error, + Error::Meilisearch(MeilisearchError { + error_code: ErrorCode::InvalidIndexUid, + error_type: ErrorType::InvalidRequest, + .. + }) + )); + + // we try to create an index with the same uid of an already existing index + let error = client + .create_index(&*index.uid, None) + .await? + .wait_for_completion(&client, None, None) + .await? + .unwrap_failure(); + + assert!(matches!( + error, + MeilisearchError { + error_code: ErrorCode::IndexAlreadyExists, + error_type: ErrorType::InvalidRequest, + .. + } + )); + Ok(()) + } + + #[meilisearch_test] + async fn test_list_all_indexes(client: Client) { + let all_indexes = client.list_all_indexes().await.unwrap(); + + assert_eq!(all_indexes.limit, 20); + assert_eq!(all_indexes.offset, 0); + } + + #[meilisearch_test] + async fn test_list_all_indexes_with_params(client: Client) { + let mut query = IndexesQuery::new(&client); + query.with_limit(1); + let all_indexes = client.list_all_indexes_with(&query).await.unwrap(); + + assert_eq!(all_indexes.limit, 1); + assert_eq!(all_indexes.offset, 0); + } + + #[meilisearch_test] + async fn test_list_all_indexes_raw(client: Client) { + let all_indexes_raw = client.list_all_indexes_raw().await.unwrap(); + + assert_eq!(all_indexes_raw["limit"], json!(20)); + assert_eq!(all_indexes_raw["offset"], json!(0)); + } + + #[meilisearch_test] + async fn test_list_all_indexes_raw_with_params(client: Client) { + let mut query = IndexesQuery::new(&client); + query.with_limit(1); + let all_indexes_raw = client.list_all_indexes_raw_with(&query).await.unwrap(); + + assert_eq!(all_indexes_raw["limit"], json!(1)); + assert_eq!(all_indexes_raw["offset"], json!(0)); + } + + #[meilisearch_test] + async fn test_get_primary_key_is_none(mut index: Index) { + let primary_key = index.get_primary_key().await; + + assert!(primary_key.is_ok()); + assert!(primary_key.unwrap().is_none()); + } + + #[meilisearch_test] + async fn test_get_primary_key(client: Client, index_uid: String) -> Result<(), Error> { + let mut index = client + .create_index(index_uid, Some("primary_key")) + .await? + .wait_for_completion(&client, None, None) + .await? + .try_make_index(&client) + .unwrap(); + + let primary_key = index.get_primary_key().await; + assert!(primary_key.is_ok()); + assert_eq!(primary_key?.unwrap(), "primary_key"); + + index + .delete() + .await? + .wait_for_completion(&client, None, None) + .await?; + + Ok(()) + } +} diff --git a/backend/vendor/meilisearch-sdk/src/documents.rs b/backend/vendor/meilisearch-sdk/src/documents.rs new file mode 100644 index 000000000..17e2e6102 --- /dev/null +++ b/backend/vendor/meilisearch-sdk/src/documents.rs @@ -0,0 +1,689 @@ +use async_trait::async_trait; +use serde::{de::DeserializeOwned, Deserialize, Serialize}; + +/// Derive the [`IndexConfig`](crate::documents::IndexConfig) trait. +/// +/// ## Field attribute +/// Use the `#[index_config(..)]` field attribute to generate the correct settings +/// for each field. The available parameters are: +/// - `primary_key` (can only be used once) +/// - `distinct` (can only be used once) +/// - `searchable` +/// - `displayed` +/// - `filterable` +/// - `sortable` +/// +/// ## Index name +/// The name of the index will be the name of the struct converted to snake case. +/// +/// ## Sample usage: +/// ``` +/// use serde::{Serialize, Deserialize}; +/// use meilisearch_sdk::documents::IndexConfig; +/// use meilisearch_sdk::settings::Settings; +/// use meilisearch_sdk::indexes::Index; +/// use meilisearch_sdk::client::Client; +/// +/// #[derive(Serialize, Deserialize, IndexConfig)] +/// struct Movie { +/// #[index_config(primary_key)] +/// movie_id: u64, +/// #[index_config(displayed, searchable)] +/// title: String, +/// #[index_config(displayed)] +/// description: String, +/// #[index_config(filterable, sortable, displayed)] +/// release_date: String, +/// #[index_config(filterable, displayed)] +/// genres: Vec, +/// } +/// +/// async fn usage(client: Client) { +/// // Default settings with the distinct, searchable, displayed, filterable, and sortable fields set correctly. +/// let settings: Settings = Movie::generate_settings(); +/// // Index created with the name `movie` and the primary key set to `movie_id` +/// let index: Index = Movie::generate_index(&client).await.unwrap(); +/// } +/// ``` +pub use meilisearch_index_setting_macro::IndexConfig; + +use crate::client::Client; +use crate::request::HttpClient; +use crate::settings::Settings; +use crate::task_info::TaskInfo; +use crate::tasks::Task; +use crate::{errors::Error, indexes::Index}; + +#[async_trait(?Send)] +pub trait IndexConfig { + const INDEX_STR: &'static str; + + #[must_use] + fn index(client: &Client) -> Index { + client.index(Self::INDEX_STR) + } + fn generate_settings() -> Settings; + async fn generate_index(client: &Client) -> Result, Task>; +} + +#[derive(Debug, Clone, Deserialize)] +pub struct DocumentsResults { + pub results: Vec, + pub limit: u32, + pub offset: u32, + pub total: u32, +} + +#[derive(Debug, Clone, Serialize)] +pub struct DocumentQuery<'a, Http: HttpClient> { + #[serde(skip_serializing)] + pub index: &'a Index, + + /// The fields that should appear in the documents. By default, all of the fields are present. + #[serde(skip_serializing_if = "Option::is_none")] + pub fields: Option>, +} + +impl<'a, Http: HttpClient> DocumentQuery<'a, Http> { + #[must_use] + pub fn new(index: &Index) -> DocumentQuery { + DocumentQuery { + index, + fields: None, + } + } + + /// Specify the fields to return in the document. + /// + /// # Example + /// + /// ``` + /// # use meilisearch_sdk::{client::*, indexes::*, documents::*}; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// let index = client.index("document_query_with_fields"); + /// let mut document_query = DocumentQuery::new(&index); + /// + /// document_query.with_fields(["title"]); + /// ``` + pub fn with_fields( + &mut self, + fields: impl IntoIterator, + ) -> &mut DocumentQuery<'a, Http> { + self.fields = Some(fields.into_iter().collect()); + self + } + + /// Execute the get document query. + /// + /// # Example + /// + /// ``` + /// # use meilisearch_sdk::{client::*, indexes::*, documents::*}; + /// # use serde::{Deserialize, Serialize}; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// #[derive(Debug, Serialize, Deserialize, PartialEq)] + /// struct MyObject { + /// id: String, + /// kind: String, + /// } + /// + /// #[derive(Debug, Serialize, Deserialize, PartialEq)] + /// struct MyObjectReduced { + /// id: String, + /// } + /// # let index = client.index("document_query_execute"); + /// # 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(); + /// + /// let document = DocumentQuery::new(&index).with_fields(["id"]) + /// .execute::("1") + /// .await + /// .unwrap(); + /// + /// assert_eq!( + /// document, + /// MyObjectReduced { id: "1".to_string() } + /// ); + /// # index.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// # }); + pub async fn execute( + &self, + document_id: &str, + ) -> Result { + self.index.get_document_with::(document_id, self).await + } +} + +#[derive(Debug, Clone, Serialize)] +pub struct DocumentsQuery<'a, Http: HttpClient> { + #[serde(skip_serializing)] + pub index: &'a Index, + + /// The number of documents to skip. + /// + /// If the value of the parameter `offset` is `n`, the `n` first documents will not be returned. + /// This is helpful for pagination. + /// + /// Example: If you want to skip the first document, set offset to `1`. + #[serde(skip_serializing_if = "Option::is_none")] + pub offset: Option, + + /// The maximum number of documents returned. + /// If the value of the parameter `limit` is `n`, there will never be more than `n` documents in the response. + /// This is helpful for pagination. + /// + /// Example: If you don't want to get more than two documents, set limit to `2`. + /// + /// **Default: `20`** + #[serde(skip_serializing_if = "Option::is_none")] + pub limit: Option, + + /// The fields that should appear in the documents. By default, all of the fields are present. + #[serde(skip_serializing_if = "Option::is_none")] + pub fields: Option>, + + /// Filters to apply. + /// + /// Available since v1.2 of Meilisearch + /// Read the [dedicated guide](https://www.meilisearch.com/docs/learn/fine_tuning_results/filtering#filter-basics) to learn the syntax. + #[serde(skip_serializing_if = "Option::is_none")] + pub filter: Option<&'a str>, +} + +impl<'a, Http: HttpClient> DocumentsQuery<'a, Http> { + #[must_use] + pub fn new(index: &Index) -> DocumentsQuery { + DocumentsQuery { + index, + offset: None, + limit: None, + fields: None, + filter: None, + } + } + + /// Specify the offset. + /// + /// # Example + /// + /// ``` + /// # use meilisearch_sdk::{client::*, indexes::*, documents::*}; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// let index = client.index("my_index"); + /// + /// let mut documents_query = DocumentsQuery::new(&index).with_offset(1); + /// ``` + pub fn with_offset(&mut self, offset: usize) -> &mut DocumentsQuery<'a, Http> { + self.offset = Some(offset); + self + } + + /// Specify the limit. + /// + /// # Example + /// + /// ``` + /// # use meilisearch_sdk::{client::*, indexes::*, documents::*}; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// let index = client.index("my_index"); + /// + /// let mut documents_query = DocumentsQuery::new(&index); + /// + /// documents_query.with_limit(1); + /// ``` + pub fn with_limit(&mut self, limit: usize) -> &mut DocumentsQuery<'a, Http> { + self.limit = Some(limit); + self + } + + /// Specify the fields to return in the documents. + /// + /// # Example + /// + /// ``` + /// # use meilisearch_sdk::{client::*, indexes::*, documents::*}; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// let index = client.index("my_index"); + /// + /// let mut documents_query = DocumentsQuery::new(&index); + /// + /// documents_query.with_fields(["title"]); + /// ``` + pub fn with_fields( + &mut self, + fields: impl IntoIterator, + ) -> &mut DocumentsQuery<'a, Http> { + self.fields = Some(fields.into_iter().collect()); + self + } + + pub fn with_filter<'b>(&'b mut self, filter: &'a str) -> &'b mut DocumentsQuery<'a, Http> { + self.filter = Some(filter); + self + } + + /// Execute the get documents query. + /// + /// # Example + /// + /// ``` + /// # use meilisearch_sdk::{client::*, indexes::*, documents::*}; + /// # use serde::{Deserialize, Serialize}; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// # let index = client.create_index("documents_query_execute", None).await.unwrap().wait_for_completion(&client, None, None).await.unwrap().try_make_index(&client).unwrap(); + /// #[derive(Debug, Serialize, Deserialize, PartialEq)] + /// struct MyObject { + /// id: Option, + /// kind: String, + /// } + /// let index = client.index("documents_query_execute"); + /// + /// let document = DocumentsQuery::new(&index) + /// .with_offset(1) + /// .execute::() + /// .await + /// .unwrap(); + /// + /// # index.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// # }); + /// ``` + pub async fn execute( + &self, + ) -> Result, Error> { + self.index.get_documents_with::(self).await + } +} + +#[derive(Debug, Clone, Serialize)] +pub struct DocumentDeletionQuery<'a, Http: HttpClient> { + #[serde(skip_serializing)] + pub index: &'a Index, + + /// Filters to apply. + /// + /// Read the [dedicated guide](https://www.meilisearch.com/docs/learn/fine_tuning_results/filtering#filter-basics) to learn the syntax. + pub filter: Option<&'a str>, +} + +impl<'a, Http: HttpClient> DocumentDeletionQuery<'a, Http> { + #[must_use] + pub fn new(index: &Index) -> DocumentDeletionQuery { + DocumentDeletionQuery { + index, + filter: None, + } + } + + pub fn with_filter<'b>( + &'b mut self, + filter: &'a str, + ) -> &'b mut DocumentDeletionQuery<'a, Http> { + self.filter = Some(filter); + self + } + + pub async fn execute(&self) -> Result { + self.index.delete_documents_with(self).await + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{client::Client, errors::*, indexes::*}; + use meilisearch_test_macro::meilisearch_test; + use serde::{Deserialize, Serialize}; + + #[derive(Debug, Serialize, Deserialize, PartialEq)] + struct MyObject { + id: Option, + kind: String, + } + + #[allow(unused)] + #[derive(IndexConfig)] + struct MovieClips { + #[index_config(primary_key)] + movie_id: u64, + #[index_config(distinct)] + owner: String, + #[index_config(displayed, searchable)] + title: String, + #[index_config(displayed)] + description: String, + #[index_config(filterable, sortable, displayed)] + release_date: String, + #[index_config(filterable, displayed)] + genres: Vec, + } + + #[allow(unused)] + #[derive(IndexConfig)] + struct VideoClips { + video_id: u64, + } + + async fn setup_test_index(client: &Client, index: &Index) -> Result<(), Error> { + let t0 = index + .add_documents( + &[ + MyObject { + id: Some(0), + kind: "text".into(), + }, + MyObject { + id: Some(1), + kind: "text".into(), + }, + MyObject { + id: Some(2), + kind: "title".into(), + }, + MyObject { + id: Some(3), + kind: "title".into(), + }, + ], + None, + ) + .await?; + + t0.wait_for_completion(client, None, None).await?; + + Ok(()) + } + + #[meilisearch_test] + async fn test_get_documents_with_execute(client: Client, index: Index) -> Result<(), Error> { + setup_test_index(&client, &index).await?; + let documents = DocumentsQuery::new(&index) + .with_limit(1) + .with_offset(1) + .with_fields(["kind"]) + .execute::() + .await + .unwrap(); + + assert_eq!(documents.limit, 1); + assert_eq!(documents.offset, 1); + assert_eq!(documents.results.len(), 1); + + Ok(()) + } + + #[meilisearch_test] + async fn test_delete_documents_with(client: Client, index: Index) -> Result<(), Error> { + setup_test_index(&client, &index).await?; + index + .set_filterable_attributes(["id"]) + .await? + .wait_for_completion(&client, None, None) + .await?; + + let mut query = DocumentDeletionQuery::new(&index); + query.with_filter("id = 1"); + index + .delete_documents_with(&query) + .await? + .wait_for_completion(&client, None, None) + .await?; + let document_result = index.get_document::("1").await; + + match document_result { + Ok(_) => panic!("The test was expecting no documents to be returned but got one."), + Err(e) => match e { + Error::Meilisearch(err) => { + assert_eq!(err.error_code, ErrorCode::DocumentNotFound); + } + _ => panic!("The error was expected to be a Meilisearch error, but it was not."), + }, + } + + Ok(()) + } + + #[meilisearch_test] + async fn test_delete_documents_with_filter_not_filterable( + client: Client, + index: Index, + ) -> Result<(), Error> { + setup_test_index(&client, &index).await?; + + let mut query = DocumentDeletionQuery::new(&index); + query.with_filter("id = 1"); + let error = index + .delete_documents_with(&query) + .await? + .wait_for_completion(&client, None, None) + .await?; + + let error = error.unwrap_failure(); + + assert!(matches!( + error, + MeilisearchError { + error_code: ErrorCode::InvalidDocumentFilter, + error_type: ErrorType::InvalidRequest, + .. + } + )); + + Ok(()) + } + + #[meilisearch_test] + async fn test_get_documents_with_only_one_param( + client: Client, + index: Index, + ) -> Result<(), Error> { + setup_test_index(&client, &index).await?; + // let documents = index.get_documents(None, None, None).await.unwrap(); + let documents = DocumentsQuery::new(&index) + .with_limit(1) + .execute::() + .await + .unwrap(); + + assert_eq!(documents.limit, 1); + assert_eq!(documents.offset, 0); + assert_eq!(documents.results.len(), 1); + + Ok(()) + } + + #[meilisearch_test] + async fn test_get_documents_with_filter(client: Client, index: Index) -> Result<(), Error> { + setup_test_index(&client, &index).await?; + + index + .set_filterable_attributes(["id"]) + .await + .unwrap() + .wait_for_completion(&client, None, None) + .await + .unwrap(); + + let documents = DocumentsQuery::new(&index) + .with_filter("id = 1") + .execute::() + .await?; + + assert_eq!(documents.results.len(), 1); + + Ok(()) + } + + #[meilisearch_test] + async fn test_get_documents_with_error_hint() -> Result<(), Error> { + let meilisearch_url = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + let client = Client::new(format!("{meilisearch_url}/hello"), Some("masterKey")).unwrap(); + let index = client.index("test_get_documents_with_filter_wrong_ms_version"); + + let documents = DocumentsQuery::new(&index) + .with_filter("id = 1") + .execute::() + .await; + + let error = documents.unwrap_err(); + + 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()); + let url = format!( + "{meilisearch_url}/hello/indexes/test_get_documents_with_filter_wrong_ms_version/documents/fetch" + ); + let status_code = 404; + 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"); + + match &error { + Error::MeilisearchCommunication(error) => { + assert_eq!(error.status_code, status_code); + assert_eq!(error.message, message); + assert_eq!(error.url, url); + } + _ => panic!("The error was expected to be a MeilisearchCommunicationError error, but it was not."), + }; + assert_eq!(format!("{error}"), displayed_error); + + Ok(()) + } + + #[meilisearch_test] + async fn test_get_documents_with_error_hint_meilisearch_api_error( + index: Index, + client: Client, + ) -> Result<(), Error> { + setup_test_index(&client, &index).await?; + + let error = DocumentsQuery::new(&index) + .with_filter("id = 1") + .execute::() + .await + .unwrap_err(); + + let message = "Attribute `id` is not filterable. This index does not have configured filterable attributes. +1:3 id = 1 +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(); + let displayed_error = "Meilisearch invalid_request: invalid_document_filter: Attribute `id` is not filterable. This index does not have configured filterable attributes. +1:3 id = 1 +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"; + + match &error { + Error::Meilisearch(error) => { + assert_eq!(error.error_message, message); + } + _ => panic!("The error was expected to be a MeilisearchCommunicationError error, but it was not."), + }; + assert_eq!(format!("{error}"), displayed_error); + + Ok(()) + } + + #[meilisearch_test] + async fn test_get_documents_with_invalid_filter( + client: Client, + index: Index, + ) -> Result<(), Error> { + setup_test_index(&client, &index).await?; + + // Does not work because `id` is not filterable + let error = DocumentsQuery::new(&index) + .with_filter("id = 1") + .execute::() + .await + .unwrap_err(); + + assert!(matches!( + error, + Error::Meilisearch(MeilisearchError { + error_code: ErrorCode::InvalidDocumentFilter, + error_type: ErrorType::InvalidRequest, + .. + }) + )); + + Ok(()) + } + + #[meilisearch_test] + async fn test_settings_generated_by_macro(client: Client, index: Index) -> Result<(), Error> { + setup_test_index(&client, &index).await?; + + let movie_settings: Settings = MovieClips::generate_settings(); + let video_settings: Settings = VideoClips::generate_settings(); + + assert_eq!(movie_settings.searchable_attributes.unwrap(), ["title"]); + assert!(video_settings.searchable_attributes.unwrap().is_empty()); + + assert_eq!( + movie_settings.displayed_attributes.unwrap(), + ["title", "description", "release_date", "genres"] + ); + assert!(video_settings.displayed_attributes.unwrap().is_empty()); + + assert_eq!( + movie_settings.filterable_attributes.unwrap(), + ["release_date", "genres"] + ); + assert!(video_settings.filterable_attributes.unwrap().is_empty()); + + assert_eq!( + movie_settings.sortable_attributes.unwrap(), + ["release_date"] + ); + assert!(video_settings.sortable_attributes.unwrap().is_empty()); + + Ok(()) + } + + #[meilisearch_test] + async fn test_generate_index(client: Client) -> Result<(), Error> { + let index: Index = MovieClips::generate_index(&client).await.unwrap(); + + assert_eq!(index.uid, "movie_clips"); + + index + .delete() + .await? + .wait_for_completion(&client, None, None) + .await?; + + Ok(()) + } + #[derive(Serialize, Deserialize, IndexConfig)] + struct Movie { + #[index_config(primary_key)] + movie_id: u64, + #[index_config(displayed, searchable)] + title: String, + #[index_config(displayed)] + description: String, + #[index_config(filterable, sortable, displayed)] + release_date: String, + #[index_config(filterable, displayed)] + genres: Vec, + } +} diff --git a/backend/vendor/meilisearch-sdk/src/dumps.rs b/backend/vendor/meilisearch-sdk/src/dumps.rs new file mode 100644 index 000000000..e9b093731 --- /dev/null +++ b/backend/vendor/meilisearch-sdk/src/dumps.rs @@ -0,0 +1,130 @@ +//! The `dumps` module allows the creation of database dumps. +//! +//! - Dumps are `.dump` files that can be used to launch Meilisearch. +//! +//! - Dumps are compatible between Meilisearch versions. +//! +//! - Creating a dump is also referred to as exporting it, whereas launching Meilisearch with a dump is referred to as importing it. +//! +//! - During a [dump export](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. +//! +//! - During a dump import, all indexes contained in the indicated `.dump` file are imported along with their associated documents and [settings](crate::settings::Settings). +//! Any existing [index](crate::indexes::Index) with the same uid as an index in the dump file will be overwritten. +//! +//! - Dump imports are [performed at launch](https://www.meilisearch.com/docs/learn/configuration/instance_options#import-dump) using an option. +//! +//! # Example +//! +//! ``` +//! # use meilisearch_sdk::{client::*, errors::*, dumps::*, dumps::*, task_info::*, tasks::*}; +//! # use futures_await_test::async_test; +//! # use std::{thread::sleep, time::Duration}; +//! # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { +//! # +//! # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); +//! # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); +//! # +//! # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); +//! +//! // Create a dump +//! let task_info = client.create_dump().await.unwrap(); +//! assert!(matches!( +//! task_info, +//! TaskInfo { +//! update_type: TaskType::DumpCreation { .. }, +//! .. +//! } +//! )); +//! # }); +//! ``` + +use crate::{client::Client, errors::Error, request::*, task_info::TaskInfo}; + +/// Dump related methods. +/// See the [dumps](crate::dumps) module. +impl Client { + /// Triggers a dump creation process. + /// + /// Once the process is complete, a dump is created in the [dumps directory](https://www.meilisearch.com/docs/learn/configuration/instance_options#dump-directory). + /// If the dumps directory does not exist yet, it will be created. + /// + /// # Example + /// + /// ``` + /// # use meilisearch_sdk::{client::*, errors::*, dumps::*, dumps::*, task_info::*, tasks::*}; + /// # use futures_await_test::async_test; + /// # use std::{thread::sleep, time::Duration}; + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// # + /// let task_info = client.create_dump().await.unwrap(); + /// + /// assert!(matches!( + /// task_info, + /// TaskInfo { + /// update_type: TaskType::DumpCreation { .. }, + /// .. + /// } + /// )); + /// # }); + /// ``` + pub async fn create_dump(&self) -> Result { + self.http_client + .request::<(), (), TaskInfo>( + &format!("{}/dumps", self.host), + Method::Post { + query: (), + body: (), + }, + 202, + ) + .await + } +} + +/// Alias for [`create_dump`](Client::create_dump). +pub async fn create_dump(client: &Client) -> Result { + client.create_dump().await +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{client::*, tasks::*}; + use meilisearch_test_macro::meilisearch_test; + use std::time::Duration; + + #[meilisearch_test] + async fn test_dumps_success_creation(client: Client) -> Result<(), Error> { + let task = client + .create_dump() + .await? + .wait_for_completion( + &client, + Some(Duration::from_millis(1)), + Some(Duration::from_millis(6000)), + ) + .await?; + + assert!(matches!(task, Task::Succeeded { .. })); + Ok(()) + } + + #[meilisearch_test] + async fn test_dumps_correct_update_type(client: Client) -> Result<(), Error> { + let task_info = client.create_dump().await.unwrap(); + + assert!(matches!( + task_info, + TaskInfo { + update_type: TaskType::DumpCreation { .. }, + .. + } + )); + Ok(()) + } +} diff --git a/backend/vendor/meilisearch-sdk/src/errors.rs b/backend/vendor/meilisearch-sdk/src/errors.rs new file mode 100644 index 000000000..44b853861 --- /dev/null +++ b/backend/vendor/meilisearch-sdk/src/errors.rs @@ -0,0 +1,421 @@ +use serde::{Deserialize, Serialize}; +use thiserror::Error; + +/// An enum representing the errors that can occur. + +#[derive(Debug, Error)] +#[non_exhaustive] +pub enum Error { + /// The exhaustive list of Meilisearch errors: + /// + /// Also check out: + #[error(transparent)] + Meilisearch(#[from] MeilisearchError), + + #[error(transparent)] + MeilisearchCommunication(#[from] MeilisearchCommunicationError), + /// The Meilisearch server returned an invalid JSON for a request. + #[error("Error parsing response JSON: {}", .0)] + ParseError(#[from] serde_json::Error), + + /// A timeout happened while waiting for an update to complete. + #[error("A task did not succeed in time.")] + Timeout, + /// This Meilisearch SDK generated an invalid request (which was not sent). + /// + /// It probably comes from an invalid API key resulting in an invalid HTTP header. + #[error("Unable to generate a valid HTTP request. It probably comes from an invalid API key.")] + InvalidRequest, + + /// Can't call this method without setting an api key in the client. + #[error("You need to provide an api key to use the `{0}` method.")] + CantUseWithoutApiKey(String), + /// It is not possible to generate a tenant token with an invalid api key. + /// + /// Empty strings or with less than 8 characters are considered invalid. + #[error("The provided api_key is invalid.")] + TenantTokensInvalidApiKey, + /// It is not possible to generate an already expired tenant token. + #[error("The provided expires_at is already expired.")] + TenantTokensExpiredSignature, + + /// When jsonwebtoken cannot generate the token successfully. + #[cfg(not(target_arch = "wasm32"))] + #[error("Impossible to generate the token, jsonwebtoken encountered an error: {}", .0)] + InvalidTenantToken(#[from] jsonwebtoken::errors::Error), + + /// The http client encountered an error. + #[cfg(feature = "reqwest")] + #[error("HTTP request failed: {}", .0)] + HttpError(#[from] reqwest::Error), + + // The library formatting the query parameters encountered an error. + #[error("Internal Error: could not parse the query parameters: {}", .0)] + Yaup(#[from] yaup::Error), + + // The library validating the format of an uuid. + #[cfg(not(target_arch = "wasm32"))] + #[error("The uid of the token has bit an uuid4 format: {}", .0)] + Uuid(#[from] uuid::Error), + + // Error thrown in case the version of the Uuid is not v4. + #[error("The uid provided to the token is not of version uuidv4")] + InvalidUuid4Version, + + #[error(transparent)] + Other(Box), +} + +#[derive(Debug, Clone, Deserialize, Error)] +#[serde(rename_all = "camelCase")] +pub struct MeilisearchCommunicationError { + pub status_code: u16, + pub message: Option, + pub url: String, +} + +impl std::fmt::Display for MeilisearchCommunicationError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "MeilisearchCommunicationError: The server responded with a {}.", + self.status_code + )?; + if let Some(message) = &self.message { + write!(f, " {message}")?; + } + write!(f, "\nurl: {}", self.url)?; + Ok(()) + } +} + +#[derive(Debug, Clone, Deserialize, Error)] +#[serde(rename_all = "camelCase")] +#[error("Meilisearch {}: {}: {}. {}", .error_type, .error_code, .error_message, .error_link)] +pub struct MeilisearchError { + /// The human readable error message + #[serde(rename = "message")] + pub error_message: String, + /// The error code of the error. Officially documented at + /// . + #[serde(rename = "code")] + pub error_code: ErrorCode, + /// The type of error (invalid request, internal error, or authentication error) + #[serde(rename = "type")] + pub error_type: ErrorType, + /// A link to the Meilisearch documentation for an error. + #[serde(rename = "link")] + pub error_link: String, +} + +/// The type of error that was encountered. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +#[non_exhaustive] +pub enum ErrorType { + /// The submitted request was invalid. + InvalidRequest, + /// The Meilisearch instance encountered an internal error. + Internal, + /// Authentication was either incorrect or missing. + Auth, + + /// That's unexpected. Please open a GitHub issue after ensuring you are + /// using the supported version of the Meilisearch server. + #[serde(other)] + Unknown, +} + +impl std::fmt::Display for ErrorType { + fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { + write!( + fmt, + "{}", + // this can't fail + serde_json::to_value(self).unwrap().as_str().unwrap() + ) + } +} + +/// The error code. +/// +/// Officially documented at . +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +#[non_exhaustive] +pub enum ErrorCode { + IndexCreationFailed, + IndexAlreadyExists, + IndexNotFound, + InvalidIndexUid, + InvalidState, + PrimaryKeyInferenceFailed, + IndexPrimaryKeyAlreadyPresent, + InvalidStoreFile, + MaxFieldsLimitExceeded, + MissingDocumentId, + InvalidDocumentId, + BadParameter, + BadRequest, + DatabaseSizeLimitReached, + DocumentNotFound, + InternalError, + InvalidApiKey, + MissingAuthorizationHeader, + TaskNotFound, + DumpNotFound, + MissingMasterKey, + NoSpaceLeftOnDevice, + PayloadTooLarge, + UnretrievableDocument, + SearchError, + UnsupportedMediaType, + DumpAlreadyProcessing, + DumpProcessFailed, + MissingContentType, + MalformedPayload, + InvalidContentType, + MissingPayload, + InvalidApiKeyDescription, + InvalidApiKeyActions, + InvalidApiKeyIndexes, + InvalidApiKeyExpiresAt, + ApiKeyNotFound, + MissingTaskFilters, + MissingIndexUid, + InvalidIndexOffset, + InvalidIndexLimit, + InvalidIndexPrimaryKey, + InvalidDocumentFilter, + MissingDocumentFilter, + InvalidDocumentFields, + InvalidDocumentLimit, + InvalidDocumentOffset, + InvalidDocumentGeoField, + InvalidSearchQ, + InvalidSearchOffset, + InvalidSearchLimit, + InvalidSearchPage, + InvalidSearchHitsPerPage, + InvalidSearchAttributesToRetrieve, + InvalidSearchAttributesToCrop, + InvalidSearchCropLength, + InvalidSearchAttributesToHighlight, + InvalidSearchShowMatchesPosition, + InvalidSearchFilter, + InvalidSearchSort, + InvalidSearchFacets, + InvalidSearchHighlightPreTag, + InvalidSearchHighlightPostTag, + InvalidSearchCropMarker, + InvalidSearchMatchingStrategy, + ImmutableApiKeyUid, + ImmutableApiKeyActions, + ImmutableApiKeyIndexes, + ImmutableExpiresAt, + ImmutableCreatedAt, + ImmutableUpdatedAt, + InvalidSwapDuplicateIndexFound, + InvalidSwapIndexes, + MissingSwapIndexes, + InvalidTaskTypes, + InvalidTaskUids, + InvalidTaskStatuses, + InvalidTaskLimit, + InvalidTaskFrom, + InvalidTaskCanceledBy, + InvalidTaskFilters, + TooManyOpenFiles, + IoError, + InvalidTaskIndexUids, + ImmutableIndexUid, + ImmutableIndexCreatedAt, + ImmutableIndexUpdatedAt, + InvalidSettingsDisplayedAttributes, + InvalidSettingsSearchableAttributes, + InvalidSettingsFilterableAttributes, + InvalidSettingsSortableAttributes, + InvalidSettingsRankingRules, + InvalidSettingsStopWords, + InvalidSettingsSynonyms, + InvalidSettingsDistinctAttributes, + InvalidSettingsTypoTolerance, + InvalidSettingsFaceting, + InvalidSettingsDictionary, + InvalidSettingsPagination, + InvalidTaskBeforeEnqueuedAt, + InvalidTaskAfterEnqueuedAt, + InvalidTaskBeforeStartedAt, + InvalidTaskAfterStartedAt, + InvalidTaskBeforeFinishedAt, + InvalidTaskAfterFinishedAt, + MissingApiKeyActions, + MissingApiKeyIndexes, + MissingApiKeyExpiresAt, + InvalidApiKeyLimit, + InvalidApiKeyOffset, + + /// That's unexpected. Please open a GitHub issue after ensuring you are + /// using the supported version of the Meilisearch server. + #[serde(other)] + Unknown, +} + +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"; + +impl std::fmt::Display for ErrorCode { + fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { + write!( + fmt, + "{}", + // this can't fail + serde_json::to_value(self).unwrap().as_str().unwrap() + ) + } +} + +#[cfg(test)] +mod test { + use super::*; + + use jsonwebtoken::errors::ErrorKind::InvalidToken; + use meilisearch_test_macro::meilisearch_test; + use uuid::Uuid; + + #[meilisearch_test] + async fn test_meilisearch_error() { + let error: MeilisearchError = serde_json::from_str( + r#" +{ + "message": "The cool error message.", + "code": "index_creation_failed", + "type": "internal", + "link": "https://the best link ever" +}"#, + ) + .unwrap(); + + assert_eq!(error.error_message, "The cool error message."); + assert_eq!(error.error_code, ErrorCode::IndexCreationFailed); + assert_eq!(error.error_type, ErrorType::Internal); + assert_eq!(error.error_link, "https://the best link ever"); + + let error: MeilisearchError = serde_json::from_str( + r#" +{ + "message": "", + "code": "An unknown error", + "type": "An unknown type", + "link": "" +}"#, + ) + .unwrap(); + + assert_eq!(error.error_code, ErrorCode::Unknown); + assert_eq!(error.error_type, ErrorType::Unknown); + } + + #[meilisearch_test] + async fn test_error_message_parsing() { + let error: MeilisearchError = serde_json::from_str( + r#" +{ + "message": "The cool error message.", + "code": "index_creation_failed", + "type": "internal", + "link": "https://the best link ever" +}"#, + ) + .unwrap(); + + assert_eq!(error.to_string(), "Meilisearch internal: index_creation_failed: The cool error message.. https://the best link ever"); + + let error: MeilisearchCommunicationError = MeilisearchCommunicationError { + status_code: 404, + message: Some("Hint: something.".to_string()), + url: "http://localhost:7700/something".to_string(), + }; + + assert_eq!( + error.to_string(), + "MeilisearchCommunicationError: The server responded with a 404. Hint: something.\nurl: http://localhost:7700/something" + ); + + let error: MeilisearchCommunicationError = MeilisearchCommunicationError { + status_code: 404, + message: None, + url: "http://localhost:7700/something".to_string(), + }; + + assert_eq!( + error.to_string(), + "MeilisearchCommunicationError: The server responded with a 404.\nurl: http://localhost:7700/something" + ); + + let error = Error::Timeout; + assert_eq!(error.to_string(), "A task did not succeed in time."); + + let error = Error::InvalidRequest; + assert_eq!( + error.to_string(), + "Unable to generate a valid HTTP request. It probably comes from an invalid API key." + ); + + let error = Error::TenantTokensInvalidApiKey; + assert_eq!(error.to_string(), "The provided api_key is invalid."); + + let error = Error::TenantTokensExpiredSignature; + assert_eq!( + error.to_string(), + "The provided expires_at is already expired." + ); + + let error = Error::InvalidUuid4Version; + assert_eq!( + error.to_string(), + "The uid provided to the token is not of version uuidv4" + ); + + let error = Error::Uuid(Uuid::parse_str("67e55044").unwrap_err()); + 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"); + + let data = r#" + { + "name": "John Doe" + "age": 43, + }"#; + + let error = Error::ParseError(serde_json::from_str::(data).unwrap_err()); + assert_eq!( + error.to_string(), + "Error parsing response JSON: invalid type: map, expected a string at line 2 column 8" + ); + + let error = Error::HttpError( + reqwest::Client::new() + .execute(reqwest::Request::new( + reqwest::Method::POST, + // there will never be a `meilisearch.gouv.fr` addr since these domain name are controlled by the state of france + reqwest::Url::parse("https://meilisearch.gouv.fr").unwrap(), + )) + .await + .unwrap_err(), + ); + assert_eq!( + error.to_string(), + "HTTP request failed: error sending request for url (https://meilisearch.gouv.fr/)" + ); + + let error = Error::InvalidTenantToken(jsonwebtoken::errors::Error::from(InvalidToken)); + assert_eq!( + error.to_string(), + "Impossible to generate the token, jsonwebtoken encountered an error: InvalidToken" + ); + + let error = Error::Yaup(yaup::Error::Custom("Test yaup error".to_string())); + assert_eq!( + error.to_string(), + "Internal Error: could not parse the query parameters: Test yaup error" + ); + } +} diff --git a/backend/vendor/meilisearch-sdk/src/features.rs b/backend/vendor/meilisearch-sdk/src/features.rs new file mode 100644 index 000000000..3001beb48 --- /dev/null +++ b/backend/vendor/meilisearch-sdk/src/features.rs @@ -0,0 +1,132 @@ +use crate::{ + client::Client, + errors::Error, + request::{HttpClient, Method}, +}; +use serde::{Deserialize, Serialize}; + +/// Struct representing the experimental features result from the API. +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ExperimentalFeaturesResult { + pub vector_store: bool, +} + +/// Struct representing the experimental features request. +/// +/// You can build this struct using the builder pattern. +/// +/// # Example +/// +/// ``` +/// # use meilisearch_sdk::{client::Client, features::ExperimentalFeatures}; +/// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); +/// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); +/// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); +/// let mut features = ExperimentalFeatures::new(&client); +/// features.set_vector_store(true); +/// ``` +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ExperimentalFeatures<'a, Http: HttpClient> { + #[serde(skip_serializing)] + client: &'a Client, + #[serde(skip_serializing_if = "Option::is_none")] + pub vector_store: Option, +} + +impl<'a, Http: HttpClient> ExperimentalFeatures<'a, Http> { + #[must_use] + pub fn new(client: &'a Client) -> Self { + ExperimentalFeatures { + client, + vector_store: None, + } + } + + pub fn set_vector_store(&mut self, vector_store: bool) -> &mut Self { + self.vector_store = Some(vector_store); + self + } + + /// Get all the experimental features + /// + /// # Example + /// + /// ``` + /// # use meilisearch_sdk::{client::Client, features::ExperimentalFeatures}; + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// let features = ExperimentalFeatures::new(&client); + /// features.get().await.unwrap(); + /// }); + /// ``` + pub async fn get(&self) -> Result { + self.client + .http_client + .request::<(), (), ExperimentalFeaturesResult>( + &format!("{}/experimental-features", self.client.host), + Method::Get { query: () }, + 200, + ) + .await + } + + /// Update the experimental features + /// + /// # Example + /// + /// ``` + /// # use meilisearch_sdk::{client::Client, features::ExperimentalFeatures}; + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// let mut features = ExperimentalFeatures::new(&client); + /// features.set_vector_store(true); + /// features.update().await.unwrap(); + /// }); + /// ``` + pub async fn update(&self) -> Result { + self.client + .http_client + .request::<(), &Self, ExperimentalFeaturesResult>( + &format!("{}/experimental-features", self.client.host), + Method::Patch { + query: (), + body: self, + }, + 200, + ) + .await + } +} + +#[cfg(test)] +mod tests { + use super::*; + use meilisearch_test_macro::meilisearch_test; + + #[meilisearch_test] + async fn test_experimental_features_get(client: Client) { + let mut features = ExperimentalFeatures::new(&client); + features.set_vector_store(false); + let _ = features.update().await.unwrap(); + + let res = features.get().await.unwrap(); + + assert!(!res.vector_store); + } + + #[meilisearch_test] + async fn test_experimental_features_enable_vector_store(client: Client) { + let mut features = ExperimentalFeatures::new(&client); + features.set_vector_store(true); + + let res = features.update().await.unwrap(); + + assert!(res.vector_store); + } +} diff --git a/backend/vendor/meilisearch-sdk/src/indexes.rs b/backend/vendor/meilisearch-sdk/src/indexes.rs new file mode 100644 index 000000000..05c728f1b --- /dev/null +++ b/backend/vendor/meilisearch-sdk/src/indexes.rs @@ -0,0 +1,2265 @@ +use crate::{ + client::Client, + documents::{DocumentDeletionQuery, DocumentQuery, DocumentsQuery, DocumentsResults}, + errors::{Error, MeilisearchCommunicationError, MeilisearchError, MEILISEARCH_VERSION_HINT}, + request::*, + search::*, + task_info::TaskInfo, + tasks::*, + DefaultHttpClient, +}; +use serde::{de::DeserializeOwned, Deserialize, Serialize}; +use std::{collections::HashMap, fmt::Display, time::Duration}; +use time::OffsetDateTime; + +/// A Meilisearch [index](https://www.meilisearch.com/docs/learn/core_concepts/indexes). +/// +/// # Example +/// +/// You can create an index remotely and, if that succeed, make an `Index` out of it. +/// See the [`Client::create_index`] method. +/// ``` +/// # use meilisearch_sdk::{client::*, indexes::*}; +/// # +/// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); +/// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); +/// # +/// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { +/// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); +/// +/// // get the index called movies or create it if it does not exist +/// let movies = client +/// .create_index("index", None) +/// .await +/// .unwrap() +/// // We wait for the task to execute until completion +/// .wait_for_completion(&client, None, None) +/// .await +/// .unwrap() +/// // Once the task finished, we try to create an `Index` out of it +/// .try_make_index(&client) +/// .unwrap(); +/// +/// assert_eq!(movies.as_ref(), "index"); +/// # movies.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); +/// # }); +/// ``` +/// +/// Or, if you know the index already exist remotely you can create an [Index] with its builder. +/// ``` +/// # use meilisearch_sdk::{client::*, indexes::*}; +/// # +/// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); +/// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); +/// # +/// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { +/// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); +/// +/// // Meilisearch would be able to create the index if it does not exist during: +/// // - the documents addition (add and update routes) +/// // - the settings update +/// let movies = Index::new("movies", client); +/// +/// assert_eq!(movies.uid, "movies"); +/// # }); +/// ``` +#[derive(Debug, Serialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct Index { + #[serde(skip_serializing)] + pub client: Client, + pub uid: String, + #[serde(with = "time::serde::rfc3339::option")] + pub updated_at: Option, + #[serde(with = "time::serde::rfc3339::option")] + pub created_at: Option, + pub primary_key: Option, +} + +impl Index { + pub fn new(uid: impl Into, client: Client) -> Index { + Index { + uid: uid.into(), + client, + primary_key: None, + created_at: None, + updated_at: None, + } + } + /// Internal Function to create an [Index] from `serde_json::Value` and [Client]. + pub(crate) fn from_value( + raw_index: serde_json::Value, + client: Client, + ) -> Result, Error> { + #[derive(Deserialize, Debug)] + #[allow(non_snake_case)] + struct IndexFromSerde { + uid: String, + #[serde(with = "time::serde::rfc3339::option")] + updatedAt: Option, + #[serde(with = "time::serde::rfc3339::option")] + createdAt: Option, + primaryKey: Option, + } + + let i: IndexFromSerde = serde_json::from_value(raw_index).map_err(Error::ParseError)?; + + Ok(Index { + uid: i.uid, + client, + created_at: i.createdAt, + updated_at: i.updatedAt, + primary_key: i.primaryKey, + }) + } + + /// Update an [Index]. + /// + /// # Example + /// + /// ``` + /// # use meilisearch_sdk::{client::*, indexes::*, task_info::*, tasks::*}; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// # let mut index = client + /// # .create_index("index_update", None) + /// # .await + /// # .unwrap() + /// # .wait_for_completion(&client, None, None) + /// # .await + /// # .unwrap() + /// # // Once the task finished, we try to create an `Index` out of it + /// # .try_make_index(&client) + /// # .unwrap(); + /// # + /// index.primary_key = Some("special_id".to_string()); + /// let task = index.update() + /// .await + /// .unwrap() + /// .wait_for_completion(&client, None, None) + /// .await + /// .unwrap(); + /// + /// let index = client.get_index("index_update").await.unwrap(); + /// + /// assert_eq!(index.primary_key, Some("special_id".to_string())); + /// # index.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// # }); + /// ``` + pub async fn update(&self) -> Result { + let mut index_update = IndexUpdater::new(self, &self.client); + + if let Some(ref primary_key) = self.primary_key { + index_update.with_primary_key(primary_key); + } + + index_update.execute().await + } + + /// Delete the index. + /// + /// # Example + /// + /// ``` + /// # use meilisearch_sdk::{client::*, indexes::*}; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// # let index = client.create_index("delete", None).await.unwrap().wait_for_completion(&client, None, None).await.unwrap().try_make_index(&client).unwrap(); + /// + /// // get the index named "movies" and delete it + /// let index = client.index("delete"); + /// let task = index.delete().await.unwrap(); + /// + /// client.wait_for_task(task, None, None).await.unwrap(); + /// # }); + /// ``` + pub async fn delete(self) -> Result { + self.client + .http_client + .request::<(), (), TaskInfo>( + &format!("{}/indexes/{}", self.client.host, self.uid), + Method::Delete { query: () }, + 202, + ) + .await + } + + /// Search for documents matching a specific query in the index. + /// + /// See also [`Index::search`]. + /// + /// # Example + /// + /// ``` + /// # use serde::{Serialize, Deserialize}; + /// # use meilisearch_sdk::{client::*, indexes::*, search::*}; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// #[derive(Serialize, Deserialize, Debug)] + /// struct Movie { + /// name: String, + /// description: String, + /// } + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// let movies = client.index("execute_query"); + /// + /// // add some documents + /// # movies.add_or_replace(&[Movie{name:String::from("Interstellar"), description:String::from("Interstellar chronicles the adventures of a group of explorers who make use of a newly discovered wormhole to surpass the limitations on human space travel and conquer the vast distances involved in an interstellar voyage.")},Movie{name:String::from("Unknown"), description:String::from("Unknown")}], Some("name")).await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// + /// let query = SearchQuery::new(&movies).with_query("Interstellar").with_limit(5).build(); + /// let results = movies.execute_query::(&query).await.unwrap(); + /// + /// assert!(results.hits.len() > 0); + /// # movies.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// # }); + /// ``` + pub async fn execute_query( + &self, + body: &SearchQuery<'_, Http>, + ) -> Result, Error> { + self.client + .http_client + .request::<(), &SearchQuery, SearchResults>( + &format!("{}/indexes/{}/search", self.client.host, self.uid), + Method::Post { body, query: () }, + 200, + ) + .await + } + + /// Search for documents matching a specific query in the index. + /// + /// See also [`Index::execute_query`]. + /// + /// # Example + /// + /// ``` + /// # use serde::{Serialize, Deserialize}; + /// # use meilisearch_sdk::{client::*, indexes::*, search::*}; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// #[derive(Serialize, Deserialize, Debug)] + /// struct Movie { + /// name: String, + /// description: String, + /// } + /// + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// let mut movies = client.index("search"); + /// # // add some documents + /// # movies.add_or_replace(&[Movie{name:String::from("Interstellar"), description:String::from("Interstellar chronicles the adventures of a group of explorers who make use of a newly discovered wormhole to surpass the limitations on human space travel and conquer the vast distances involved in an interstellar voyage.")},Movie{name:String::from("Unknown"), description:String::from("Unknown")}], Some("name")).await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// + /// let results = movies.search() + /// .with_query("Interstellar") + /// .with_limit(5) + /// .execute::() + /// .await + /// .unwrap(); + /// + /// assert!(results.hits.len() > 0); + /// # movies.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// # }); + /// ``` + #[must_use] + pub fn search(&self) -> SearchQuery { + SearchQuery::new(self) + } + + /// Get one document using its unique id. + /// + /// Serde is needed. Add `serde = {version="1.0", features=["derive"]}` in the dependencies section of your Cargo.toml. + /// + /// # Example + /// + /// ``` + /// # use serde::{Serialize, Deserialize}; + /// # use meilisearch_sdk::{client::*, indexes::*}; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// #[derive(Serialize, Deserialize, Debug, PartialEq)] + /// struct Movie { + /// name: String, + /// description: String + /// } + /// + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// let movies = client.index("get_document"); + /// # movies.add_or_replace(&[Movie{name:String::from("Interstellar"), description:String::from("Interstellar chronicles the adventures of a group of explorers who make use of a newly discovered wormhole to surpass the limitations on human space travel and conquer the vast distances involved in an interstellar voyage.")}], Some("name")).await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// + /// // retrieve a document (you have to put the document in the index before) + /// let interstellar = movies.get_document::("Interstellar").await.unwrap(); + /// + /// assert_eq!(interstellar, Movie { + /// name: String::from("Interstellar"), + /// description: String::from("Interstellar chronicles the adventures of a group of explorers who make use of a newly discovered wormhole to surpass the limitations on human space travel and conquer the vast distances involved in an interstellar voyage."), + /// }); + /// # movies.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// # }); + /// ``` + pub async fn get_document( + &self, + document_id: &str, + ) -> Result { + let url = format!( + "{}/indexes/{}/documents/{}", + self.client.host, self.uid, document_id + ); + self.client + .http_client + .request::<(), (), T>(&url, Method::Get { query: () }, 200) + .await + } + + /// Get one document with parameters. + /// + /// # Example + /// + /// ``` + /// # use meilisearch_sdk::{client::*, indexes::*, documents::*}; + /// # use serde::{Deserialize, Serialize}; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// #[derive(Debug, Serialize, Deserialize, PartialEq)] + /// struct MyObject { + /// id: String, + /// kind: String, + /// } + /// + /// #[derive(Debug, Serialize, Deserialize, PartialEq)] + /// struct MyObjectReduced { + /// id: String, + /// } + /// # let index = client.index("document_query_execute"); + /// # 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(); + /// + /// let mut document_query = DocumentQuery::new(&index); + /// document_query.with_fields(["id"]); + /// + /// let document = index.get_document_with::("1", &document_query).await.unwrap(); + /// + /// assert_eq!( + /// document, + /// MyObjectReduced { id: "1".to_string() } + /// ); + /// # index.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// # }); + pub async fn get_document_with( + &self, + document_id: &str, + document_query: &DocumentQuery<'_, Http>, + ) -> Result { + let url = format!( + "{}/indexes/{}/documents/{}", + self.client.host, self.uid, document_id + ); + self.client + .http_client + .request::<&DocumentQuery, (), T>( + &url, + Method::Get { + query: document_query, + }, + 200, + ) + .await + } + + /// Get documents by batch. + /// + /// # Example + /// + /// ``` + /// # use serde::{Serialize, Deserialize}; + /// # use meilisearch_sdk::{client::*, indexes::*}; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// #[derive(Serialize, Deserialize, PartialEq, Debug)] + /// struct Movie { + /// name: String, + /// description: String, + /// } + /// + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// let movie_index = client.index("get_documents"); + /// # movie_index.add_or_replace(&[Movie{name:String::from("Interstellar"), description:String::from("Interstellar chronicles the adventures of a group of explorers who make use of a newly discovered wormhole to surpass the limitations on human space travel and conquer the vast distances involved in an interstellar voyage.")}], Some("name")).await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// + /// // retrieve movies (you have to put some movies in the index before) + /// let movies = movie_index.get_documents::().await.unwrap(); + /// + /// assert!(movies.results.len() > 0); + /// # movie_index.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// # }); + /// ``` + pub async fn get_documents( + &self, + ) -> Result, Error> { + let url = format!("{}/indexes/{}/documents", self.client.host, self.uid); + self.client + .http_client + .request::<(), (), DocumentsResults>(&url, Method::Get { query: () }, 200) + .await + } + + /// Get documents by batch with parameters. + /// + /// # Example + /// + /// ``` + /// # use serde::{Serialize, Deserialize}; + /// # use meilisearch_sdk::{client::*, indexes::*, documents::*}; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// #[derive(Serialize, Deserialize, PartialEq, Debug)] + /// struct Movie { + /// name: String, + /// description: String, + /// } + /// + /// #[derive(Deserialize, Debug, PartialEq)] + /// struct ReturnedMovie { + /// name: String, + /// } + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// + /// let movie_index = client.index("get_documents_with"); + /// # movie_index.add_or_replace(&[Movie{name:String::from("Interstellar"), description:String::from("Interstellar chronicles the adventures of a group of explorers who make use of a newly discovered wormhole to surpass the limitations on human space travel and conquer the vast distances involved in an interstellar voyage.")}], Some("name")).await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// + /// let mut query = DocumentsQuery::new(&movie_index); + /// query.with_limit(1); + /// query.with_fields(["name"]); + /// // retrieve movies (you have to put some movies in the index before) + /// let movies = movie_index.get_documents_with::(&query).await.unwrap(); + /// + /// assert_eq!(movies.results.len(), 1); + /// # movie_index.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// # }); + /// ``` + pub async fn get_documents_with( + &self, + documents_query: &DocumentsQuery<'_, Http>, + ) -> Result, Error> { + if documents_query.filter.is_some() { + let url = format!("{}/indexes/{}/documents/fetch", self.client.host, self.uid); + return self + .client + .http_client + .request::<(), &DocumentsQuery, DocumentsResults>( + &url, + Method::Post { + body: documents_query, + query: (), + }, + 200, + ) + .await + .map_err(|err| match err { + Error::MeilisearchCommunication(error) => { + Error::MeilisearchCommunication(MeilisearchCommunicationError { + status_code: error.status_code, + url: error.url, + message: Some(format!("{}.", MEILISEARCH_VERSION_HINT)), + }) + } + Error::Meilisearch(error) => Error::Meilisearch(MeilisearchError { + error_code: error.error_code, + error_link: error.error_link, + error_type: error.error_type, + error_message: format!( + "{}\n{}.", + error.error_message, MEILISEARCH_VERSION_HINT + ), + }), + _ => err, + }); + } + + let url = format!("{}/indexes/{}/documents", self.client.host, self.uid); + self.client + .http_client + .request::<&DocumentsQuery, (), DocumentsResults>( + &url, + Method::Get { + query: documents_query, + }, + 200, + ) + .await + } + + /// Add a list of documents or replace them if they already exist. + /// + /// If you send an already existing document (same id) the **whole existing document** will be overwritten by the new document. + /// Fields previously in the document not present in the new document are removed. + /// + /// For a partial update of the document see [`Index::add_or_update`]. + /// + /// You can use the alias [`Index::add_documents`] if you prefer. + /// + /// # Example + /// + /// ``` + /// # use serde::{Serialize, Deserialize}; + /// # use meilisearch_sdk::{client::*, indexes::*}; + /// # use std::thread::sleep; + /// # use std::time::Duration; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// #[derive(Serialize, Deserialize, Debug)] + /// struct Movie { + /// name: String, + /// description: String, + /// } + /// + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// let movie_index = client.index("add_or_replace"); + /// + /// let task = movie_index.add_or_replace(&[ + /// Movie{ + /// name: String::from("Interstellar"), + /// description: String::from("Interstellar chronicles the adventures of a group of explorers who make use of a newly discovered wormhole to surpass the limitations on human space travel and conquer the vast distances involved in an interstellar voyage.") + /// }, + /// Movie{ + /// // note that the id field can only take alphanumerics characters (and '-' and '/') + /// name: String::from("MrsDoubtfire"), + /// description: String::from("Loving but irresponsible dad Daniel Hillard, estranged from his exasperated spouse, is crushed by a court order allowing only weekly visits with his kids. When Daniel learns his ex needs a housekeeper, he gets the job -- disguised as an English nanny. Soon he becomes not only his children's best pal but the kind of parent he should have been from the start.") + /// }, + /// Movie{ + /// name: String::from("Apollo13"), + /// description: String::from("The true story of technical troubles that scuttle the Apollo 13 lunar mission in 1971, risking the lives of astronaut Jim Lovell and his crew, with the failed journey turning into a thrilling saga of heroism. Drifting more than 200,000 miles from Earth, the astronauts work furiously with the ground crew to avert tragedy.") + /// }, + /// ], Some("name")).await.unwrap(); + /// // Meilisearch may take some time to execute the request so we are going to wait till it's completed + /// client.wait_for_task(task, None, None).await.unwrap(); + /// + /// let movies = movie_index.get_documents::().await.unwrap(); + /// assert!(movies.results.len() >= 3); + /// # movie_index.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// # }); + /// ``` + pub async fn add_or_replace( + &self, + documents: &[T], + primary_key: Option<&str>, + ) -> Result { + let url = if let Some(primary_key) = primary_key { + format!( + "{}/indexes/{}/documents?primaryKey={}", + self.client.host, self.uid, primary_key + ) + } else { + format!("{}/indexes/{}/documents", self.client.host, self.uid) + }; + self.client + .http_client + .request::<(), &[T], TaskInfo>( + &url, + Method::Post { + query: (), + body: documents, + }, + 202, + ) + .await + } + + /// Add a raw and unchecked payload to meilisearch. + /// + /// This can be useful if your application is only forwarding data from other sources. + /// + /// If you send an already existing document (same id) the **whole existing document** will be overwritten by the new document. + /// Fields previously in the document not present in the new document are removed. + /// + /// For a partial update of the document see [`Index::add_or_update_unchecked_payload`]. + /// + /// # Example + /// + /// ``` + /// # use serde::{Serialize, Deserialize}; + /// # use meilisearch_sdk::{client::*, indexes::*}; + /// # use std::thread::sleep; + /// # use std::time::Duration; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// let movie_index = client.index("add_or_replace_unchecked_payload"); + /// + /// let task = movie_index.add_or_replace_unchecked_payload( + /// r#"{ "id": 1, "body": "doggo" } + /// { "id": 2, "body": "catto" }"#.as_bytes(), + /// "application/x-ndjson", + /// Some("id"), + /// ).await.unwrap(); + /// // Meilisearch may take some time to execute the request so we are going to wait till it's completed + /// client.wait_for_task(task, None, None).await.unwrap(); + /// + /// let movies = movie_index.get_documents::().await.unwrap(); + /// assert_eq!(movies.results.len(), 2); + /// # movie_index.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// # }); + /// ``` + pub async fn add_or_replace_unchecked_payload< + T: futures_io::AsyncRead + Send + Sync + 'static, + >( + &self, + payload: T, + content_type: &str, + primary_key: Option<&str>, + ) -> Result { + let url = if let Some(primary_key) = primary_key { + format!( + "{}/indexes/{}/documents?primaryKey={}", + self.client.host, self.uid, primary_key + ) + } else { + format!("{}/indexes/{}/documents", self.client.host, self.uid) + }; + self.client + .http_client + .stream_request::<(), T, TaskInfo>( + &url, + Method::Post { + query: (), + body: payload, + }, + content_type, + 202, + ) + .await + } + + /// Alias for [`Index::add_or_replace`]. + pub async fn add_documents( + &self, + documents: &[T], + primary_key: Option<&str>, + ) -> Result { + self.add_or_replace(documents, primary_key).await + } + + /// Add a raw ndjson payload and update them if they already. + /// + /// It configures the correct content type for ndjson data. + /// + /// If you send an already existing document (same id) the old document will be only partially updated according to the fields of the new document. + /// Thus, any fields not present in the new document are kept and remained unchanged. + /// + /// To completely overwrite a document, check out the [`Index::add_documents_ndjson`] documents method. + /// + /// # Example + /// + /// ``` + /// # use serde::{Serialize, Deserialize}; + /// # use meilisearch_sdk::{client::*, indexes::*}; + /// # use std::thread::sleep; + /// # use std::time::Duration; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// let movie_index = client.index("update_documents_ndjson"); + /// + /// let task = movie_index.update_documents_ndjson( + /// r#"{ "id": 1, "body": "doggo" } + /// { "id": 2, "body": "catto" }"#.as_bytes(), + /// Some("id"), + /// ).await.unwrap(); + /// // Meilisearch may take some time to execute the request so we are going to wait till it's completed + /// client.wait_for_task(task, None, None).await.unwrap(); + /// + /// let movies = movie_index.get_documents::().await.unwrap(); + /// assert_eq!(movies.results.len(), 2); + /// # movie_index.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// # }); + /// ``` + #[cfg(not(target_arch = "wasm32"))] + pub async fn update_documents_ndjson( + &self, + payload: T, + primary_key: Option<&str>, + ) -> Result { + self.add_or_update_unchecked_payload(payload, "application/x-ndjson", primary_key) + .await + } + + /// Add a raw ndjson payload to meilisearch. + /// + /// It configures the correct content type for ndjson data. + /// + /// If you send an already existing document (same id) the **whole existing document** will be overwritten by the new document. + /// Fields previously in the document not present in the new document are removed. + /// + /// For a partial update of the document see [`Index::update_documents_ndjson`]. + /// + /// # Example + /// + /// ``` + /// # use serde::{Serialize, Deserialize}; + /// # use meilisearch_sdk::{client::*, indexes::*}; + /// # use std::thread::sleep; + /// # use std::time::Duration; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// let movie_index = client.index("add_documents_ndjson"); + /// + /// let task = movie_index.add_documents_ndjson( + /// r#"{ "id": 1, "body": "doggo" } + /// { "id": 2, "body": "catto" }"#.as_bytes(), + /// Some("id"), + /// ).await.unwrap(); + /// // Meilisearch may take some time to execute the request so we are going to wait till it's completed + /// client.wait_for_task(task, None, None).await.unwrap(); + /// + /// let movies = movie_index.get_documents::().await.unwrap(); + /// assert_eq!(movies.results.len(), 2); + /// # movie_index.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// # }); + /// ``` + #[cfg(not(target_arch = "wasm32"))] + pub async fn add_documents_ndjson( + &self, + payload: T, + primary_key: Option<&str>, + ) -> Result { + self.add_or_replace_unchecked_payload(payload, "application/x-ndjson", primary_key) + .await + } + + /// Add a raw csv payload and update them if they already. + /// + /// It configures the correct content type for csv data. + /// + /// If you send an already existing document (same id) the old document will be only partially updated according to the fields of the new document. + /// Thus, any fields not present in the new document are kept and remained unchanged. + /// + /// To completely overwrite a document, check out the [`Index::add_documents_csv`] documents method. + /// + /// # Example + /// + /// ``` + /// # use serde::{Serialize, Deserialize}; + /// # use meilisearch_sdk::{client::*, indexes::*}; + /// # use std::thread::sleep; + /// # use std::time::Duration; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// let movie_index = client.index("update_documents_csv"); + /// + /// let task = movie_index.update_documents_csv( + /// "id,body\n1,\"doggo\"\n2,\"catto\"".as_bytes(), + /// Some("id"), + /// ).await.unwrap(); + /// // Meilisearch may take some time to execute the request so we are going to wait till it's completed + /// client.wait_for_task(task, None, None).await.unwrap(); + /// + /// let movies = movie_index.get_documents::().await.unwrap(); + /// assert_eq!(movies.results.len(), 2); + /// # movie_index.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// # }); + /// ``` + #[cfg(not(target_arch = "wasm32"))] + pub async fn update_documents_csv( + &self, + payload: T, + primary_key: Option<&str>, + ) -> Result { + self.add_or_update_unchecked_payload(payload, "text/csv", primary_key) + .await + } + + /// Add a raw csv payload to meilisearch. + /// + /// It configures the correct content type for csv data. + /// + /// If you send an already existing document (same id) the **whole existing document** will be overwritten by the new document. + /// Fields previously in the document not present in the new document are removed. + /// + /// For a partial update of the document see [`Index::update_documents_csv`]. + /// + /// # Example + /// + /// ``` + /// # use serde::{Serialize, Deserialize}; + /// # use meilisearch_sdk::{client::*, indexes::*}; + /// # use std::thread::sleep; + /// # use std::time::Duration; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// let movie_index = client.index("add_documents_csv"); + /// + /// let task = movie_index.add_documents_csv( + /// "id,body\n1,\"doggo\"\n2,\"catto\"".as_bytes(), + /// Some("id"), + /// ).await.unwrap(); + /// // Meilisearch may take some time to execute the request so we are going to wait till it's completed + /// client.wait_for_task(task, None, None).await.unwrap(); + /// + /// let movies = movie_index.get_documents::().await.unwrap(); + /// assert_eq!(movies.results.len(), 2); + /// # movie_index.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// # }); + /// ``` + #[cfg(not(target_arch = "wasm32"))] + pub async fn add_documents_csv( + &self, + payload: T, + primary_key: Option<&str>, + ) -> Result { + self.add_or_replace_unchecked_payload(payload, "text/csv", primary_key) + .await + } + + /// Add a list of documents and update them if they already. + /// + /// If you send an already existing document (same id) the old document will be only partially updated according to the fields of the new document. + /// Thus, any fields not present in the new document are kept and remained unchanged. + /// + /// To completely overwrite a document, check out the [`Index::add_or_replace`] documents method. + /// + /// # Example + /// + /// ``` + /// # use serde::{Serialize, Deserialize}; + /// # use meilisearch_sdk::client::*; + /// # use std::thread::sleep; + /// # use std::time::Duration; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// #[derive(Serialize, Deserialize, Debug)] + /// struct Movie { + /// name: String, + /// description: String, + /// } + /// + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// let movie_index = client.index("add_or_update"); + /// + /// let task = movie_index.add_or_update(&[ + /// Movie { + /// name: String::from("Interstellar"), + /// description: String::from("Interstellar chronicles the adventures of a group of explorers who make use of a newly discovered wormhole to surpass the limitations on human space travel and conquer the vast distances involved in an interstellar voyage.") + /// }, + /// Movie { + /// // note that the id field can only take alphanumerics characters (and '-' and '/') + /// name: String::from("MrsDoubtfire"), + /// description: String::from("Loving but irresponsible dad Daniel Hillard, estranged from his exasperated spouse, is crushed by a court order allowing only weekly visits with his kids. When Daniel learns his ex needs a housekeeper, he gets the job -- disguised as an English nanny. Soon he becomes not only his children's best pal but the kind of parent he should have been from the start.") + /// }, + /// Movie { + /// name: String::from("Apollo13"), + /// description: String::from("The true story of technical troubles that scuttle the Apollo 13 lunar mission in 1971, risking the lives of astronaut Jim Lovell and his crew, with the failed journey turning into a thrilling saga of heroism. Drifting more than 200,000 miles from Earth, the astronauts work furiously with the ground crew to avert tragedy.") + /// }, + /// ], Some("name")).await.unwrap(); + /// + /// // Meilisearch may take some time to execute the request so we are going to wait till it's completed + /// client.wait_for_task(task, None, None).await.unwrap(); + /// + /// let movies = movie_index.get_documents::().await.unwrap(); + /// assert!(movies.results.len() >= 3); + /// # movie_index.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// # }); + /// ``` + pub async fn add_or_update( + &self, + documents: &[T], + primary_key: Option<&str>, + ) -> Result { + let url = if let Some(primary_key) = primary_key { + format!( + "{}/indexes/{}/documents?primaryKey={}", + self.client.host, self.uid, primary_key + ) + } else { + format!("{}/indexes/{}/documents", self.client.host, self.uid) + }; + self.client + .http_client + .request::<(), &[T], TaskInfo>( + &url, + Method::Put { + query: (), + body: documents, + }, + 202, + ) + .await + } + + /// Add a raw and unchecked payload to meilisearch. + /// + /// This can be useful if your application is only forwarding data from other sources. + /// + /// If you send an already existing document (same id) the old document will be only partially updated according to the fields of the new document. + /// Thus, any fields not present in the new document are kept and remained unchanged. + /// + /// To completely overwrite a document, check out the [`Index::add_or_replace_unchecked_payload`] documents method. + /// + /// # Example + /// + /// ``` + /// # use serde::{Serialize, Deserialize}; + /// # use meilisearch_sdk::{client::*, indexes::*}; + /// # use std::thread::sleep; + /// # use std::time::Duration; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// let movie_index = client.index("add_or_replace_unchecked_payload"); + /// + /// let task = movie_index.add_or_update_unchecked_payload( + /// r#"{ "id": 1, "body": "doggo" } + /// { "id": 2, "body": "catto" }"#.as_bytes(), + /// "application/x-ndjson", + /// Some("id"), + /// ).await.unwrap(); + /// // Meilisearch may take some time to execute the request so we are going to wait till it's completed + /// client.wait_for_task(task, None, None).await.unwrap(); + /// + /// let movies = movie_index.get_documents::().await.unwrap(); + /// + /// assert_eq!(movies.results.len(), 2); + /// # movie_index.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// # }); + /// ``` + #[cfg(not(target_arch = "wasm32"))] + pub async fn add_or_update_unchecked_payload< + T: futures_io::AsyncRead + Send + Sync + 'static, + >( + &self, + payload: T, + content_type: &str, + primary_key: Option<&str>, + ) -> Result { + let url = if let Some(primary_key) = primary_key { + format!( + "{}/indexes/{}/documents?primaryKey={}", + self.client.host, self.uid, primary_key + ) + } else { + format!("{}/indexes/{}/documents", self.client.host, self.uid) + }; + self.client + .http_client + .stream_request::<(), T, TaskInfo>( + &url, + Method::Put { + query: (), + body: payload, + }, + content_type, + 202, + ) + .await + } + + /// Delete all documents in the [Index]. + /// + /// # Example + /// + /// ``` + /// # use serde::{Serialize, Deserialize}; + /// # use meilisearch_sdk::{client::*, indexes::*}; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # #[derive(Serialize, Deserialize, Debug)] + /// # struct Movie { + /// # name: String, + /// # description: String, + /// # } + /// # + /// # + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// # + /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// let movie_index = client.index("delete_all_documents"); + /// # + /// # movie_index.add_or_replace(&[Movie{name:String::from("Interstellar"), description:String::from("Interstellar chronicles the adventures of a group of explorers who make use of a newly discovered wormhole to surpass the limitations on human space travel and conquer the vast distances involved in an interstellar voyage.")}], Some("name")).await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// # + /// movie_index.delete_all_documents() + /// .await + /// .unwrap() + /// .wait_for_completion(&client, None, None) + /// .await + /// .unwrap(); + /// let movies = movie_index.get_documents::().await.unwrap(); + /// assert_eq!(movies.results.len(), 0); + /// # movie_index.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// # }); + /// ``` + pub async fn delete_all_documents(&self) -> Result { + self.client + .http_client + .request::<(), (), TaskInfo>( + &format!("{}/indexes/{}/documents", self.client.host, self.uid), + Method::Delete { query: () }, + 202, + ) + .await + } + + /// Delete one document based on its unique id. + /// + /// # Example + /// + /// ``` + /// # use serde::{Serialize, Deserialize}; + /// # use meilisearch_sdk::client::*; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # #[derive(Serialize, Deserialize, Debug)] + /// # struct Movie { + /// # name: String, + /// # description: String, + /// # } + /// # + /// # + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// # + /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// let mut movies = client.index("delete_document"); + /// # movies.add_or_replace(&[Movie{name:String::from("Interstellar"), description:String::from("Interstellar chronicles the adventures of a group of explorers who make use of a newly discovered wormhole to surpass the limitations on human space travel and conquer the vast distances involved in an interstellar voyage.")}], Some("name")).await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// // add a document with id = Interstellar + /// movies.delete_document("Interstellar") + /// .await + /// .unwrap() + /// .wait_for_completion(&client, None, None) + /// .await + /// .unwrap(); + /// # movies.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// # }); + /// ``` + pub async fn delete_document(&self, uid: T) -> Result { + self.client + .http_client + .request::<(), (), TaskInfo>( + &format!( + "{}/indexes/{}/documents/{}", + self.client.host, self.uid, uid + ), + Method::Delete { query: () }, + 202, + ) + .await + } + + /// Delete a selection of documents based on array of document id's. + /// + /// # Example + /// + /// ``` + /// # use serde::{Serialize, Deserialize}; + /// # use meilisearch_sdk::client::*; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # #[derive(Serialize, Deserialize, Debug)] + /// # struct Movie { + /// # name: String, + /// # description: String, + /// # } + /// # + /// # + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// # + /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// let movies = client.index("delete_documents"); + /// # + /// # // add some documents + /// # movies.add_or_replace(&[Movie{name:String::from("Interstellar"), description:String::from("Interstellar chronicles the adventures of a group of explorers who make use of a newly discovered wormhole to surpass the limitations on human space travel and conquer the vast distances involved in an interstellar voyage.")},Movie{name:String::from("Unknown"), description:String::from("Unknown")}], Some("name")).await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// # + /// // delete some documents + /// movies.delete_documents(&["Interstellar", "Unknown"]) + /// .await + /// .unwrap() + /// .wait_for_completion(&client, None, None) + /// .await + /// .unwrap(); + /// # movies.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// # }); + /// ``` + pub async fn delete_documents( + &self, + uids: &[T], + ) -> Result { + self.client + .http_client + .request::<(), &[T], TaskInfo>( + &format!( + "{}/indexes/{}/documents/delete-batch", + self.client.host, self.uid + ), + Method::Post { + query: (), + body: uids, + }, + 202, + ) + .await + } + + /// Delete a selection of documents with filters. + /// + /// # Example + /// + /// ``` + /// # use serde::{Serialize, Deserialize}; + /// # use meilisearch_sdk::{client::*, documents::*}; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # #[derive(Serialize, Deserialize, Debug)] + /// # struct Movie { + /// # name: String, + /// # id: String, + /// # } + /// # + /// # + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// # + /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// let index = client.index("delete_documents_with"); + /// # + /// # index.set_filterable_attributes(["id"]); + /// # // add some documents + /// # index.add_or_replace(&[Movie{id:String::from("1"), name: String::from("First movie") }, Movie{id:String::from("1"), name: String::from("First movie") }], Some("id")).await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// + /// let mut query = DocumentDeletionQuery::new(&index); + /// query.with_filter("id = 1"); + /// // delete some documents + /// index.delete_documents_with(&query) + /// .await + /// .unwrap() + /// .wait_for_completion(&client, None, None) + /// .await + /// .unwrap(); + /// # index.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// # }); + /// ``` + pub async fn delete_documents_with( + &self, + query: &DocumentDeletionQuery<'_, Http>, + ) -> Result { + self.client + .http_client + .request::<(), &DocumentDeletionQuery, TaskInfo>( + &format!("{}/indexes/{}/documents/delete", self.client.host, self.uid), + Method::Post { + query: (), + body: query, + }, + 202, + ) + .await + } + + /// Alias for the [`Index::update`] method. + pub async fn set_primary_key( + &mut self, + primary_key: impl AsRef, + ) -> Result { + self.primary_key = Some(primary_key.as_ref().to_string()); + + self.update().await + } + + /// Fetch the information of the index as a raw JSON [Index], this index should already exist. + /// + /// If you use it directly from the [Client], you can use the method [`Client::get_raw_index`], which is the equivalent method from the client. + /// + /// # Example + /// + /// ``` + /// # use meilisearch_sdk::{client::*, indexes::*}; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// # let index = client.create_index("fetch_info", None).await.unwrap().wait_for_completion(&client, None, None).await.unwrap().try_make_index(&client).unwrap(); + /// let mut idx = client.index("fetch_info"); + /// idx.fetch_info().await.unwrap(); + /// + /// println!("{idx:?}"); + /// # index.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// # }); + /// ``` + pub async fn fetch_info(&mut self) -> Result<(), Error> { + let v = self.client.get_raw_index(&self.uid).await?; + *self = Index::from_value(v, self.client.clone())?; + Ok(()) + } + + /// Fetch the primary key of the index. + /// + /// # Example + /// + /// ``` + /// # use meilisearch_sdk::{client::*, indexes::*}; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// # // create the client + /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// let mut index = client.create_index("get_primary_key", Some("id")) + /// .await + /// .unwrap() + /// .wait_for_completion(&client, None, None) + /// .await.unwrap() + /// .try_make_index(&client) + /// .unwrap(); + /// + /// let primary_key = index.get_primary_key().await.unwrap(); + /// + /// assert_eq!(primary_key, Some("id")); + /// # index.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// # }); + /// ``` + pub async fn get_primary_key(&mut self) -> Result, Error> { + self.fetch_info().await?; + Ok(self.primary_key.as_deref()) + } + + /// Get a [Task] from a specific [Index] to keep track of [asynchronous operations](https://www.meilisearch.com/docs/learn/advanced/asynchronous_operations). + /// + /// # Example + /// + /// ``` + /// # use serde::{Serialize, Deserialize}; + /// # use std::thread::sleep; + /// # use std::time::Duration; + /// # use meilisearch_sdk::{client::*, indexes::*, tasks::Task}; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # #[derive(Debug, Serialize, Deserialize, PartialEq)] + /// # struct Document { + /// # id: usize, + /// # value: String, + /// # kind: String, + /// # } + /// # + /// # + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// let movies = client.index("get_task"); + /// + /// let task = movies.add_documents(&[ + /// Document { id: 0, kind: "title".into(), value: "The Social Network".to_string() } + /// ], None).await.unwrap(); + /// # task.clone().wait_for_completion(&client, None, None).await.unwrap(); + /// + /// // Get task status from the index, using `uid` + /// let status = movies.get_task(&task).await.unwrap(); + /// + /// let from_index = match status { + /// Task::Enqueued { content } => content.uid, + /// Task::Processing { content } => content.uid, + /// Task::Failed { content } => content.task.uid, + /// Task::Succeeded { content } => content.uid, + /// }; + /// + /// assert_eq!(task.get_task_uid(), from_index); + /// # movies.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// # }); + /// ``` + pub async fn get_task(&self, uid: impl AsRef) -> Result { + self.client + .http_client + .request::<(), (), Task>( + &format!("{}/tasks/{}", self.client.host, uid.as_ref()), + Method::Get { query: () }, + 200, + ) + .await + } + + /// Get the status of all tasks in a given index. + /// + /// # Example + /// + /// ``` + /// # use serde::{Serialize, Deserialize}; + /// # use meilisearch_sdk::{client::*, indexes::*}; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// # let index = client.create_index("get_tasks", None).await.unwrap().wait_for_completion(&client, None, None).await.unwrap().try_make_index(&client).unwrap(); + /// let tasks = index.get_tasks().await.unwrap(); + /// + /// assert!(tasks.results.len() > 0); + /// # index.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// # }); + /// ``` + pub async fn get_tasks(&self) -> Result { + let mut query = TasksSearchQuery::new(&self.client); + query.with_index_uids([self.uid.as_str()]); + + self.client.get_tasks_with(&query).await + } + + /// Get the status of all tasks in a given index. + /// + /// # Example + /// + /// ``` + /// # use serde::{Serialize, Deserialize}; + /// # use meilisearch_sdk::{client::*, indexes::*, tasks::*}; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// # let index = client.create_index("get_tasks_with", None).await.unwrap().wait_for_completion(&client, None, None).await.unwrap().try_make_index(&client).unwrap(); + /// let mut query = TasksSearchQuery::new(&client); + /// query.with_index_uids(["none_existant"]); + /// + /// let tasks = index.get_tasks_with(&query).await.unwrap(); + /// + /// assert!(tasks.results.len() > 0); + /// # index.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// # }); + /// ``` + pub async fn get_tasks_with( + &self, + tasks_query: &TasksQuery<'_, TasksPaginationFilters, Http>, + ) -> Result { + let mut query = tasks_query.clone(); + query.with_index_uids([self.uid.as_str()]); + + self.client.get_tasks_with(&query).await + } + + /// Get stats of an index. + /// + /// # Example + /// + /// ``` + /// # use meilisearch_sdk::{client::*, indexes::*}; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// # let index = client.create_index("get_stats", None).await.unwrap().wait_for_completion(&client, None, None).await.unwrap().try_make_index(&client).unwrap(); + /// let stats = index.get_stats().await.unwrap(); + /// + /// assert_eq!(stats.is_indexing, false); + /// # index.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// # }); + /// ``` + pub async fn get_stats(&self) -> Result { + self.client + .http_client + .request::<(), (), IndexStats>( + &format!("{}/indexes/{}/stats", self.client.host, self.uid), + Method::Get { query: () }, + 200, + ) + .await + } + + /// Wait until Meilisearch processes a [Task], and get its status. + /// + /// `interval` = The frequency at which the server should be polled. **Default = 50ms** + /// + /// `timeout` = The maximum time to wait for processing to complete. **Default = 5000ms** + /// + /// If the waited time exceeds `timeout` then an [`Error::Timeout`] will be returned. + /// + /// See also [`Client::wait_for_task`, `Task::wait_for_completion`]. + /// + /// # Example + /// + /// ``` + /// # use meilisearch_sdk::{client::*, indexes::*, tasks::Task}; + /// # use serde::{Serialize, Deserialize}; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # #[derive(Debug, Serialize, Deserialize, PartialEq)] + /// # struct Document { + /// # id: usize, + /// # value: String, + /// # kind: String, + /// # } + /// # + /// # + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// let movies = client.index("movies_index_wait_for_task"); + /// + /// let task = movies.add_documents(&[ + /// Document { id: 0, kind: "title".into(), value: "The Social Network".to_string() }, + /// Document { id: 1, kind: "title".into(), value: "Harry Potter and the Sorcerer's Stone".to_string() }, + /// ], None).await.unwrap(); + /// + /// let status = movies.wait_for_task(task, None, None).await.unwrap(); + /// + /// assert!(matches!(status, Task::Succeeded { .. })); + /// # movies.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// # }); + /// ``` + pub async fn wait_for_task( + &self, + task_id: impl AsRef, + interval: Option, + timeout: Option, + ) -> Result { + self.client.wait_for_task(task_id, interval, timeout).await + } + + /// Add documents to the index in batches. + /// + /// `documents` = A slice of documents + /// + /// `batch_size` = Optional parameter that allows you to specify the size of the batch + /// + /// **`batch_size` is 1000 by default** + /// + /// # Example + /// + /// ``` + /// # use serde::{Serialize, Deserialize}; + /// # use meilisearch_sdk::client::*; + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// #[derive(Serialize, Deserialize, Debug)] + /// struct Movie { + /// name: String, + /// description: String, + /// } + /// + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// let movie_index = client.index("add_documents_in_batches"); + /// + /// let tasks = movie_index.add_documents_in_batches(&[ + /// Movie { + /// name: String::from("Interstellar"), + /// description: String::from("Interstellar chronicles the adventures of a group of explorers who make use of a newly discovered wormhole to surpass the limitations on human space travel and conquer the vast distances involved in an interstellar voyage.") + /// }, + /// Movie { + /// // note that the id field can only take alphanumerics characters (and '-' and '/') + /// name: String::from("MrsDoubtfire"), + /// description: String::from("Loving but irresponsible dad Daniel Hillard, estranged from his exasperated spouse, is crushed by a court order allowing only weekly visits with his kids. When Daniel learns his ex needs a housekeeper, he gets the job -- disguised as an English nanny. Soon he becomes not only his children's best pal but the kind of parent he should have been from the start.") + /// }, + /// Movie { + /// name: String::from("Apollo13"), + /// description: String::from("The true story of technical troubles that scuttle the Apollo 13 lunar mission in 1971, risking the lives of astronaut Jim Lovell and his crew, with the failed journey turning into a thrilling saga of heroism. Drifting more than 200,000 miles from Earth, the astronauts work furiously with the ground crew to avert tragedy.") + /// }], + /// Some(1), + /// Some("name") + /// ).await.unwrap(); + /// + /// client.wait_for_task(tasks.last().unwrap(), None, None).await.unwrap(); + /// + /// let movies = movie_index.get_documents::().await.unwrap(); + /// + /// assert!(movies.results.len() >= 3); + /// # movie_index.delete().await.unwrap().wait_for_completion(&client, None, + /// # None).await.unwrap(); + /// # }); + /// ``` + pub async fn add_documents_in_batches( + &self, + documents: &[T], + batch_size: Option, + primary_key: Option<&str>, + ) -> Result, Error> { + let mut task = Vec::with_capacity(documents.len()); + for document_batch in documents.chunks(batch_size.unwrap_or(1000)) { + task.push(self.add_documents(document_batch, primary_key).await?); + } + Ok(task) + } + + /// Update documents to the index in batches. + /// + /// `documents` = A slice of documents + /// + /// `batch_size` = Optional parameter that allows you to specify the size of the batch + /// + /// **`batch_size` is 1000 by default** + /// + /// # Example + /// + /// ``` + /// # use serde::{Serialize, Deserialize}; + /// # use meilisearch_sdk::client::*; + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// #[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] + /// struct Movie { + /// name: String, + /// description: String, + /// } + /// + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// let movie_index = client.index("update_documents_in_batches"); + /// + /// let tasks = movie_index.add_documents_in_batches(&[ + /// Movie { + /// name: String::from("Interstellar"), + /// description: String::from("Interstellar chronicles the adventures of a group of explorers who make use of a newly discovered wormhole to surpass the limitations on human space travel and conquer the vast distances involved in an interstellar voyage.") + /// }, + /// Movie { + /// // note that the id field can only take alphanumerics characters (and '-' and '/') + /// name: String::from("MrsDoubtfire"), + /// description: String::from("Loving but irresponsible dad Daniel Hillard, estranged from his exasperated spouse, is crushed by a court order allowing only weekly visits with his kids. When Daniel learns his ex needs a housekeeper, he gets the job -- disguised as an English nanny. Soon he becomes not only his children's best pal but the kind of parent he should have been from the start.") + /// }, + /// Movie { + /// name: String::from("Apollo13"), + /// description: String::from("The true story of technical troubles that scuttle the Apollo 13 lunar mission in 1971, risking the lives of astronaut Jim Lovell and his crew, with the failed journey turning into a thrilling saga of heroism. Drifting more than 200,000 miles from Earth, the astronauts work furiously with the ground crew to avert tragedy.") + /// }], + /// Some(1), + /// Some("name") + /// ).await.unwrap(); + /// + /// client.wait_for_task(tasks.last().unwrap(), None, None).await.unwrap(); + /// + /// let movies = movie_index.get_documents::().await.unwrap(); + /// assert!(movies.results.len() >= 3); + /// + /// let updated_movies = [ + /// Movie { + /// name: String::from("Interstellar"), + /// description: String::from("Updated!") + /// }, + /// Movie { + /// // note that the id field can only take alphanumerics characters (and '-' and '/') + /// name: String::from("MrsDoubtfire"), + /// description: String::from("Updated!") + /// }, + /// Movie { + /// name: String::from("Apollo13"), + /// description: String::from("Updated!") + /// }]; + /// + /// let tasks = movie_index.update_documents_in_batches(&updated_movies, Some(1), None).await.unwrap(); + /// + /// client.wait_for_task(tasks.last().unwrap(), None, None).await.unwrap(); + /// + /// let movies_updated = movie_index.get_documents::().await.unwrap(); + /// + /// assert!(movies_updated.results.len() >= 3); + /// # movie_index.delete().await.unwrap().wait_for_completion(&client, None, + /// # None).await.unwrap(); + /// # }); + /// ``` + pub async fn update_documents_in_batches( + &self, + documents: &[T], + batch_size: Option, + primary_key: Option<&str>, + ) -> Result, Error> { + let mut task = Vec::with_capacity(documents.len()); + for document_batch in documents.chunks(batch_size.unwrap_or(1000)) { + task.push(self.add_or_update(document_batch, primary_key).await?); + } + Ok(task) + } +} + +impl AsRef for Index { + fn as_ref(&self) -> &str { + &self.uid + } +} + +/// An [`IndexUpdater`] used to update the specifics of an index. +/// +/// # Example +/// +/// ``` +/// # use meilisearch_sdk::{client::*, indexes::*, task_info::*, tasks::{Task, SucceededTask}}; +/// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); +/// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); +/// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { +/// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); +/// # let index = client +/// # .create_index("index_updater", None) +/// # .await +/// # .unwrap() +/// # .wait_for_completion(&client, None, None) +/// # .await +/// # .unwrap() +/// # // Once the task finished, we try to create an `Index` out of it +/// # .try_make_index(&client) +/// # .unwrap(); +/// let task = IndexUpdater::new("index_updater", &client) +/// .with_primary_key("special_id") +/// .execute() +/// .await +/// .unwrap() +/// .wait_for_completion(&client, None, None) +/// .await +/// .unwrap(); +/// +/// let index = client.get_index("index_updater").await.unwrap(); +/// +/// assert_eq!(index.primary_key, Some("special_id".to_string())); +/// # index.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); +/// # }); +/// ``` +#[derive(Debug, Serialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct IndexUpdater<'a, Http: HttpClient> { + #[serde(skip)] + pub client: &'a Client, + #[serde(skip_serializing)] + pub uid: String, + pub primary_key: Option, +} + +impl<'a, Http: HttpClient> IndexUpdater<'a, Http> { + pub fn new(uid: impl AsRef, client: &Client) -> IndexUpdater { + IndexUpdater { + client, + primary_key: None, + uid: uid.as_ref().to_string(), + } + } + /// Define the new `primary_key` to set on the [Index]. + /// + /// # Example + /// + /// ``` + /// # use meilisearch_sdk::{client::*, indexes::*, task_info::*, tasks::{Task, SucceededTask}}; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// # let index = client + /// # .create_index("index_updater_with_primary_key", None) + /// # .await + /// # .unwrap() + /// # .wait_for_completion(&client, None, None) + /// # .await + /// # .unwrap() + /// # // Once the task finished, we try to create an `Index` out of it + /// # .try_make_index(&client) + /// # .unwrap(); + /// let task = IndexUpdater::new("index_updater_with_primary_key", &client) + /// .with_primary_key("special_id") + /// .execute() + /// .await + /// .unwrap() + /// .wait_for_completion(&client, None, None) + /// .await + /// .unwrap(); + /// + /// let index = client.get_index("index_updater_with_primary_key").await.unwrap(); + /// + /// assert_eq!(index.primary_key, Some("special_id".to_string())); + /// # index.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// # }); + /// ``` + pub fn with_primary_key( + &mut self, + primary_key: impl AsRef, + ) -> &mut IndexUpdater<'a, Http> { + self.primary_key = Some(primary_key.as_ref().to_string()); + self + } + + /// Execute the update of an [Index] using the [`IndexUpdater`]. + /// + /// # Example + /// + /// ``` + /// # use meilisearch_sdk::{client::*, indexes::*, task_info::*, tasks::{Task, SucceededTask}}; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// # let index = client + /// # .create_index("index_updater_execute", None) + /// # .await + /// # .unwrap() + /// # .wait_for_completion(&client, None, None) + /// # .await + /// # .unwrap() + /// # // Once the task finished, we try to create an `Index` out of it + /// # .try_make_index(&client) + /// # .unwrap(); + /// let task = IndexUpdater::new("index_updater_execute", &client) + /// .with_primary_key("special_id") + /// .execute() + /// .await + /// .unwrap() + /// .wait_for_completion(&client, None, None) + /// .await + /// .unwrap(); + /// + /// let index = client.get_index("index_updater_execute").await.unwrap(); + /// + /// assert_eq!(index.primary_key, Some("special_id".to_string())); + /// # index.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// # }); + /// ``` + pub async fn execute(&'a self) -> Result { + self.client + .http_client + .request::<(), &IndexUpdater, TaskInfo>( + &format!("{}/indexes/{}", self.client.host, self.uid), + Method::Patch { + query: (), + body: self, + }, + 202, + ) + .await + } +} + +impl AsRef for IndexUpdater<'_, Http> { + fn as_ref(&self) -> &str { + &self.uid + } +} + +impl<'a, Http: HttpClient> AsRef> for IndexUpdater<'a, Http> { + fn as_ref(&self) -> &IndexUpdater<'a, Http> { + self + } +} + +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct IndexStats { + pub number_of_documents: usize, + pub is_indexing: bool, + pub field_distribution: HashMap, +} + +/// An [`IndexesQuery`] containing filter and pagination parameters when searching for [Indexes](Index). +/// +/// # Example +/// +/// ``` +/// # use meilisearch_sdk::{client::*, indexes::*}; +/// # +/// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); +/// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); +/// # +/// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { +/// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); +/// # let index = client +/// # .create_index("index_query_builder", None) +/// # .await +/// # .unwrap() +/// # .wait_for_completion(&client, None, None) +/// # .await +/// # .unwrap() +/// # // Once the task finished, we try to create an `Index` out of it. +/// # .try_make_index(&client) +/// # .unwrap(); +/// let mut indexes = IndexesQuery::new(&client) +/// .with_limit(1) +/// .execute().await.unwrap(); +/// +/// assert_eq!(indexes.results.len(), 1); +/// # index.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); +/// # }); +/// ``` +#[derive(Debug, Serialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct IndexesQuery<'a, Http: HttpClient> { + #[serde(skip_serializing)] + pub client: &'a Client, + /// The number of [Indexes](Index) to skip. + /// + /// If the value of the parameter `offset` is `n`, the `n` first indexes will not be returned. + /// This is helpful for pagination. + /// + /// Example: If you want to skip the first index, set offset to `1`. + #[serde(skip_serializing_if = "Option::is_none")] + pub offset: Option, + + /// The maximum number of [Indexes](Index) returned. + /// + /// If the value of the parameter `limit` is `n`, there will never be more than `n` indexes in the response. + /// This is helpful for pagination. + /// + /// Example: If you don't want to get more than two indexes, set limit to `2`. + /// + /// **Default: `20`** + #[serde(skip_serializing_if = "Option::is_none")] + pub limit: Option, +} + +impl<'a, Http: HttpClient> IndexesQuery<'a, Http> { + #[must_use] + pub fn new(client: &Client) -> IndexesQuery { + IndexesQuery { + client, + offset: None, + limit: None, + } + } + + /// Specify the offset. + /// + /// # Example + /// + /// ``` + /// # use meilisearch_sdk::{client::*, indexes::*}; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// # let index = client + /// # .create_index("index_query_with_offset", None) + /// # .await + /// # .unwrap() + /// # .wait_for_completion(&client, None, None) + /// # .await + /// # .unwrap() + /// # // Once the task finished, we try to create an `Index` out of it + /// # .try_make_index(&client) + /// # .unwrap(); + /// let mut indexes = IndexesQuery::new(&client) + /// .with_offset(1) + /// .execute().await.unwrap(); + /// + /// assert_eq!(indexes.offset, 1); + /// # index.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// # }); + /// ``` + pub fn with_offset(&mut self, offset: usize) -> &mut IndexesQuery<'a, Http> { + self.offset = Some(offset); + self + } + + /// Specify the maximum number of [Indexes](Index) to return. + /// + /// # Example + /// + /// ``` + /// # use meilisearch_sdk::{client::*, indexes::*}; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// # let index = client + /// # .create_index("index_query_with_limit", None) + /// # .await + /// # .unwrap() + /// # .wait_for_completion(&client, None, None) + /// # .await + /// # .unwrap() + /// # // Once the task finished, we try to create an `Index` out of it + /// # .try_make_index(&client) + /// # .unwrap(); + /// let mut indexes = IndexesQuery::new(&client) + /// .with_limit(1) + /// .execute().await.unwrap(); + /// + /// assert_eq!(indexes.results.len(), 1); + /// # index.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// # }); + /// ``` + pub fn with_limit(&mut self, limit: usize) -> &mut IndexesQuery<'a, Http> { + self.limit = Some(limit); + self + } + /// Get [Indexes](Index). + /// + /// # Example + /// + /// ``` + /// # use meilisearch_sdk::{indexes::IndexesQuery, client::Client}; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// # let index = client + /// # .create_index("index_query_with_execute", None) + /// # .await + /// # .unwrap() + /// # .wait_for_completion(&client, None, None) + /// # .await + /// # .unwrap() + /// # // Once the task finished, we try to create an `Index` out of it + /// # .try_make_index(&client) + /// # .unwrap(); + /// let mut indexes = IndexesQuery::new(&client) + /// .with_limit(1) + /// .execute().await.unwrap(); + /// + /// assert_eq!(indexes.results.len(), 1); + /// # index.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// # }); + /// ``` + pub async fn execute(&self) -> Result, Error> { + self.client.list_all_indexes_with(self).await + } +} + +#[derive(Debug, Clone)] +pub struct IndexesResults { + pub results: Vec>, + pub limit: u32, + pub offset: u32, + pub total: u32, +} + +#[cfg(test)] +mod tests { + use super::*; + + use big_s::S; + use meilisearch_test_macro::meilisearch_test; + use serde_json::json; + + #[meilisearch_test] + async fn test_from_value(client: Client) { + let t = OffsetDateTime::now_utc(); + let trfc3339 = t + .format(&time::format_description::well_known::Rfc3339) + .unwrap(); + + let value = json!({ + "createdAt": &trfc3339, + "primaryKey": null, + "uid": "test_from_value", + "updatedAt": &trfc3339, + }); + + let idx = Index { + uid: S("test_from_value"), + primary_key: None, + created_at: Some(t), + updated_at: Some(t), + client: client.clone(), + }; + + let res = Index::from_value(value, client).unwrap(); + + assert_eq!(res.updated_at, idx.updated_at); + assert_eq!(res.created_at, idx.created_at); + assert_eq!(res.uid, idx.uid); + assert_eq!(res.primary_key, idx.primary_key); + assert_eq!(res.client.host, idx.client.host); + assert_eq!(res.client.api_key, idx.client.api_key); + } + + #[meilisearch_test] + async fn test_fetch_info(mut index: Index) { + let res = index.fetch_info().await; + assert!(res.is_ok()); + assert!(index.updated_at.is_some()); + assert!(index.created_at.is_some()); + assert!(index.primary_key.is_none()); + } + + #[meilisearch_test] + async fn test_get_documents(index: Index) { + #[derive(Debug, Serialize, Deserialize, PartialEq)] + struct Object { + id: usize, + value: String, + kind: String, + } + let res = index.get_documents::().await.unwrap(); + + assert_eq!(res.limit, 20); + } + + #[meilisearch_test] + async fn test_get_documents_with(index: Index) { + #[derive(Debug, Serialize, Deserialize, PartialEq)] + struct Object { + id: usize, + value: String, + kind: String, + } + + let mut documents_query = DocumentsQuery::new(&index); + documents_query.with_limit(1).with_offset(2); + + let res = index + .get_documents_with::(&documents_query) + .await + .unwrap(); + + assert_eq!(res.limit, 1); + assert_eq!(res.offset, 2); + } + + #[meilisearch_test] + async fn test_update_document_json(client: Client, index: Index) -> Result<(), Error> { + let old_json = [ + json!({ "id": 1, "body": "doggo" }), + json!({ "id": 2, "body": "catto" }), + ]; + let updated_json = [ + json!({ "id": 1, "second_body": "second_doggo" }), + json!({ "id": 2, "second_body": "second_catto" }), + ]; + + let task = index + .add_documents(&old_json, Some("id")) + .await + .unwrap() + .wait_for_completion(&client, None, None) + .await + .unwrap(); + let _ = index.get_task(task).await?; + + let task = index + .add_or_update(&updated_json, None) + .await + .unwrap() + .wait_for_completion(&client, None, None) + .await + .unwrap(); + + let status = index.get_task(task).await?; + let elements = index.get_documents::().await.unwrap(); + + assert!(matches!(status, Task::Succeeded { .. })); + assert_eq!(elements.results.len(), 2); + + let expected_result = vec![ + json!( {"body": "doggo", "id": 1, "second_body": "second_doggo"}), + json!( {"body": "catto", "id": 2, "second_body": "second_catto"}), + ]; + + assert_eq!(elements.results, expected_result); + + Ok(()) + } + + #[meilisearch_test] + async fn test_add_documents_ndjson(client: Client, index: Index) -> Result<(), Error> { + let ndjson = r#"{ "id": 1, "body": "doggo" }{ "id": 2, "body": "catto" }"#.as_bytes(); + + let task = index + .add_documents_ndjson(ndjson, Some("id")) + .await? + .wait_for_completion(&client, None, None) + .await?; + + let status = index.get_task(task).await?; + let elements = index.get_documents::().await.unwrap(); + assert!(matches!(status, Task::Succeeded { .. })); + assert_eq!(elements.results.len(), 2); + + Ok(()) + } + + #[meilisearch_test] + async fn test_update_documents_ndjson(client: Client, index: Index) -> Result<(), Error> { + let old_ndjson = r#"{ "id": 1, "body": "doggo" }{ "id": 2, "body": "catto" }"#.as_bytes(); + let updated_ndjson = + r#"{ "id": 1, "second_body": "second_doggo" }{ "id": 2, "second_body": "second_catto" }"#.as_bytes(); + // Add first njdson document + let task = index + .add_documents_ndjson(old_ndjson, Some("id")) + .await? + .wait_for_completion(&client, None, None) + .await?; + let _ = index.get_task(task).await?; + + // Update via njdson document + let task = index + .update_documents_ndjson(updated_ndjson, Some("id")) + .await? + .wait_for_completion(&client, None, None) + .await?; + + let status = index.get_task(task).await?; + let elements = index.get_documents::().await.unwrap(); + + assert!(matches!(status, Task::Succeeded { .. })); + assert_eq!(elements.results.len(), 2); + + let expected_result = vec![ + json!( {"body": "doggo", "id": 1, "second_body": "second_doggo"}), + json!( {"body": "catto", "id": 2, "second_body": "second_catto"}), + ]; + + assert_eq!(elements.results, expected_result); + + Ok(()) + } + + #[meilisearch_test] + async fn test_add_documents_csv(client: Client, index: Index) -> Result<(), Error> { + let csv_input = "id,body\n1,\"doggo\"\n2,\"catto\"".as_bytes(); + + let task = index + .add_documents_csv(csv_input, Some("id")) + .await? + .wait_for_completion(&client, None, None) + .await?; + + let status = index.get_task(task).await?; + let elements = index.get_documents::().await.unwrap(); + assert!(matches!(status, Task::Succeeded { .. })); + assert_eq!(elements.results.len(), 2); + + Ok(()) + } + + #[meilisearch_test] + async fn test_update_documents_csv(client: Client, index: Index) -> Result<(), Error> { + let old_csv = "id,body\n1,\"doggo\"\n2,\"catto\"".as_bytes(); + let updated_csv = "id,body\n1,\"new_doggo\"\n2,\"new_catto\"".as_bytes(); + // Add first njdson document + let task = index + .add_documents_csv(old_csv, Some("id")) + .await? + .wait_for_completion(&client, None, None) + .await?; + let _ = index.get_task(task).await?; + + // Update via njdson document + let task = index + .update_documents_csv(updated_csv, Some("id")) + .await? + .wait_for_completion(&client, None, None) + .await?; + + let status = index.get_task(task).await?; + let elements = index.get_documents::().await.unwrap(); + + assert!(matches!(status, Task::Succeeded { .. })); + assert_eq!(elements.results.len(), 2); + + let expected_result = vec![ + json!( {"body": "new_doggo", "id": "1"}), + json!( {"body": "new_catto", "id": "2"}), + ]; + + assert_eq!(elements.results, expected_result); + + Ok(()) + } + #[meilisearch_test] + + async fn test_get_one_task(client: Client, index: Index) -> Result<(), Error> { + let task = index + .delete_all_documents() + .await? + .wait_for_completion(&client, None, None) + .await?; + + let status = index.get_task(task).await?; + + match status { + Task::Enqueued { + content: + EnqueuedTask { + index_uid: Some(index_uid), + .. + }, + } + | Task::Processing { + content: + ProcessingTask { + index_uid: Some(index_uid), + .. + }, + } + | Task::Failed { + content: + FailedTask { + task: + SucceededTask { + index_uid: Some(index_uid), + .. + }, + .. + }, + } + | Task::Succeeded { + content: + SucceededTask { + index_uid: Some(index_uid), + .. + }, + } => assert_eq!(index_uid, *index.uid), + task => panic!( + "The task should have an index_uid that is not null {:?}", + task + ), + } + Ok(()) + } +} diff --git a/backend/vendor/meilisearch-sdk/src/key.rs b/backend/vendor/meilisearch-sdk/src/key.rs new file mode 100644 index 000000000..bda7eb267 --- /dev/null +++ b/backend/vendor/meilisearch-sdk/src/key.rs @@ -0,0 +1,723 @@ +use serde::{Deserialize, Serialize}; +use time::OffsetDateTime; + +use crate::{client::Client, errors::Error, request::HttpClient}; + +/// Represents a [meilisearch key](https://www.meilisearch.com/docs/reference/api/keys#returned-fields). +/// +/// You can get a [Key] from the [`Client::get_key`] method, or you can create a [Key] with the [`KeyBuilder::new`] or [`Client::create_key`] methods. +#[derive(Debug, Serialize, Deserialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct Key { + #[serde(skip_serializing_if = "Vec::is_empty")] + pub actions: Vec, + #[serde(skip_serializing, with = "time::serde::rfc3339")] + pub created_at: OffsetDateTime, + pub description: Option, + pub name: Option, + #[serde(with = "time::serde::rfc3339::option")] + pub expires_at: Option, + #[serde(skip_serializing_if = "Vec::is_empty")] + pub indexes: Vec, + #[serde(skip_serializing)] + pub key: String, + #[serde(skip_serializing)] + pub uid: String, + #[serde(skip_serializing, with = "time::serde::rfc3339")] + pub updated_at: OffsetDateTime, +} + +impl Key { + /// Update the description of the [Key]. + /// + /// # Example + /// + /// ``` + /// # use meilisearch_sdk::{key::*, client::Client}; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// let description = "My not so little lovely test key".to_string(); + /// let mut key = KeyBuilder::new() + /// .with_action(Action::DocumentsAdd) + /// .with_index("*") + /// .with_description(&description) + /// .execute(&client).await.unwrap(); + /// + /// assert_eq!(key.description, Some(description)); + /// # client.delete_key(key).await.unwrap(); + /// # }); + /// ``` + pub fn with_description(&mut self, desc: impl AsRef) -> &mut Key { + self.description = Some(desc.as_ref().to_string()); + self + } + + /// Update the name of the [Key]. + /// + /// # Example + /// + /// ``` + /// # use meilisearch_sdk::{key::*, client::Client}; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// let name = "lovely key".to_string(); + /// let mut key = KeyBuilder::new() + /// .with_action(Action::DocumentsAdd) + /// .with_index("*") + /// .execute(&client) + /// .await + /// .unwrap(); + /// + /// key.with_name(&name); + /// + /// assert_eq!(key.name, Some(name)); + /// # client.delete_key(key).await.unwrap(); + /// # }); + /// ``` + pub fn with_name(&mut self, desc: impl AsRef) -> &mut Key { + self.name = Some(desc.as_ref().to_string()); + self + } + + /// Update the [Key]. + /// + /// # Example + /// + /// ``` + /// # use meilisearch_sdk::{key::KeyBuilder, client::Client}; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// let mut key = KeyBuilder::new() + /// .execute(&client) + /// .await + /// .unwrap(); + /// + /// let description = "My not so little lovely test key".to_string(); + /// key.with_description(&description); + /// + /// let key = key.update(&client).await.unwrap(); + /// + /// assert_eq!(key.description, Some(description)); + /// # client.delete_key(key).await.unwrap(); + /// # }); + /// ``` + pub async fn update(&self, client: &Client) -> Result { + // only send description and name + let mut key_update = KeyUpdater::new(self); + + if let Some(ref description) = self.description { + key_update.with_description(description); + } + if let Some(ref name) = self.name { + key_update.with_name(name); + } + + key_update.execute(client).await + } + + /// Delete the [Key]. + /// + /// # Example + /// + /// ``` + /// # use meilisearch_sdk::{key::KeyBuilder, client::Client}; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// let mut key = KeyBuilder::new() + /// .execute(&client).await.unwrap(); + /// + /// client.delete_key(key).await.unwrap(); + /// # }); + /// ``` + pub async fn delete(&self, client: &Client) -> Result<(), Error> { + client.delete_key(self).await + } +} + +impl AsRef for Key { + fn as_ref(&self) -> &str { + &self.key + } +} + +impl AsRef for Key { + fn as_ref(&self) -> &Key { + self + } +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct KeyUpdater { + pub description: Option, + pub name: Option, + #[serde(skip_serializing)] + pub key: String, +} + +impl KeyUpdater { + pub fn new(key_or_uid: impl AsRef) -> KeyUpdater { + KeyUpdater { + description: None, + name: None, + key: key_or_uid.as_ref().to_string(), + } + } + + /// Update the description of the [Key]. + /// + /// # Example + /// + /// ``` + /// # use meilisearch_sdk::{key::*, client::Client}; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// let mut new_key = KeyBuilder::new() + /// .execute(&client) + /// .await + /// .unwrap(); + /// + /// let description = "My not so little lovely test key".to_string(); + /// let mut key_update = KeyUpdater::new(new_key) + /// .with_description(&description) + /// .execute(&client) + /// .await + /// .unwrap(); + /// + /// assert_eq!(key_update.description, Some(description)); + /// # client.delete_key(key_update).await.unwrap(); + /// # }); + /// ``` + pub fn with_description(&mut self, desc: impl AsRef) -> &mut KeyUpdater { + self.description = Some(desc.as_ref().to_string()); + self + } + + /// Update the name of the [Key]. + /// + /// # Example + /// + /// ``` + /// # use meilisearch_sdk::{key::*, client::Client}; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// let mut new_key = KeyBuilder::new() + /// .execute(&client) + /// .await + /// .unwrap(); + /// + /// let name = "lovely key".to_string(); + /// let mut key_update = KeyUpdater::new(new_key) + /// .with_name(&name) + /// .execute(&client) + /// .await + /// .unwrap(); + /// + /// assert_eq!(key_update.name, Some(name)); + /// # client.delete_key(key_update).await.unwrap(); + /// # }); + /// ``` + pub fn with_name(&mut self, desc: impl AsRef) -> &mut KeyUpdater { + self.name = Some(desc.as_ref().to_string()); + self + } + + /// Update a [Key] using the [`KeyUpdater`]. + /// + /// # Example + /// + /// ``` + /// # use meilisearch_sdk::{key::*, client::Client}; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// let description = "My little lovely test key".to_string(); + /// let key = KeyBuilder::new() + /// .execute(&client).await.unwrap(); + /// + /// let mut key_update = KeyUpdater::new(&key.key); + /// key_update.with_description(&description).execute(&client).await; + /// + /// assert_eq!(key_update.description, Some(description)); + /// # client.delete_key(key).await.unwrap(); + /// # }); + /// ``` + pub async fn execute(&self, client: &Client) -> Result { + client.update_key(self).await + } +} + +impl AsRef for KeyUpdater { + fn as_ref(&self) -> &str { + &self.key + } +} + +impl AsRef for KeyUpdater { + fn as_ref(&self) -> &KeyUpdater { + self + } +} + +#[derive(Debug, Serialize, Clone, Default)] +#[serde(rename_all = "camelCase")] +pub struct KeysQuery { + /// The number of documents to skip. + /// + /// If the value of the parameter `offset` is `n`, the `n` first documents (ordered by relevance) will not be returned. + /// This is helpful for pagination. + /// + /// Example: If you want to skip the first document, set offset to `1`. + #[serde(skip_serializing_if = "Option::is_none")] + pub offset: Option, + /// The maximum number of documents returned. + /// + /// If the value of the parameter `limit` is `n`, there will never be more than `n` documents in the response. + /// This is helpful for pagination. + /// + /// Example: If you don't want to get more than two documents, set limit to `2`. + /// + /// **Default: `20`** + #[serde(skip_serializing_if = "Option::is_none")] + pub limit: Option, +} + +impl KeysQuery { + /// Create a [`KeysQuery`] with only a description. + /// + /// # Example + /// + /// ``` + /// # use meilisearch_sdk::{key::KeysQuery}; + /// let builder = KeysQuery::new(); + /// ``` + #[must_use] + pub fn new() -> KeysQuery { + Self::default() + } + + /// Specify the offset. + /// + /// # Example + /// + /// ``` + /// # use meilisearch_sdk::{key::*, client::Client}; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// let mut keys = KeysQuery::new() + /// .with_offset(1) + /// .execute(&client).await.unwrap(); + /// + /// assert_eq!(keys.offset, 1); + /// # }); + /// ``` + pub fn with_offset(&mut self, offset: usize) -> &mut KeysQuery { + self.offset = Some(offset); + self + } + + /// Specify the maximum number of keys to return. + /// + /// # Example + /// + /// ``` + /// # use meilisearch_sdk::{key::*, client::Client}; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// let mut keys = KeysQuery::new() + /// .with_limit(1) + /// .execute(&client).await.unwrap(); + /// + /// assert_eq!(keys.results.len(), 1); + /// # }); + /// ``` + pub fn with_limit(&mut self, limit: usize) -> &mut KeysQuery { + self.limit = Some(limit); + self + } + + /// Get [Key]'s. + /// + /// # Example + /// + /// ``` + /// # use meilisearch_sdk::{key::*, client::Client}; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// let mut keys = KeysQuery::new() + /// .with_limit(1) + /// .execute(&client).await.unwrap(); + /// + /// assert_eq!(keys.results.len(), 1); + /// # }); + /// ``` + pub async fn execute( + &self, + client: &Client, + ) -> Result { + client.get_keys_with(self).await + } +} + +/// The [`KeyBuilder`] is an analog to the [Key] type but without all the fields managed by Meilisearch. +/// +/// It's used to create [Key]. +/// +/// # Example +/// +/// ``` +/// # use meilisearch_sdk::{key::*, client::Client}; +/// # +/// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); +/// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); +/// # +/// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { +/// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); +/// let description = "My little lovely test key".to_string(); +/// let key = KeyBuilder::new() +/// .with_description(&description) +/// .execute(&client).await.unwrap(); +/// +/// assert_eq!(key.description, Some(description)); +/// # client.delete_key(key).await.unwrap(); +/// # }); +/// ``` +#[derive(Debug, Clone, Serialize, Default)] +#[serde(rename_all = "camelCase")] +pub struct KeyBuilder { + pub actions: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + pub description: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub name: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub uid: Option, + #[serde(with = "time::serde::rfc3339::option")] + pub expires_at: Option, + pub indexes: Vec, +} + +impl KeyBuilder { + /// Create a [`KeyBuilder`]. + /// + /// # Example + /// + /// ``` + /// # use meilisearch_sdk::key::KeyBuilder; + /// let builder = KeyBuilder::new(); + /// ``` + #[must_use] + pub fn new() -> KeyBuilder { + Self::default() + } + + /// Declare a set of actions the [Key] will be able to execute. + /// + /// # Example + /// + /// ``` + /// # use meilisearch_sdk::key::*; + /// let mut builder = KeyBuilder::new(); + /// builder.with_actions(vec![Action::Search, Action::DocumentsAdd]); + /// ``` + pub fn with_actions(&mut self, actions: impl IntoIterator) -> &mut KeyBuilder { + self.actions.extend(actions); + self + } + + /// Add one action the [Key] will be able to execute. + /// + /// # Example + /// + /// ``` + /// # use meilisearch_sdk::key::*; + /// let mut builder = KeyBuilder::new(); + /// builder.with_action(Action::DocumentsAdd); + /// ``` + pub fn with_action(&mut self, action: Action) -> &mut KeyBuilder { + self.actions.push(action); + self + } + + /// Set the expiration date of the [Key]. + /// + /// # Example + /// + /// ``` + /// # use meilisearch_sdk::key::KeyBuilder; + /// # use time::{OffsetDateTime, Duration}; + /// let mut builder = KeyBuilder::new(); + /// // create a key that expires in two weeks from now + /// builder.with_expires_at(OffsetDateTime::now_utc() + Duration::WEEK * 2); + /// ``` + pub fn with_expires_at(&mut self, expires_at: OffsetDateTime) -> &mut KeyBuilder { + self.expires_at = Some(expires_at); + self + } + + /// Set the indexes the [Key] can manage. + /// + /// # Example + /// + /// ``` + /// # use meilisearch_sdk::{key::KeyBuilder, client::Client}; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// let mut key = KeyBuilder::new() + /// .with_indexes(vec!["test", "movies"]) + /// .execute(&client) + /// .await + /// .unwrap(); + /// + /// assert_eq!(vec!["test", "movies"], key.indexes); + /// # client.delete_key(key).await.unwrap(); + /// # }); + /// ``` + pub fn with_indexes( + &mut self, + indexes: impl IntoIterator>, + ) -> &mut KeyBuilder { + self.indexes = indexes + .into_iter() + .map(|index| index.as_ref().to_string()) + .collect(); + self + } + + /// Add one index the [Key] can manage. + /// + /// # Example + /// + /// ``` + /// # use meilisearch_sdk::key::KeyBuilder; + /// let mut builder = KeyBuilder::new(); + /// builder.with_index("test"); + /// ``` + pub fn with_index(&mut self, index: impl AsRef) -> &mut KeyBuilder { + self.indexes.push(index.as_ref().to_string()); + self + } + + /// Add a description to the [Key]. + /// + /// # Example + /// + /// ``` + /// # use meilisearch_sdk::{key::*, client::Client}; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// let description = "My not so little lovely test key".to_string(); + /// let mut key = KeyBuilder::new() + /// .with_description(&description) + /// .execute(&client).await.unwrap(); + /// + /// assert_eq!(key.description, Some(description)); + /// # client.delete_key(key).await.unwrap(); + /// # }); + /// ``` + pub fn with_description(&mut self, desc: impl AsRef) -> &mut KeyBuilder { + self.description = Some(desc.as_ref().to_string()); + self + } + + /// Add a name to the [Key]. + /// + /// # Example + /// + /// ``` + /// # use meilisearch_sdk::{key::*, client::Client}; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// let name = "lovely key".to_string(); + /// let mut key = KeyBuilder::new() + /// .with_name(&name) + /// .execute(&client).await.unwrap(); + /// + /// assert_eq!(key.name, Some(name)); + /// # client.delete_key(key).await.unwrap(); + /// # }); + /// ``` + pub fn with_name(&mut self, desc: impl AsRef) -> &mut KeyBuilder { + self.name = Some(desc.as_ref().to_string()); + self + } + + /// Add a uid to the [Key]. + /// + /// # Example + /// + /// ``` + /// # use meilisearch_sdk::{key::*, client::Client}; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// let uid = "93bcd7fb-2196-4fd9-acb7-3fca8a96e78f".to_string(); + /// let mut key = KeyBuilder::new() + /// .with_uid(&uid) + /// .execute(&client).await.unwrap(); + /// + /// assert_eq!(key.uid, uid); + /// # client.delete_key(key).await.unwrap(); + /// # }); + /// ``` + pub fn with_uid(&mut self, desc: impl AsRef) -> &mut KeyBuilder { + self.uid = Some(desc.as_ref().to_string()); + self + } + + /// Create a [Key] from the builder. + /// + /// # Example + /// + /// ``` + /// # use meilisearch_sdk::{key::KeyBuilder, client::Client}; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// let description = "My little lovely test key".to_string(); + /// let key = KeyBuilder::new() + /// .with_description(&description) + /// .execute(&client).await.unwrap(); + /// + /// assert_eq!(key.description, Some(description)); + /// # client.delete_key(key).await.unwrap(); + /// # }); + /// ``` + pub async fn execute(&self, client: &Client) -> Result { + client.create_key(self).await + } +} + +impl AsRef for KeyBuilder { + fn as_ref(&self) -> &KeyBuilder { + self + } +} + +#[derive(Debug, Copy, Clone, Eq, PartialEq, Serialize, Deserialize)] +pub enum Action { + /// Provides access to everything. + #[serde(rename = "*")] + All, + /// Provides access to both [`POST`](https://www.meilisearch.com/docs/reference/api/search.md#search-in-an-index-with-post-route) and [`GET`](https://www.meilisearch.com/docs/reference/api/search.md#search-in-an-index-with-get-route) search endpoints on authorized indexes. + #[serde(rename = "search")] + Search, + /// Provides access to the [add documents](https://www.meilisearch.com/docs/reference/api/documents.md#add-or-replace-documents) and [update documents](https://www.meilisearch.com/docs/reference/api/documents.md#add-or-update-documents) endpoints on authorized indexes. + #[serde(rename = "documents.add")] + DocumentsAdd, + /// Provides access to the [get one document](https://www.meilisearch.com/docs/reference/api/documents.md#get-one-document) and [get documents](https://www.meilisearch.com/docs/reference/api/documents.md#get-documents) endpoints on authorized indexes. + #[serde(rename = "documents.get")] + DocumentsGet, + /// Provides access to the [delete one document](https://www.meilisearch.com/docs/reference/api/documents.md#delete-one-document), [delete all documents](https://www.meilisearch.com/docs/reference/api/documents.md#delete-all-documents), and [batch delete](https://www.meilisearch.com/docs/reference/api/documents.md#delete-documents-by-batch) endpoints on authorized indexes. + #[serde(rename = "documents.delete")] + DocumentsDelete, + /// Provides access to the [create index](https://www.meilisearch.com/docs/reference/api/indexes.md#create-an-index) endpoint. + #[serde(rename = "indexes.create")] + IndexesCreate, + /// Provides access to the [get one index](https://www.meilisearch.com/docs/reference/api/indexes.md#get-one-index) and [list all indexes](https://www.meilisearch.com/docs/reference/api/indexes.md#list-all-indexes) endpoints. **Non-authorized `indexes` will be omitted from the response**. + #[serde(rename = "indexes.get")] + IndexesGet, + /// Provides access to the [update index](https://www.meilisearch.com/docs/reference/api/indexes.md#update-an-index) endpoint. + #[serde(rename = "indexes.update")] + IndexesUpdate, + /// Provides access to the [delete index](https://www.meilisearch.com/docs/reference/api/indexes.md#delete-an-index) endpoint. + #[serde(rename = "indexes.delete")] + IndexesDelete, + /// Provides access to the [get one task](https://www.meilisearch.com/docs/reference/api/tasks.md#get-task) and [get all tasks](https://www.meilisearch.com/docs/reference/api/tasks.md#get-all-tasks) endpoints. **Tasks from non-authorized `indexes` will be omitted from the response**. Also provides access to the [get one task by index](https://www.meilisearch.com/docs/reference/api/tasks.md#get-task-by-index) and [get all tasks by index](https://www.meilisearch.com/docs/reference/api/tasks.md#get-all-tasks-by-index) endpoints on authorized indexes. + #[serde(rename = "tasks.get")] + TasksGet, + /// Provides access to the [get settings](https://www.meilisearch.com/docs/reference/api/settings.md#get-settings) endpoint and equivalents for all subroutes on authorized indexes. + #[serde(rename = "settings.get")] + SettingsGet, + /// Provides access to the [update settings](https://www.meilisearch.com/docs/reference/api/settings.md#update-settings) and [reset settings](https://www.meilisearch.com/docs/reference/api/settings.md#reset-settings) endpoints and equivalents for all subroutes on authorized indexes. + #[serde(rename = "settings.update")] + SettingsUpdate, + /// Provides access to the [get stats of an index](https://www.meilisearch.com/docs/reference/api/stats.md#get-stats-of-an-index) endpoint and the [get stats of all indexes](https://www.meilisearch.com/docs/reference/api/stats.md#get-stats-of-all-indexes) endpoint. For the latter, **non-authorized `indexes` are omitted from the response**. + #[serde(rename = "stats.get")] + StatsGet, + /// Provides access to the [create dump](https://www.meilisearch.com/docs/reference/api/dump.md#create-a-dump) endpoint. **Not restricted by `indexes`.** + #[serde(rename = "dumps.create")] + DumpsCreate, + /// Provides access to the [get dump status](https://www.meilisearch.com/docs/reference/api/dump.md#get-dump-status) endpoint. **Not restricted by `indexes`.** + #[serde(rename = "dumps.get")] + DumpsGet, + /// Provides access to the [get Meilisearch version](https://www.meilisearch.com/docs/reference/api/version.md#get-version-of-meilisearch) endpoint. + #[serde(rename = "version")] + Version, + /// Provides access to the [get Key](https://www.meilisearch.com/docs/reference/api/keys#get-one-key) and [get Keys](https://www.meilisearch.com/docs/reference/api/keys#get-all-keys) endpoints. + #[serde(rename = "keys.get")] + KeyGet, + /// Provides access to the [create key](https://www.meilisearch.com/docs/reference/api/keys#create-a-key) endpoint. + #[serde(rename = "keys.create")] + KeyCreate, + /// Provides access to the [update key](https://www.meilisearch.com/docs/reference/api/keys#update-a-key) endpoint. + #[serde(rename = "keys.update")] + KeyUpdate, + /// Provides access to the [delete key](https://www.meilisearch.com/docs/reference/api/keys#delete-a-key) endpoint. + #[serde(rename = "keys.delete")] + KeyDelete, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct KeysResults { + pub results: Vec, + pub limit: u32, + pub offset: u32, +} diff --git a/backend/vendor/meilisearch-sdk/src/lib.rs b/backend/vendor/meilisearch-sdk/src/lib.rs new file mode 100644 index 000000000..96e7bc886 --- /dev/null +++ b/backend/vendor/meilisearch-sdk/src/lib.rs @@ -0,0 +1,280 @@ +//! # 🚀 Getting started +//! +//! ### Add Documents +//! +//! ``` +//! use meilisearch_sdk::client::*; +//! use serde::{Serialize, Deserialize}; +//! use futures::executor::block_on; +//! +//! #[derive(Serialize, Deserialize, Debug)] +//! struct Movie { +//! id: usize, +//! title: String, +//! genres: Vec, +//! } +//! +//! +//! #[tokio::main(flavor = "current_thread")] +//! async fn main() { +//! # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); +//! # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); +//! // Create a client (without sending any request so that can't fail) +//! let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); +//! +//! # let index = client.create_index("movies", None).await.unwrap().wait_for_completion(&client, None, None).await.unwrap().try_make_index(&client).unwrap(); +//! // An index is where the documents are stored. +//! let movies = client.index("movies"); +//! +//! // Add some movies in the index. If the index 'movies' does not exist, Meilisearch creates it when you first add the documents. +//! movies.add_documents(&[ +//! Movie { id: 1, title: String::from("Carol"), genres: vec!["Romance".to_string(), "Drama".to_string()] }, +//! Movie { id: 2, title: String::from("Wonder Woman"), genres: vec!["Action".to_string(), "Adventure".to_string()] }, +//! Movie { id: 3, title: String::from("Life of Pi"), genres: vec!["Adventure".to_string(), "Drama".to_string()] }, +//! Movie { id: 4, title: String::from("Mad Max"), genres: vec!["Adventure".to_string(), "Science Fiction".to_string()] }, +//! Movie { id: 5, title: String::from("Moana"), genres: vec!["Fantasy".to_string(), "Action".to_string()] }, +//! Movie { id: 6, title: String::from("Philadelphia"), genres: vec!["Drama".to_string()] }, +//! ], Some("id")).await.unwrap(); +//! # index.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); +//! } +//! ``` +//! +//! 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). +//! +//! ### Basic Search +//! +//! ``` +//! # use meilisearch_sdk::client::*; +//! # use serde::{Serialize, Deserialize}; +//! # #[derive(Serialize, Deserialize, Debug)] +//! # struct Movie { +//! # id: usize, +//! # title: String, +//! # genres: Vec, +//! # } +//! # fn main() { tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { +//! # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); +//! # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); +//! # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); +//! # let movies = client.create_index("movies_2", None).await.unwrap().wait_for_completion(&client, None, None).await.unwrap().try_make_index(&client).unwrap(); +//! // Meilisearch is typo-tolerant: +//! println!("{:?}", client.index("movies_2").search().with_query("caorl").execute::().await.unwrap().hits); +//! # movies.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); +//! # })} +//! ``` +//! +//! Output: +//! ```text +//! [Movie { id: 1, title: String::from("Carol"), genres: vec!["Romance", "Drama"] }] +//! ``` +//! +//! Json output: +//! ```json +//! { +//! "hits": [{ +//! "id": 1, +//! "title": "Carol", +//! "genres": ["Romance", "Drama"] +//! }], +//! "offset": 0, +//! "limit": 10, +//! "processingTimeMs": 1, +//! "query": "caorl" +//! } +//! ``` +//! +//! ### Custom Search +//! +//! ``` +//! # use meilisearch_sdk::{client::*, search::*}; +//! # use serde::{Serialize, Deserialize}; +//! # #[derive(Serialize, Deserialize, Debug)] +//! # struct Movie { +//! # id: usize, +//! # title: String, +//! # genres: Vec, +//! # } +//! # fn main() { tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { +//! # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); +//! # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); +//! # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); +//! # let movies = client.create_index("movies_3", None).await.unwrap().wait_for_completion(&client, None, None).await.unwrap().try_make_index(&client).unwrap(); +//! let search_result = client.index("movies_3") +//! .search() +//! .with_query("phil") +//! .with_attributes_to_highlight(Selectors::Some(&["*"])) +//! .execute::() +//! .await +//! .unwrap(); +//! println!("{:?}", search_result.hits); +//! # movies.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); +//! # })} +//! ``` +//! +//! Json output: +//! ```json +//! { +//! "hits": [ +//! { +//! "id": 6, +//! "title": "Philadelphia", +//! "_formatted": { +//! "id": 6, +//! "title": "Philadelphia", +//! "genre": ["Drama"] +//! } +//! } +//! ], +//! "offset": 0, +//! "limit": 20, +//! "processingTimeMs": 0, +//! "query": "phil" +//! } +//! ``` +//! +//! ### Custom Search With Filters +//! +//! If you want to enable filtering, you must add your attributes to the `filterableAttributes` +//! index setting. +//! +//! ``` +//! # use meilisearch_sdk::{client::*}; +//! # use serde::{Serialize, Deserialize}; +//! # fn main() { tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { +//! # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); +//! # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); +//! # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); +//! # let movies = client.create_index("movies_4", None).await.unwrap().wait_for_completion(&client, None, None).await.unwrap().try_make_index(&client).unwrap(); +//! let filterable_attributes = [ +//! "id", +//! "genres", +//! ]; +//! client.index("movies_4").set_filterable_attributes(&filterable_attributes).await.unwrap(); +//! # movies.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); +//! # })} +//! ``` +//! +//! You only need to perform this operation once. +//! +//! 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). +//! +//! Then, you can perform the search: +//! +//! ``` +//! # use meilisearch_sdk::{client::*, search::*}; +//! # use serde::{Serialize, Deserialize}; +//! # #[derive(Serialize, Deserialize, Debug)] +//! # struct Movie { +//! # id: usize, +//! # title: String, +//! # genres: Vec, +//! # } +//! # fn main() { tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { +//! # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); +//! # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); +//! # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); +//! # let movies = client.create_index("movies_5", None).await.unwrap().wait_for_completion(&client, None, None).await.unwrap().try_make_index(&client).unwrap(); +//! # let filterable_attributes = [ +//! # "id", +//! # "genres" +//! # ]; +//! # movies.set_filterable_attributes(&filterable_attributes).await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); +//! # movies.add_documents(&[ +//! # Movie { id: 1, title: String::from("Carol"), genres: vec!["Romance".to_string(), "Drama".to_string()] }, +//! # Movie { id: 2, title: String::from("Wonder Woman"), genres: vec!["Action".to_string(), "Adventure".to_string()] }, +//! # Movie { id: 3, title: String::from("Life of Pi"), genres: vec!["Adventure".to_string(), "Drama".to_string()] }, +//! # Movie { id: 4, title: String::from("Mad Max"), genres: vec!["Adventure".to_string(), "Science Fiction".to_string()] }, +//! # Movie { id: 5, title: String::from("Moana"), genres: vec!["Fantasy".to_string(), "Action".to_string()] }, +//! # Movie { id: 6, title: String::from("Philadelphia"), genres: vec!["Drama".to_string()] }, +//! # ], Some("id")).await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); +//! let search_result = client.index("movies_5") +//! .search() +//! .with_query("wonder") +//! .with_filter("id > 1 AND genres = Action") +//! .execute::() +//! .await +//! .unwrap(); +//! println!("{:?}", search_result.hits); +//! # movies.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); +//! # })} +//! ``` +//! +//! Json output: +//! ```json +//! { +//! "hits": [ +//! { +//! "id": 2, +//! "title": "Wonder Woman", +//! "genres": ["Action", "Adventure"] +//! } +//! ], +//! "offset": 0, +//! "limit": 20, +//! "estimatedTotalHits": 1, +//! "processingTimeMs": 0, +//! "query": "wonder" +//! } +//! ``` +//! +//! ### Customize the `HttpClient` +//! +//! By default, the SDK uses [`reqwest`](https://docs.rs/reqwest/latest/reqwest/) to make http calls. +//! The SDK lets you customize the http client by implementing the `HttpClient` trait yourself and +//! initializing the `Client` with the `new_with_client` method. +//! You may be interested by the `futures-unsend` feature which lets you specify a non-Send http client. +//! +//! ### Wasm support +//! +//! The SDK supports wasm through reqwest. You'll need to enable the `futures-unsend` feature while importing it, though. +#![warn(clippy::all)] +#![allow(clippy::needless_doctest_main)] + +/// Module containing the [`Client`] struct. +pub mod client; +/// Module representing the [documents] structures. +pub mod documents; +/// Module containing the [dumps] trait. +pub mod dumps; +/// Module containing the [`errors::Error`] struct. +pub mod errors; +/// Module related to runtime and instance features. +pub mod features; +/// Module containing the Index struct. +pub mod indexes; +/// Module containing the [`Key`] struct. +pub mod key; +pub mod request; +/// Module related to search queries and results. +pub mod search; +/// Module containing [`Settings`]. +pub mod settings; +/// Module containing the [snapshots] trait. +pub mod snapshots; +/// Module representing the [`TaskInfo`]s. +pub mod task_info; +/// Module representing the [`Task`]s. +pub mod tasks; +/// Module that generates tenant tokens. +#[cfg(not(target_arch = "wasm32"))] +mod tenant_tokens; +/// Module containing utilizes functions. +mod utils; + +#[cfg(feature = "reqwest")] +pub mod reqwest; + +#[cfg(feature = "reqwest")] +pub type DefaultHttpClient = reqwest::ReqwestClient; + +#[cfg(not(feature = "reqwest"))] +pub type DefaultHttpClient = std::convert::Infallible; + +#[cfg(test)] +/// Support for the `IndexConfig` derive proc macro in the crate's tests. +extern crate self as meilisearch_sdk; +/// 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. +#[doc(hidden)] +pub mod macro_helper { + pub use async_trait::async_trait; +} diff --git a/backend/vendor/meilisearch-sdk/src/request.rs b/backend/vendor/meilisearch-sdk/src/request.rs new file mode 100644 index 000000000..366e51b9a --- /dev/null +++ b/backend/vendor/meilisearch-sdk/src/request.rs @@ -0,0 +1,177 @@ +use std::convert::Infallible; + +use async_trait::async_trait; +use log::{error, trace, warn}; +use serde::{de::DeserializeOwned, Serialize}; +use serde_json::{from_str, to_vec}; + +use crate::errors::{Error, MeilisearchCommunicationError, MeilisearchError}; + +#[derive(Debug)] +pub enum Method { + Get { query: Q }, + Post { query: Q, body: B }, + Patch { query: Q, body: B }, + Put { query: Q, body: B }, + Delete { query: Q }, +} + +impl Method { + pub fn map_body(self, f: impl Fn(B) -> B2) -> Method { + match self { + Method::Get { query } => Method::Get { query }, + Method::Delete { query } => Method::Delete { query }, + Method::Post { query, body } => Method::Post { + query, + body: f(body), + }, + Method::Patch { query, body } => Method::Patch { + query, + body: f(body), + }, + Method::Put { query, body } => Method::Put { + query, + body: f(body), + }, + } + } + + pub fn query(&self) -> &Q { + match self { + Method::Get { query } => query, + Method::Delete { query } => query, + Method::Post { query, .. } => query, + Method::Put { query, .. } => query, + Method::Patch { query, .. } => query, + } + } + + pub fn body(&self) -> Option<&B> { + match self { + Method::Get { query: _ } | Method::Delete { query: _ } => None, + Method::Post { body, query: _ } => Some(body), + Method::Put { body, query: _ } => Some(body), + Method::Patch { body, query: _ } => Some(body), + } + } + + pub fn into_body(self) -> Option { + match self { + Method::Get { query: _ } | Method::Delete { query: _ } => None, + Method::Post { body, query: _ } => Some(body), + Method::Put { body, query: _ } => Some(body), + Method::Patch { body, query: _ } => Some(body), + } + } +} + +#[cfg_attr(feature = "futures-unsend", async_trait(?Send))] +#[cfg_attr(not(feature = "futures-unsend"), async_trait)] +pub trait HttpClient: Clone + Send + Sync { + async fn request( + &self, + url: &str, + method: Method, + expected_status_code: u16, + ) -> Result + where + Query: Serialize + Send + Sync, + Body: Serialize + Send + Sync, + Output: DeserializeOwned + 'static + Send, + { + use futures::io::Cursor; + + self.stream_request( + url, + method.map_body(|body| Cursor::new(to_vec(&body).unwrap())), + "application/json", + expected_status_code, + ) + .await + } + + async fn stream_request< + Query: Serialize + Send + Sync, + Body: futures_io::AsyncRead + Send + Sync + 'static, + Output: DeserializeOwned + 'static, + >( + &self, + url: &str, + method: Method, + content_type: &str, + expected_status_code: u16, + ) -> Result; +} + +pub fn parse_response( + status_code: u16, + expected_status_code: u16, + body: &str, + url: String, +) -> Result { + if status_code == expected_status_code { + return match from_str::(body) { + Ok(output) => { + trace!("Request succeed"); + Ok(output) + } + Err(e) => { + error!("Request succeeded but failed to parse response"); + Err(Error::ParseError(e)) + } + }; + } + + warn!( + "Expected response code {}, got {}", + expected_status_code, status_code + ); + + match from_str::(body) { + Ok(e) => Err(Error::from(e)), + Err(e) => { + if status_code >= 400 { + return Err(Error::MeilisearchCommunication( + MeilisearchCommunicationError { + status_code, + message: None, + url, + }, + )); + } + Err(Error::ParseError(e)) + } + } +} + +#[cfg_attr(feature = "futures-unsend", async_trait(?Send))] +#[cfg_attr(not(feature = "futures-unsend"), async_trait)] +impl HttpClient for Infallible { + async fn request( + &self, + _url: &str, + _method: Method, + _expected_status_code: u16, + ) -> Result + where + Query: Serialize + Send + Sync, + Body: Serialize + Send + Sync, + Output: DeserializeOwned + 'static + Send, + { + unreachable!() + } + + async fn stream_request< + Query: Serialize + Send + Sync, + Body: futures_io::AsyncRead + Send + Sync + 'static, + Output: DeserializeOwned + 'static, + >( + &self, + _url: &str, + _method: Method, + _content_type: &str, + _expected_status_code: u16, + ) -> Result { + unreachable!() + } +} diff --git a/backend/vendor/meilisearch-sdk/src/reqwest.rs b/backend/vendor/meilisearch-sdk/src/reqwest.rs new file mode 100644 index 000000000..26cf892aa --- /dev/null +++ b/backend/vendor/meilisearch-sdk/src/reqwest.rs @@ -0,0 +1,173 @@ +use std::{ + pin::Pin, + task::{Context, Poll}, +}; + +use async_trait::async_trait; +use bytes::{Bytes, BytesMut}; +use futures::{AsyncRead, Stream}; +use pin_project_lite::pin_project; +use serde::{de::DeserializeOwned, Serialize}; + +use crate::{ + errors::Error, + request::{parse_response, HttpClient, Method}, +}; + +#[derive(Debug, Clone, Default)] +pub struct ReqwestClient { + client: reqwest::Client, +} + +impl ReqwestClient { + pub fn new(api_key: Option<&str>) -> Result { + use reqwest::{header, ClientBuilder}; + + let builder = ClientBuilder::new(); + let mut headers = header::HeaderMap::new(); + #[cfg(not(target_arch = "wasm32"))] + headers.insert( + header::USER_AGENT, + header::HeaderValue::from_str(&qualified_version()).unwrap(), + ); + #[cfg(target_arch = "wasm32")] + headers.insert( + header::HeaderName::from_static("x-meilisearch-client"), + header::HeaderValue::from_str(&qualified_version()).unwrap(), + ); + + if let Some(api_key) = api_key { + headers.insert( + header::AUTHORIZATION, + header::HeaderValue::from_str(&format!("Bearer {api_key}")).unwrap(), + ); + } + + let builder = builder.default_headers(headers); + let client = builder.build()?; + + Ok(ReqwestClient { client }) + } +} + +#[cfg_attr(feature = "futures-unsend", async_trait(?Send))] +#[cfg_attr(not(feature = "futures-unsend"), async_trait)] +impl HttpClient for ReqwestClient { + async fn stream_request< + Query: Serialize + Send + Sync, + Body: futures_io::AsyncRead + Send + Sync + 'static, + Output: DeserializeOwned + 'static, + >( + &self, + url: &str, + method: Method, + content_type: &str, + expected_status_code: u16, + ) -> Result { + use reqwest::header; + + let query = method.query(); + let query = yaup::to_string(query)?; + + let url = if query.is_empty() { + url.to_string() + } else { + format!("{url}{query}") + }; + + let mut request = self.client.request(verb(&method), &url); + + if let Some(body) = method.into_body() { + // TODO: Currently reqwest doesn't support streaming data in wasm so we need to collect everything in RAM + #[cfg(not(target_arch = "wasm32"))] + { + let stream = ReaderStream::new(body); + let body = reqwest::Body::wrap_stream(stream); + + request = request + .header(header::CONTENT_TYPE, content_type) + .body(body); + } + #[cfg(target_arch = "wasm32")] + { + use futures::{pin_mut, AsyncReadExt}; + + let mut buf = Vec::new(); + pin_mut!(body); + body.read_to_end(&mut buf) + .await + .map_err(|err| Error::Other(Box::new(err)))?; + request = request.header(header::CONTENT_TYPE, content_type).body(buf); + } + } + + let response = self.client.execute(request.build()?).await?; + let status = response.status().as_u16(); + let mut body = response.text().await?; + + if body.is_empty() { + body = "null".to_string(); + } + + parse_response(status, expected_status_code, &body, url.to_string()) + } +} + +fn verb(method: &Method) -> reqwest::Method { + match method { + Method::Get { .. } => reqwest::Method::GET, + Method::Delete { .. } => reqwest::Method::DELETE, + Method::Post { .. } => reqwest::Method::POST, + Method::Put { .. } => reqwest::Method::PUT, + Method::Patch { .. } => reqwest::Method::PATCH, + } +} + +pub fn qualified_version() -> String { + const VERSION: Option<&str> = option_env!("CARGO_PKG_VERSION"); + + format!("Meilisearch Rust (v{})", VERSION.unwrap_or("unknown")) +} + +pin_project! { + #[derive(Debug)] + pub struct ReaderStream { + #[pin] + reader: R, + buf: BytesMut, + capacity: usize, + } +} + +impl ReaderStream { + pub fn new(reader: R) -> Self { + Self { + reader, + buf: BytesMut::new(), + // 8KiB of capacity, the default capacity used by `BufReader` in the std + capacity: 8 * 1024 * 1024, + } + } +} + +impl Stream for ReaderStream { + type Item = std::io::Result; + + fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + let this = self.as_mut().project(); + + if this.buf.capacity() == 0 { + this.buf.resize(*this.capacity, 0); + } + + match AsyncRead::poll_read(this.reader, cx, this.buf) { + Poll::Pending => Poll::Pending, + Poll::Ready(Err(err)) => Poll::Ready(Some(Err(err))), + Poll::Ready(Ok(0)) => Poll::Ready(None), + Poll::Ready(Ok(i)) => { + let chunk = this.buf.split_to(i); + Poll::Ready(Some(Ok(chunk.freeze()))) + } + } + } +} diff --git a/backend/vendor/meilisearch-sdk/src/search.rs b/backend/vendor/meilisearch-sdk/src/search.rs new file mode 100644 index 000000000..b1e548fc6 --- /dev/null +++ b/backend/vendor/meilisearch-sdk/src/search.rs @@ -0,0 +1,1406 @@ +use crate::{ + client::Client, errors::Error, indexes::Index, request::HttpClient, DefaultHttpClient, +}; +use either::Either; +use serde::{de::DeserializeOwned, Deserialize, Serialize, Serializer}; +use serde_json::{Map, Value}; +use std::collections::HashMap; + +#[derive(Deserialize, Debug, Eq, PartialEq, Clone)] +pub struct MatchRange { + pub start: usize, + pub length: usize, + + /// If the match is somewhere inside a (potentially nested) array, this + /// field is set to the index/indices of the matched element(s). + /// + /// In the simple case, if the field has the value `["foo", "bar"]`, then + /// searching for `ba` will return `indices: Some([1])`. If the value + /// contains multiple nested arrays, the first index describes the most + /// top-level array, and descending from there. For example, if the value is + /// `[{ x: "cat" }, "bear", { y: ["dog", "fox"] }]`, searching for `dog` + /// will return `indices: Some([2, 0])`. + pub indices: Option>, +} + +#[derive(Serialize, Debug, Eq, PartialEq, Clone)] +#[serde(transparent)] +pub struct Filter<'a> { + #[serde(with = "either::serde_untagged")] + inner: Either<&'a str, Vec<&'a str>>, +} + +impl<'a> Filter<'a> { + #[must_use] + pub fn new(inner: Either<&'a str, Vec<&'a str>>) -> Filter<'a> { + Filter { inner } + } +} + +#[derive(Debug, Clone, Serialize)] +pub enum MatchingStrategies { + #[serde(rename = "all")] + ALL, + #[serde(rename = "last")] + LAST, + #[serde(rename = "frequency")] + FREQUENCY, +} + +/// A single result. +/// +/// Contains the complete object, optionally the formatted object, and optionally an object that contains information about the matches. +#[derive(Deserialize, Debug, Clone)] +pub struct SearchResult { + /// The full result. + #[serde(flatten)] + pub result: T, + /// The formatted result. + #[serde(rename = "_formatted")] + pub formatted_result: Option>, + /// The object that contains information about the matches. + #[serde(rename = "_matchesPosition")] + pub matches_position: Option>>, + /// The relevancy score of the match. + #[serde(rename = "_rankingScore")] + pub ranking_score: Option, + #[serde(rename = "_rankingScoreDetails")] + pub ranking_score_details: Option>, + /// Only returned for federated multi search. + #[serde(rename = "_federation")] + pub federation: Option, +} + +#[derive(Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct FacetStats { + pub min: f64, + pub max: f64, +} + +#[derive(Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +/// A struct containing search results and other information about the search. +pub struct SearchResults { + /// Results of the query. + pub hits: Vec>, + /// Number of documents skipped. + pub offset: Option, + /// Number of results returned. + pub limit: Option, + /// Estimated total number of matches. + pub estimated_total_hits: Option, + // Current page number + pub page: Option, + // Maximum number of hits in a page. + pub hits_per_page: Option, + // Exhaustive number of matches. + pub total_hits: Option, + // Exhaustive number of pages. + pub total_pages: Option, + /// Distribution of the given facets. + pub facet_distribution: Option>>, + /// facet stats of the numerical facets requested in the `facet` search parameter. + pub facet_stats: Option>, + /// Processing time of the query. + pub processing_time_ms: usize, + /// Query originating the response. + pub query: String, + /// Index uid on which the search was made. + pub index_uid: Option, +} + +fn serialize_with_wildcard( + data: &Option>, + s: S, +) -> Result { + match data { + Some(Selectors::All) => ["*"].serialize(s), + Some(Selectors::Some(data)) => data.serialize(s), + None => s.serialize_none(), + } +} + +fn serialize_attributes_to_crop_with_wildcard( + data: &Option>, + s: S, +) -> Result { + match data { + Some(Selectors::All) => ["*"].serialize(s), + Some(Selectors::Some(data)) => { + let results = data + .iter() + .map(|(name, value)| { + let mut result = (*name).to_string(); + if let Some(value) = value { + result.push(':'); + result.push_str(value.to_string().as_str()); + } + result + }) + .collect::>(); + results.serialize(s) + } + None => s.serialize_none(), + } +} + +/// Some list fields in a `SearchQuery` can be set to a wildcard value. +/// +/// This structure allows you to choose between the wildcard value and an exhaustive list of selectors. +#[derive(Debug, Clone)] +pub enum Selectors { + /// A list of selectors. + Some(T), + /// The wildcard. + All, +} + +type AttributeToCrop<'a> = (&'a str, Option); + +/// A struct representing a query. +/// +/// You can add search parameters using the builder syntax. +/// +/// See [this page](https://www.meilisearch.com/docs/reference/api/search#query-q) for the official list and description of all parameters. +/// +/// # Examples +/// +/// ``` +/// # use serde::{Serialize, Deserialize}; +/// # use meilisearch_sdk::{client::Client, search::*, indexes::Index}; +/// # +/// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); +/// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); +/// # +/// #[derive(Serialize, Deserialize, Debug)] +/// struct Movie { +/// name: String, +/// description: String, +/// } +/// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { +/// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); +/// # let index = client +/// # .create_index("search_query_builder", None) +/// # .await +/// # .unwrap() +/// # .wait_for_completion(&client, None, None) +/// # .await.unwrap() +/// # .try_make_index(&client) +/// # .unwrap(); +/// +/// let mut res = SearchQuery::new(&index) +/// .with_query("space") +/// .with_offset(42) +/// .with_limit(21) +/// .execute::() +/// .await +/// .unwrap(); +/// +/// assert_eq!(res.limit, Some(21)); +/// # index.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); +/// # }); +/// ``` +/// +/// ``` +/// # use meilisearch_sdk::{client::Client, search::*, indexes::Index}; +/// # +/// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); +/// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); +/// # +/// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); +/// # let index = client.index("search_query_builder_build"); +/// let query = index.search() +/// .with_query("space") +/// .with_offset(42) +/// .with_limit(21) +/// .build(); // you can also execute() instead of build() +/// ``` +#[derive(Debug, Serialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct SearchQuery<'a, Http: HttpClient> { + #[serde(skip_serializing)] + index: &'a Index, + /// The text that will be searched for among the documents. + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(rename = "q")] + pub query: Option<&'a str>, + /// The number of documents to skip. + /// + /// If the value of the parameter `offset` is `n`, the `n` first documents (ordered by relevance) will not be returned. + /// This is helpful for pagination. + /// + /// Example: If you want to skip the first document, set offset to `1`. + #[serde(skip_serializing_if = "Option::is_none")] + pub offset: Option, + /// The maximum number of documents returned. + /// + /// If the value of the parameter `limit` is `n`, there will never be more than `n` documents in the response. + /// This is helpful for pagination. + /// + /// Example: If you don't want to get more than two documents, set limit to `2`. + /// + /// **Default: `20`** + #[serde(skip_serializing_if = "Option::is_none")] + pub limit: Option, + /// The page number on which you paginate. + /// + /// Pagination starts at 1. If page is 0, no results are returned. + /// + /// **Default: None unless `hits_per_page` is defined, in which case page is `1`** + #[serde(skip_serializing_if = "Option::is_none")] + pub page: Option, + /// The maximum number of results in a page. A page can contain less results than the number of hits_per_page. + /// + /// **Default: None unless `page` is defined, in which case `20`** + #[serde(skip_serializing_if = "Option::is_none")] + pub hits_per_page: Option, + /// Filter applied to documents. + /// + /// Read the [dedicated guide](https://www.meilisearch.com/docs/learn/advanced/filtering) to learn the syntax. + #[serde(skip_serializing_if = "Option::is_none")] + pub filter: Option>, + /// Facets for which to retrieve the matching count. + /// + /// Can be set to a [wildcard value](enum.Selectors.html#variant.All) that will select all existing attributes. + /// + /// **Default: all attributes found in the documents.** + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(serialize_with = "serialize_with_wildcard")] + pub facets: Option>, + /// Attributes to sort. + #[serde(skip_serializing_if = "Option::is_none")] + pub sort: Option<&'a [&'a str]>, + /// Attributes to perform the search on. + /// + /// Specify the subset of searchableAttributes for a search without modifying Meilisearch’s index settings. + /// + /// **Default: all searchable attributes found in the documents.** + #[serde(skip_serializing_if = "Option::is_none")] + pub attributes_to_search_on: Option<&'a [&'a str]>, + /// Attributes to display in the returned documents. + /// + /// Can be set to a [wildcard value](enum.Selectors.html#variant.All) that will select all existing attributes. + /// + /// **Default: all attributes found in the documents.** + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(serialize_with = "serialize_with_wildcard")] + pub attributes_to_retrieve: Option>, + /// Attributes whose values have to be cropped. + /// + /// Attributes are composed by the attribute name and an optional `usize` that overwrites the `crop_length` parameter. + /// + /// Can be set to a [wildcard value](enum.Selectors.html#variant.All) that will select all existing attributes. + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(serialize_with = "serialize_attributes_to_crop_with_wildcard")] + pub attributes_to_crop: Option]>>, + /// Maximum number of words including the matched query term(s) contained in the returned cropped value(s). + /// + /// See [attributes_to_crop](#structfield.attributes_to_crop). + /// + /// **Default: `10`** + #[serde(skip_serializing_if = "Option::is_none")] + pub crop_length: Option, + /// Marker at the start and the end of a cropped value. + /// + /// ex: `...middle of a crop...` + /// + /// **Default: `...`** + #[serde(skip_serializing_if = "Option::is_none")] + pub crop_marker: Option<&'a str>, + /// Attributes whose values will contain **highlighted matching terms**. + /// + /// Can be set to a [wildcard value](enum.Selectors.html#variant.All) that will select all existing attributes. + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(serialize_with = "serialize_with_wildcard")] + pub attributes_to_highlight: Option>, + /// Tag in front of a highlighted term. + /// + /// ex: `hello world` + /// + /// **Default: ``** + #[serde(skip_serializing_if = "Option::is_none")] + pub highlight_pre_tag: Option<&'a str>, + /// Tag after a highlighted term. + /// + /// ex: `hello world` + /// + /// **Default: ``** + #[serde(skip_serializing_if = "Option::is_none")] + pub highlight_post_tag: Option<&'a str>, + /// Defines whether an object that contains information about the matches should be returned or not. + /// + /// **Default: `false`** + #[serde(skip_serializing_if = "Option::is_none")] + pub show_matches_position: Option, + + /// Defines whether to show the relevancy score of the match. + /// + /// **Default: `false`** + #[serde(skip_serializing_if = "Option::is_none")] + pub show_ranking_score: Option, + + ///Adds a detailed global ranking score field to each document. + /// + /// **Default: `false`** + #[serde(skip_serializing_if = "Option::is_none")] + pub show_ranking_score_details: Option, + + /// Defines the strategy on how to handle queries containing multiple words. + #[serde(skip_serializing_if = "Option::is_none")] + pub matching_strategy: Option, + + ///Defines one attribute in the filterableAttributes list as a distinct attribute. + #[serde(skip_serializing_if = "Option::is_none")] + pub distinct: Option<&'a str>, + + ///Excludes results below the specified ranking score. + #[serde(skip_serializing_if = "Option::is_none")] + pub ranking_score_threshold: Option, + + /// Defines the language of the search query. + #[serde(skip_serializing_if = "Option::is_none")] + pub locales: Option<&'a [&'a str]>, + + #[serde(skip_serializing_if = "Option::is_none")] + pub(crate) index_uid: Option<&'a str>, + + #[serde(skip_serializing_if = "Option::is_none")] + pub(crate) federation_options: Option, +} + +#[derive(Debug, Serialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct QueryFederationOptions { + #[serde(skip_serializing_if = "Option::is_none")] + pub weight: Option, +} + +#[allow(missing_docs)] +impl<'a, Http: HttpClient> SearchQuery<'a, Http> { + #[must_use] + pub fn new(index: &'a Index) -> SearchQuery<'a, Http> { + SearchQuery { + index, + query: None, + offset: None, + limit: None, + page: None, + hits_per_page: None, + filter: None, + sort: None, + facets: None, + attributes_to_search_on: None, + attributes_to_retrieve: None, + attributes_to_crop: None, + crop_length: None, + crop_marker: None, + attributes_to_highlight: None, + highlight_pre_tag: None, + highlight_post_tag: None, + show_matches_position: None, + show_ranking_score: None, + show_ranking_score_details: None, + matching_strategy: None, + index_uid: None, + distinct: None, + ranking_score_threshold: None, + locales: None, + federation_options: None, + } + } + pub fn with_query<'b>(&'b mut self, query: &'a str) -> &'b mut SearchQuery<'a, Http> { + self.query = Some(query); + self + } + + pub fn with_offset<'b>(&'b mut self, offset: usize) -> &'b mut SearchQuery<'a, Http> { + self.offset = Some(offset); + self + } + pub fn with_limit<'b>(&'b mut self, limit: usize) -> &'b mut SearchQuery<'a, Http> { + self.limit = Some(limit); + self + } + /// Add the page number on which to paginate. + /// + /// # Example + /// + /// ``` + /// # use serde::{Serialize, Deserialize}; + /// # use meilisearch_sdk::{client::*, indexes::*, search::*}; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// # #[derive(Serialize, Deserialize, Debug)] + /// # struct Movie { + /// # name: String, + /// # description: String, + /// # } + /// # client.create_index("search_with_page", None).await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// let mut index = client.index("search_with_page"); + /// + /// let mut query = SearchQuery::new(&index); + /// query.with_query("").with_page(2); + /// let res = query.execute::().await.unwrap(); + /// # index.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// # }); + /// ``` + pub fn with_page<'b>(&'b mut self, page: usize) -> &'b mut SearchQuery<'a, Http> { + self.page = Some(page); + self + } + + /// Add the maximum number of results per page. + /// + /// # Example + /// + /// ``` + /// # use serde::{Serialize, Deserialize}; + /// # use meilisearch_sdk::{client::*, indexes::*, search::*}; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// # #[derive(Serialize, Deserialize, Debug)] + /// # struct Movie { + /// # name: String, + /// # description: String, + /// # } + /// # client.create_index("search_with_hits_per_page", None).await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// let mut index = client.index("search_with_hits_per_page"); + /// + /// let mut query = SearchQuery::new(&index); + /// query.with_query("").with_hits_per_page(2); + /// let res = query.execute::().await.unwrap(); + /// # index.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// # }); + /// ``` + pub fn with_hits_per_page<'b>( + &'b mut self, + hits_per_page: usize, + ) -> &'b mut SearchQuery<'a, Http> { + self.hits_per_page = Some(hits_per_page); + self + } + pub fn with_filter<'b>(&'b mut self, filter: &'a str) -> &'b mut SearchQuery<'a, Http> { + self.filter = Some(Filter::new(Either::Left(filter))); + self + } + pub fn with_array_filter<'b>( + &'b mut self, + filter: Vec<&'a str>, + ) -> &'b mut SearchQuery<'a, Http> { + self.filter = Some(Filter::new(Either::Right(filter))); + self + } + pub fn with_facets<'b>( + &'b mut self, + facets: Selectors<&'a [&'a str]>, + ) -> &'b mut SearchQuery<'a, Http> { + self.facets = Some(facets); + self + } + pub fn with_sort<'b>(&'b mut self, sort: &'a [&'a str]) -> &'b mut SearchQuery<'a, Http> { + self.sort = Some(sort); + self + } + + pub fn with_attributes_to_search_on<'b>( + &'b mut self, + attributes_to_search_on: &'a [&'a str], + ) -> &'b mut SearchQuery<'a, Http> { + self.attributes_to_search_on = Some(attributes_to_search_on); + self + } + pub fn with_attributes_to_retrieve<'b>( + &'b mut self, + attributes_to_retrieve: Selectors<&'a [&'a str]>, + ) -> &'b mut SearchQuery<'a, Http> { + self.attributes_to_retrieve = Some(attributes_to_retrieve); + self + } + pub fn with_attributes_to_crop<'b>( + &'b mut self, + attributes_to_crop: Selectors<&'a [(&'a str, Option)]>, + ) -> &'b mut SearchQuery<'a, Http> { + self.attributes_to_crop = Some(attributes_to_crop); + self + } + pub fn with_crop_length<'b>(&'b mut self, crop_length: usize) -> &'b mut SearchQuery<'a, Http> { + self.crop_length = Some(crop_length); + self + } + pub fn with_crop_marker<'b>( + &'b mut self, + crop_marker: &'a str, + ) -> &'b mut SearchQuery<'a, Http> { + self.crop_marker = Some(crop_marker); + self + } + pub fn with_attributes_to_highlight<'b>( + &'b mut self, + attributes_to_highlight: Selectors<&'a [&'a str]>, + ) -> &'b mut SearchQuery<'a, Http> { + self.attributes_to_highlight = Some(attributes_to_highlight); + self + } + pub fn with_highlight_pre_tag<'b>( + &'b mut self, + highlight_pre_tag: &'a str, + ) -> &'b mut SearchQuery<'a, Http> { + self.highlight_pre_tag = Some(highlight_pre_tag); + self + } + pub fn with_highlight_post_tag<'b>( + &'b mut self, + highlight_post_tag: &'a str, + ) -> &'b mut SearchQuery<'a, Http> { + self.highlight_post_tag = Some(highlight_post_tag); + self + } + pub fn with_show_matches_position<'b>( + &'b mut self, + show_matches_position: bool, + ) -> &'b mut SearchQuery<'a, Http> { + self.show_matches_position = Some(show_matches_position); + self + } + + pub fn with_show_ranking_score<'b>( + &'b mut self, + show_ranking_score: bool, + ) -> &'b mut SearchQuery<'a, Http> { + self.show_ranking_score = Some(show_ranking_score); + self + } + + pub fn with_show_ranking_score_details<'b>( + &'b mut self, + show_ranking_score_details: bool, + ) -> &'b mut SearchQuery<'a, Http> { + self.show_ranking_score_details = Some(show_ranking_score_details); + self + } + + pub fn with_matching_strategy<'b>( + &'b mut self, + matching_strategy: MatchingStrategies, + ) -> &'b mut SearchQuery<'a, Http> { + self.matching_strategy = Some(matching_strategy); + self + } + pub fn with_index_uid<'b>(&'b mut self) -> &'b mut SearchQuery<'a, Http> { + self.index_uid = Some(&self.index.uid); + self + } + pub fn with_distinct<'b>(&'b mut self, distinct: &'a str) -> &'b mut SearchQuery<'a, Http> { + self.distinct = Some(distinct); + self + } + pub fn with_ranking_score_threshold<'b>( + &'b mut self, + ranking_score_threshold: f64, + ) -> &'b mut SearchQuery<'a, Http> { + self.ranking_score_threshold = Some(ranking_score_threshold); + self + } + pub fn with_locales<'b>(&'b mut self, locales: &'a [&'a str]) -> &'b mut SearchQuery<'a, Http> { + self.locales = Some(locales); + self + } + /// Only usable in federated multi search queries. + pub fn with_federation_options<'b>( + &'b mut self, + federation_options: QueryFederationOptions, + ) -> &'b mut SearchQuery<'a, Http> { + self.federation_options = Some(federation_options); + self + } + pub fn build(&mut self) -> SearchQuery<'a, Http> { + self.clone() + } + /// Execute the query and fetch the results. + pub async fn execute( + &'a self, + ) -> Result, Error> { + self.index.execute_query::(self).await + } +} + +#[derive(Debug, Serialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct MultiSearchQuery<'a, 'b, Http: HttpClient = DefaultHttpClient> { + #[serde(skip_serializing)] + client: &'a Client, + // The weird `serialize = ""` is actually useful: without it, serde adds the + // bound `Http: Serialize` to the `Serialize` impl block, but that's not + // necessary. `SearchQuery` always implements `Serialize` (regardless of + // type parameter), so no bound is fine. + #[serde(bound(serialize = ""))] + pub queries: Vec>, +} + +#[allow(missing_docs)] +impl<'a, 'b, Http: HttpClient> MultiSearchQuery<'a, 'b, Http> { + #[must_use] + pub fn new(client: &'a Client) -> MultiSearchQuery<'a, 'b, Http> { + MultiSearchQuery { + client, + queries: Vec::new(), + } + } + pub fn with_search_query( + &mut self, + mut search_query: SearchQuery<'b, Http>, + ) -> &mut MultiSearchQuery<'a, 'b, Http> { + search_query.with_index_uid(); + self.queries.push(search_query); + self + } + /// Adds the `federation` parameter, making the search a federated search. + pub fn with_federation( + self, + federation: FederationOptions, + ) -> FederatedMultiSearchQuery<'a, 'b, Http> { + FederatedMultiSearchQuery { + client: self.client, + queries: self.queries, + federation: Some(federation), + } + } + + /// Execute the query and fetch the results. + pub async fn execute( + &'a self, + ) -> Result, Error> { + self.client.execute_multi_search_query::(self).await + } +} +#[derive(Debug, Clone, Deserialize)] +pub struct MultiSearchResponse { + pub results: Vec>, +} + +#[derive(Debug, Serialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct FederatedMultiSearchQuery<'a, 'b, Http: HttpClient = DefaultHttpClient> { + #[serde(skip_serializing)] + client: &'a Client, + #[serde(bound(serialize = ""))] + pub queries: Vec>, + pub federation: Option, +} + +/// The `federation` field of the multi search API. +/// See [the docs](https://www.meilisearch.com/docs/reference/api/multi_search#federation). +#[derive(Debug, Serialize, Clone, Default)] +#[serde(rename_all = "camelCase")] +pub struct FederationOptions { + #[serde(skip_serializing_if = "Option::is_none")] + pub offset: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub limit: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub facets_by_index: Option>>, + #[serde(skip_serializing_if = "Option::is_none")] + pub merge_facets: Option, +} + +#[allow(missing_docs)] +impl<'a, 'b, Http: HttpClient> FederatedMultiSearchQuery<'a, 'b, Http> { + /// Execute the query and fetch the results. + pub async fn execute( + &'a self, + ) -> Result, Error> { + self.client + .execute_federated_multi_search_query::(self) + .await + } +} + +/// Returned by federated multi search. +#[derive(Debug, Deserialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct FederatedMultiSearchResponse { + /// Merged results of the query. + pub hits: Vec>, + + // TODO: are offset, limit and estimated_total_hits really non-optional? In + // my tests they are always returned, but that's not a proof. + /// Number of documents skipped. + pub offset: usize, + /// Number of results returned. + pub limit: usize, + /// Estimated total number of matches. + pub estimated_total_hits: usize, + + /// Distribution of the given facets. + pub facet_distribution: Option>>, + /// facet stats of the numerical facets requested in the `facet` search parameter. + pub facet_stats: Option>, + /// Processing time of the query. + pub processing_time_ms: usize, +} + +/// Returned for each hit in `_federation` when doing federated multi search. +#[derive(Debug, Deserialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct FederationHitInfo { + pub index_uid: String, + pub queries_position: usize, + // TOOD: not mentioned in the docs, is that optional? + pub weighted_ranking_score: f32, +} + +#[cfg(test)] +mod tests { + use crate::{ + client::*, + key::{Action, KeyBuilder}, + search::*, + }; + use big_s::S; + use meilisearch_test_macro::meilisearch_test; + use serde::{Deserialize, Serialize}; + use serde_json::{json, Map, Value}; + + #[derive(Debug, Serialize, Deserialize, PartialEq)] + struct Nested { + child: String, + } + + #[derive(Debug, Serialize, Deserialize, PartialEq)] + struct Document { + id: usize, + value: String, + kind: String, + number: i32, + nested: Nested, + } + + impl PartialEq> for Document { + #[allow(clippy::cmp_owned)] + fn eq(&self, rhs: &Map) -> bool { + self.id.to_string() == rhs["id"] + && self.value == rhs["value"] + && self.kind == rhs["kind"] + } + } + + async fn setup_test_index(client: &Client, index: &Index) -> Result<(), Error> { + let t0 = index.add_documents(&[ + Document { id: 0, kind: "text".into(), number: 0, value: S("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."), nested: Nested { child: S("first") } }, + Document { id: 1, kind: "text".into(), number: 10, value: S("dolor sit amet, consectetur adipiscing elit"), nested: Nested { child: S("second") } }, + Document { id: 2, kind: "title".into(), number: 20, value: S("The Social Network"), nested: Nested { child: S("third") } }, + Document { id: 3, kind: "title".into(), number: 30, value: S("Harry Potter and the Sorcerer's Stone"), nested: Nested { child: S("fourth") } }, + Document { id: 4, kind: "title".into(), number: 40, value: S("Harry Potter and the Chamber of Secrets"), nested: Nested { child: S("fift") } }, + Document { id: 5, kind: "title".into(), number: 50, value: S("Harry Potter and the Prisoner of Azkaban"), nested: Nested { child: S("sixth") } }, + Document { id: 6, kind: "title".into(), number: 60, value: S("Harry Potter and the Goblet of Fire"), nested: Nested { child: S("seventh") } }, + Document { id: 7, kind: "title".into(), number: 70, value: S("Harry Potter and the Order of the Phoenix"), nested: Nested { child: S("eighth") } }, + Document { id: 8, kind: "title".into(), number: 80, value: S("Harry Potter and the Half-Blood Prince"), nested: Nested { child: S("ninth") } }, + Document { id: 9, kind: "title".into(), number: 90, value: S("Harry Potter and the Deathly Hallows"), nested: Nested { child: S("tenth") } }, + ], None).await?; + let t1 = index + .set_filterable_attributes(["kind", "value", "number"]) + .await?; + let t2 = index.set_sortable_attributes(["title"]).await?; + + t2.wait_for_completion(client, None, None).await?; + t1.wait_for_completion(client, None, None).await?; + t0.wait_for_completion(client, None, None).await?; + + Ok(()) + } + + #[meilisearch_test] + async fn test_multi_search(client: Client, index: Index) -> Result<(), Error> { + setup_test_index(&client, &index).await?; + let search_query_1 = SearchQuery::new(&index) + .with_query("Sorcerer's Stone") + .build(); + let search_query_2 = SearchQuery::new(&index) + .with_query("Chamber of Secrets") + .build(); + + let response = client + .multi_search() + .with_search_query(search_query_1) + .with_search_query(search_query_2) + .execute::() + .await + .unwrap(); + + assert_eq!(response.results.len(), 2); + Ok(()) + } + + #[meilisearch_test] + async fn test_query_builder(_client: Client, index: Index) -> Result<(), Error> { + let mut query = SearchQuery::new(&index); + query.with_query("space").with_offset(42).with_limit(21); + + let res = query.execute::().await.unwrap(); + + assert_eq!(res.query, S("space")); + assert_eq!(res.limit, Some(21)); + assert_eq!(res.offset, Some(42)); + assert_eq!(res.estimated_total_hits, Some(0)); + Ok(()) + } + + #[meilisearch_test] + async fn test_query_numbered_pagination(client: Client, index: Index) -> Result<(), Error> { + setup_test_index(&client, &index).await?; + + let mut query = SearchQuery::new(&index); + query.with_query("").with_page(2).with_hits_per_page(2); + + let res = query.execute::().await.unwrap(); + + assert_eq!(res.page, Some(2)); + assert_eq!(res.hits_per_page, Some(2)); + assert_eq!(res.total_hits, Some(10)); + assert_eq!(res.total_pages, Some(5)); + Ok(()) + } + + #[meilisearch_test] + async fn test_query_string(client: Client, index: Index) -> Result<(), Error> { + setup_test_index(&client, &index).await?; + + let results: SearchResults = index.search().with_query("dolor").execute().await?; + assert_eq!(results.hits.len(), 2); + Ok(()) + } + + #[meilisearch_test] + async fn test_query_string_on_nested_field(client: Client, index: Index) -> Result<(), Error> { + setup_test_index(&client, &index).await?; + + let results: SearchResults = + index.search().with_query("second").execute().await?; + + assert_eq!( + &Document { + id: 1, + value: S("dolor sit amet, consectetur adipiscing elit"), + kind: S("text"), + number: 10, + nested: Nested { child: S("second") } + }, + &results.hits[0].result + ); + + Ok(()) + } + + #[meilisearch_test] + async fn test_query_limit(client: Client, index: Index) -> Result<(), Error> { + setup_test_index(&client, &index).await?; + + let results: SearchResults = index.search().with_limit(5).execute().await?; + assert_eq!(results.hits.len(), 5); + Ok(()) + } + + #[meilisearch_test] + async fn test_query_page(client: Client, index: Index) -> Result<(), Error> { + setup_test_index(&client, &index).await?; + + let results: SearchResults = index.search().with_page(2).execute().await?; + assert_eq!(results.page, Some(2)); + assert_eq!(results.hits_per_page, Some(20)); + Ok(()) + } + + #[meilisearch_test] + async fn test_query_hits_per_page(client: Client, index: Index) -> Result<(), Error> { + setup_test_index(&client, &index).await?; + + let results: SearchResults = + index.search().with_hits_per_page(2).execute().await?; + assert_eq!(results.page, Some(1)); + assert_eq!(results.hits_per_page, Some(2)); + Ok(()) + } + + #[meilisearch_test] + async fn test_query_offset(client: Client, index: Index) -> Result<(), Error> { + setup_test_index(&client, &index).await?; + + let results: SearchResults = index.search().with_offset(6).execute().await?; + assert_eq!(results.hits.len(), 4); + Ok(()) + } + + #[meilisearch_test] + async fn test_query_filter(client: Client, index: Index) -> Result<(), Error> { + setup_test_index(&client, &index).await?; + + let results: SearchResults = index + .search() + .with_filter("value = \"The Social Network\"") + .execute() + .await?; + assert_eq!(results.hits.len(), 1); + + let results: SearchResults = index + .search() + .with_filter("NOT value = \"The Social Network\"") + .execute() + .await?; + assert_eq!(results.hits.len(), 9); + Ok(()) + } + + #[meilisearch_test] + async fn test_query_filter_with_array(client: Client, index: Index) -> Result<(), Error> { + setup_test_index(&client, &index).await?; + + let results: SearchResults = index + .search() + .with_array_filter(vec![ + "value = \"The Social Network\"", + "value = \"The Social Network\"", + ]) + .execute() + .await?; + assert_eq!(results.hits.len(), 1); + + Ok(()) + } + + #[meilisearch_test] + async fn test_query_facet_distribution(client: Client, index: Index) -> Result<(), Error> { + setup_test_index(&client, &index).await?; + + let mut query = SearchQuery::new(&index); + query.with_facets(Selectors::All); + let results: SearchResults = index.execute_query(&query).await?; + assert_eq!( + results + .facet_distribution + .unwrap() + .get("kind") + .unwrap() + .get("title") + .unwrap(), + &8 + ); + + let mut query = SearchQuery::new(&index); + query.with_facets(Selectors::Some(&["kind"])); + let results: SearchResults = index.execute_query(&query).await?; + assert_eq!( + results + .facet_distribution + .clone() + .unwrap() + .get("kind") + .unwrap() + .get("title") + .unwrap(), + &8 + ); + assert_eq!( + results + .facet_distribution + .unwrap() + .get("kind") + .unwrap() + .get("text") + .unwrap(), + &2 + ); + Ok(()) + } + + #[meilisearch_test] + async fn test_query_attributes_to_retrieve(client: Client, index: Index) -> Result<(), Error> { + setup_test_index(&client, &index).await?; + + let results: SearchResults = index + .search() + .with_attributes_to_retrieve(Selectors::All) + .execute() + .await?; + assert_eq!(results.hits.len(), 10); + + let mut query = SearchQuery::new(&index); + query.with_attributes_to_retrieve(Selectors::Some(&["kind", "id"])); // omit the "value" field + assert!(index.execute_query::(&query).await.is_err()); // error: missing "value" field + Ok(()) + } + + #[meilisearch_test] + async fn test_query_sort(client: Client, index: Index) -> Result<(), Error> { + setup_test_index(&client, &index).await?; + + let mut query = SearchQuery::new(&index); + query.with_query("harry potter"); + query.with_sort(&["title:desc"]); + let results: SearchResults = index.execute_query(&query).await?; + assert_eq!(results.hits.len(), 7); + Ok(()) + } + + #[meilisearch_test] + async fn test_query_attributes_to_crop(client: Client, index: Index) -> Result<(), Error> { + setup_test_index(&client, &index).await?; + + let mut query = SearchQuery::new(&index); + query.with_query("lorem ipsum"); + query.with_attributes_to_crop(Selectors::All); + let results: SearchResults = index.execute_query(&query).await?; + assert_eq!( + &Document { + id: 0, + value: S("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do…"), + kind: S("text"), + number: 0, + nested: Nested { child: S("first") } + }, + results.hits[0].formatted_result.as_ref().unwrap() + ); + + let mut query = SearchQuery::new(&index); + query.with_query("lorem ipsum"); + query.with_attributes_to_crop(Selectors::Some(&[("value", Some(5)), ("kind", None)])); + let results: SearchResults = index.execute_query(&query).await?; + assert_eq!( + &Document { + id: 0, + value: S("Lorem ipsum dolor sit amet…"), + kind: S("text"), + number: 0, + nested: Nested { child: S("first") } + }, + results.hits[0].formatted_result.as_ref().unwrap() + ); + Ok(()) + } + + #[meilisearch_test] + async fn test_query_crop_length(client: Client, index: Index) -> Result<(), Error> { + setup_test_index(&client, &index).await?; + + let mut query = SearchQuery::new(&index); + query.with_query("lorem ipsum"); + query.with_attributes_to_crop(Selectors::All); + query.with_crop_length(200); + let results: SearchResults = index.execute_query(&query).await?; + assert_eq!(&Document { + id: 0, + value: S("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."), + kind: S("text"), + number: 0, + nested: Nested { child: S("first") } + }, + results.hits[0].formatted_result.as_ref().unwrap()); + + let mut query = SearchQuery::new(&index); + query.with_query("lorem ipsum"); + query.with_attributes_to_crop(Selectors::All); + query.with_crop_length(5); + let results: SearchResults = index.execute_query(&query).await?; + assert_eq!( + &Document { + id: 0, + value: S("Lorem ipsum dolor sit amet…"), + kind: S("text"), + number: 0, + nested: Nested { child: S("first") } + }, + results.hits[0].formatted_result.as_ref().unwrap() + ); + Ok(()) + } + + #[meilisearch_test] + async fn test_query_customized_crop_marker(client: Client, index: Index) -> Result<(), Error> { + setup_test_index(&client, &index).await?; + + let mut query = SearchQuery::new(&index); + query.with_query("sed do eiusmod"); + query.with_attributes_to_crop(Selectors::All); + query.with_crop_length(6); + query.with_crop_marker("(ꈍᴗꈍ)"); + + let results: SearchResults = index.execute_query(&query).await?; + + assert_eq!( + &Document { + id: 0, + value: S("(ꈍᴗꈍ)sed do eiusmod tempor incididunt ut(ꈍᴗꈍ)"), + kind: S("text"), + number: 0, + nested: Nested { child: S("first") } + }, + results.hits[0].formatted_result.as_ref().unwrap() + ); + Ok(()) + } + + #[meilisearch_test] + async fn test_query_customized_highlight_pre_tag( + client: Client, + index: Index, + ) -> Result<(), Error> { + setup_test_index(&client, &index).await?; + + let mut query = SearchQuery::new(&index); + query.with_query("Social"); + query.with_attributes_to_highlight(Selectors::All); + query.with_highlight_pre_tag("(⊃。•́‿•̀。)⊃ "); + query.with_highlight_post_tag(" ⊂(´• ω •`⊂)"); + + let results: SearchResults = index.execute_query(&query).await?; + assert_eq!( + &Document { + id: 2, + value: S("The (⊃。•́‿•̀。)⊃ Social ⊂(´• ω •`⊂) Network"), + kind: S("title"), + number: 20, + nested: Nested { child: S("third") } + }, + results.hits[0].formatted_result.as_ref().unwrap() + ); + + Ok(()) + } + + #[meilisearch_test] + async fn test_query_attributes_to_highlight(client: Client, index: Index) -> Result<(), Error> { + setup_test_index(&client, &index).await?; + + let mut query = SearchQuery::new(&index); + query.with_query("dolor text"); + query.with_attributes_to_highlight(Selectors::All); + let results: SearchResults = index.execute_query(&query).await?; + assert_eq!( + &Document { + id: 1, + value: S("dolor sit amet, consectetur adipiscing elit"), + kind: S("text"), + number: 10, + nested: Nested { child: S("first") } + }, + results.hits[0].formatted_result.as_ref().unwrap(), + ); + + let mut query = SearchQuery::new(&index); + query.with_query("dolor text"); + query.with_attributes_to_highlight(Selectors::Some(&["value"])); + let results: SearchResults = index.execute_query(&query).await?; + assert_eq!( + &Document { + id: 1, + value: S("dolor sit amet, consectetur adipiscing elit"), + kind: S("text"), + number: 10, + nested: Nested { child: S("first") } + }, + results.hits[0].formatted_result.as_ref().unwrap() + ); + Ok(()) + } + + #[meilisearch_test] + async fn test_query_show_matches_position(client: Client, index: Index) -> Result<(), Error> { + setup_test_index(&client, &index).await?; + + let mut query = SearchQuery::new(&index); + query.with_query("dolor text"); + query.with_show_matches_position(true); + let results: SearchResults = index.execute_query(&query).await?; + assert_eq!(results.hits[0].matches_position.as_ref().unwrap().len(), 2); + assert_eq!( + results.hits[0] + .matches_position + .as_ref() + .unwrap() + .get("value") + .unwrap(), + &vec![MatchRange { + start: 0, + length: 5, + indices: None, + }] + ); + Ok(()) + } + + #[meilisearch_test] + async fn test_query_show_ranking_score(client: Client, index: Index) -> Result<(), Error> { + setup_test_index(&client, &index).await?; + + let mut query = SearchQuery::new(&index); + query.with_query("dolor text"); + query.with_show_ranking_score(true); + let results: SearchResults = index.execute_query(&query).await?; + assert!(results.hits[0].ranking_score.is_some()); + Ok(()) + } + + #[meilisearch_test] + async fn test_query_show_ranking_score_details( + client: Client, + index: Index, + ) -> Result<(), Error> { + setup_test_index(&client, &index).await?; + + let mut query = SearchQuery::new(&index); + query.with_query("dolor text"); + query.with_show_ranking_score_details(true); + let results: SearchResults = index.execute_query(&query).await.unwrap(); + assert!(results.hits[0].ranking_score_details.is_some()); + Ok(()) + } + + #[meilisearch_test] + async fn test_query_show_ranking_score_threshold( + client: Client, + index: Index, + ) -> Result<(), Error> { + setup_test_index(&client, &index).await?; + + let mut query = SearchQuery::new(&index); + query.with_query("dolor text"); + query.with_ranking_score_threshold(1.0); + let results: SearchResults = index.execute_query(&query).await.unwrap(); + assert!(results.hits.is_empty()); + Ok(()) + } + + #[meilisearch_test] + async fn test_query_locales(client: Client, index: Index) -> Result<(), Error> { + setup_test_index(&client, &index).await?; + + let mut query = SearchQuery::new(&index); + query.with_query("Harry Styles"); + query.with_locales(&["eng"]); + let results: SearchResults = index.execute_query(&query).await.unwrap(); + assert_eq!(results.hits.len(), 7); + Ok(()) + } + + #[meilisearch_test] + async fn test_phrase_search(client: Client, index: Index) -> Result<(), Error> { + setup_test_index(&client, &index).await?; + + let mut query = SearchQuery::new(&index); + query.with_query("harry \"of Fire\""); + let results: SearchResults = index.execute_query(&query).await?; + + assert_eq!(results.hits.len(), 1); + Ok(()) + } + + #[meilisearch_test] + async fn test_matching_strategy_all(client: Client, index: Index) -> Result<(), Error> { + setup_test_index(&client, &index).await?; + + let results = SearchQuery::new(&index) + .with_query("Harry Styles") + .with_matching_strategy(MatchingStrategies::ALL) + .execute::() + .await + .unwrap(); + + assert_eq!(results.hits.len(), 0); + Ok(()) + } + + #[meilisearch_test] + async fn test_matching_strategy_last(client: Client, index: Index) -> Result<(), Error> { + setup_test_index(&client, &index).await?; + + let results = SearchQuery::new(&index) + .with_query("Harry Styles") + .with_matching_strategy(MatchingStrategies::LAST) + .execute::() + .await + .unwrap(); + + assert_eq!(results.hits.len(), 7); + Ok(()) + } + + #[meilisearch_test] + async fn test_matching_strategy_frequency(client: Client, index: Index) -> Result<(), Error> { + setup_test_index(&client, &index).await?; + + let results = SearchQuery::new(&index) + .with_query("Harry Styles") + .with_matching_strategy(MatchingStrategies::FREQUENCY) + .execute::() + .await + .unwrap(); + + assert_eq!(results.hits.len(), 7); + Ok(()) + } + + #[meilisearch_test] + async fn test_distinct(client: Client, index: Index) -> Result<(), Error> { + setup_test_index(&client, &index).await?; + + let results = SearchQuery::new(&index) + .with_distinct("kind") + .execute::() + .await + .unwrap(); + + assert_eq!(results.hits.len(), 2); + Ok(()) + } + + #[meilisearch_test] + async fn test_generate_tenant_token_from_client( + client: Client, + index: Index, + ) -> Result<(), Error> { + setup_test_index(&client, &index).await?; + + let meilisearch_url = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + let key = KeyBuilder::new() + .with_action(Action::All) + .with_index("*") + .execute(&client) + .await + .unwrap(); + let allowed_client = Client::new(meilisearch_url, Some(key.key)).unwrap(); + + let search_rules = vec![ + json!({ "*": {}}), + json!({ "*": Value::Null }), + json!(["*"]), + json!({ "*": { "filter": "kind = text" } }), + json!([index.uid.to_string()]), + ]; + + for rules in search_rules { + let token = allowed_client + .generate_tenant_token(key.uid.clone(), rules, None, None) + .expect("Cannot generate tenant token."); + + let new_client = Client::new(meilisearch_url, Some(token.clone())).unwrap(); + + let result: SearchResults = new_client + .index(index.uid.to_string()) + .search() + .execute() + .await?; + + assert!(!result.hits.is_empty()); + } + + Ok(()) + } +} diff --git a/backend/vendor/meilisearch-sdk/src/settings.rs b/backend/vendor/meilisearch-sdk/src/settings.rs new file mode 100644 index 000000000..557226280 --- /dev/null +++ b/backend/vendor/meilisearch-sdk/src/settings.rs @@ -0,0 +1,2697 @@ +use crate::{ + errors::Error, + indexes::Index, + request::{HttpClient, Method}, + task_info::TaskInfo, +}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq, Copy)] +#[serde(rename_all = "camelCase")] +pub struct PaginationSetting { + pub max_total_hits: usize, +} + +#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct MinWordSizeForTypos { + pub one_typo: Option, + pub two_typos: Option, +} + +#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +#[serde(default)] +pub struct TypoToleranceSettings { + pub enabled: Option, + pub disable_on_attributes: Option>, + pub disable_on_words: Option>, + pub min_word_size_for_typos: Option, +} + +#[derive(Serialize, Deserialize, Default, Debug, Clone, Eq, PartialEq, Copy)] +#[serde(rename_all = "camelCase")] +pub struct FacetingSettings { + pub max_values_per_facet: usize, +} + +#[derive(Serialize, Deserialize, Default, Debug, Clone, Eq, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct LocalizedAttributes { + pub locales: Vec, + pub attribute_patterns: Vec, +} + +/// Struct reprensenting a set of settings. +/// +/// You can build this struct using the builder syntax. +/// +/// # Example +/// +/// ``` +/// # use meilisearch_sdk::settings::Settings; +/// let settings = Settings::new() +/// .with_stop_words(["a", "the", "of"]); +/// +/// // OR +/// +/// let stop_words: Vec = vec!["a".to_string(), "the".to_string(), "of".to_string()]; +/// let mut settings = Settings::new(); +/// settings.stop_words = Some(stop_words); +/// +/// // OR +/// +/// let stop_words: Vec = vec!["a".to_string(), "the".to_string(), "of".to_string()]; +/// let settings = Settings { +/// stop_words: Some(stop_words), +/// ..Settings::new() +/// }; +/// ``` +#[derive(Serialize, Deserialize, Default, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct Settings { + /// List of associated words treated similarly. + #[serde(skip_serializing_if = "Option::is_none")] + pub synonyms: Option>>, + /// List of words ignored by Meilisearch when present in search queries. + #[serde(skip_serializing_if = "Option::is_none")] + pub stop_words: Option>, + /// List of [ranking rules](https://www.meilisearch.com/docs/learn/core_concepts/relevancy#order-of-the-rules) sorted by order of importance. + #[serde(skip_serializing_if = "Option::is_none")] + pub ranking_rules: Option>, + /// Attributes to use for [filtering](https://www.meilisearch.com/docs/learn/advanced/filtering). + #[serde(skip_serializing_if = "Option::is_none")] + pub filterable_attributes: Option>, + /// Attributes to sort. + #[serde(skip_serializing_if = "Option::is_none")] + pub sortable_attributes: Option>, + /// Search returns documents with distinct (different) values of the given field. + #[serde(skip_serializing_if = "Option::is_none")] + pub distinct_attribute: Option>, + /// Fields in which to search for matching query words sorted by order of importance. + #[serde(skip_serializing_if = "Option::is_none")] + pub searchable_attributes: Option>, + /// Fields displayed in the returned documents. + #[serde(skip_serializing_if = "Option::is_none")] + pub displayed_attributes: Option>, + /// Pagination settings. + #[serde(skip_serializing_if = "Option::is_none")] + pub pagination: Option, + /// Faceting settings. + #[serde(skip_serializing_if = "Option::is_none")] + pub faceting: Option, + /// TypoTolerance settings + #[serde(skip_serializing_if = "Option::is_none")] + pub typo_tolerance: Option, + /// Dictionary settings. + #[serde(skip_serializing_if = "Option::is_none")] + pub dictionary: Option>, + /// Proximity precision settings. + #[serde(skip_serializing_if = "Option::is_none")] + pub proximity_precision: Option, + /// SearchCutoffMs settings. + #[serde(skip_serializing_if = "Option::is_none")] + pub search_cutoff_ms: Option, + /// Configure strings as custom separator tokens indicating where a word ends and begins. + #[serde(skip_serializing_if = "Option::is_none")] + pub separator_tokens: Option>, + /// Remove tokens from Meilisearch's default [list of word separators](https://www.meilisearch.com/docs/learn/engine/datatypes#string). + #[serde(skip_serializing_if = "Option::is_none")] + pub non_separator_tokens: Option>, + /// LocalizedAttributes settings. + #[serde(skip_serializing_if = "Option::is_none")] + pub localized_attributes: Option>, +} + +#[allow(missing_docs)] +impl Settings { + /// Create undefined settings. + #[must_use] + pub fn new() -> Settings { + Self::default() + } + + #[must_use] + pub fn with_synonyms(self, synonyms: HashMap) -> Settings + where + S: AsRef, + V: AsRef, + U: IntoIterator, + { + Settings { + synonyms: Some( + synonyms + .into_iter() + .map(|(key, value)| { + ( + key.as_ref().to_string(), + value.into_iter().map(|v| v.as_ref().to_string()).collect(), + ) + }) + .collect(), + ), + ..self + } + } + + #[must_use] + pub fn with_stop_words( + self, + stop_words: impl IntoIterator>, + ) -> Settings { + Settings { + stop_words: Some( + stop_words + .into_iter() + .map(|v| v.as_ref().to_string()) + .collect(), + ), + ..self + } + } + + #[must_use] + pub fn with_pagination(self, pagination_settings: PaginationSetting) -> Settings { + Settings { + pagination: Some(pagination_settings), + ..self + } + } + + #[must_use] + pub fn with_typo_tolerance(self, typo_tolerance_settings: TypoToleranceSettings) -> Settings { + Settings { + typo_tolerance: Some(typo_tolerance_settings), + ..self + } + } + + #[must_use] + pub fn with_ranking_rules( + self, + ranking_rules: impl IntoIterator>, + ) -> Settings { + Settings { + ranking_rules: Some( + ranking_rules + .into_iter() + .map(|v| v.as_ref().to_string()) + .collect(), + ), + ..self + } + } + + #[must_use] + pub fn with_filterable_attributes( + self, + filterable_attributes: impl IntoIterator>, + ) -> Settings { + Settings { + filterable_attributes: Some( + filterable_attributes + .into_iter() + .map(|v| v.as_ref().to_string()) + .collect(), + ), + ..self + } + } + + #[must_use] + pub fn with_sortable_attributes( + self, + sortable_attributes: impl IntoIterator>, + ) -> Settings { + Settings { + sortable_attributes: Some( + sortable_attributes + .into_iter() + .map(|v| v.as_ref().to_string()) + .collect(), + ), + ..self + } + } + + #[must_use] + pub fn with_distinct_attribute(self, distinct_attribute: Option>) -> Settings { + Settings { + distinct_attribute: Some( + distinct_attribute.map(|distinct| distinct.as_ref().to_string()), + ), + ..self + } + } + + #[must_use] + pub fn with_searchable_attributes( + self, + searchable_attributes: impl IntoIterator>, + ) -> Settings { + Settings { + searchable_attributes: Some( + searchable_attributes + .into_iter() + .map(|v| v.as_ref().to_string()) + .collect(), + ), + ..self + } + } + + #[must_use] + pub fn with_displayed_attributes( + self, + displayed_attributes: impl IntoIterator>, + ) -> Settings { + Settings { + displayed_attributes: Some( + displayed_attributes + .into_iter() + .map(|v| v.as_ref().to_string()) + .collect(), + ), + ..self + } + } + + #[must_use] + pub fn with_faceting(self, faceting: &FacetingSettings) -> Settings { + Settings { + faceting: Some(*faceting), + ..self + } + } + + #[must_use] + pub fn with_dictionary( + self, + dictionary: impl IntoIterator>, + ) -> Settings { + Settings { + dictionary: Some( + dictionary + .into_iter() + .map(|v| v.as_ref().to_string()) + .collect(), + ), + ..self + } + } + + pub fn with_proximity_precision(self, proximity_precision: impl AsRef) -> Settings { + Settings { + proximity_precision: Some(proximity_precision.as_ref().to_string()), + ..self + } + } + + pub fn with_search_cutoff(self, search_cutoff_ms: u64) -> Settings { + Settings { + search_cutoff_ms: Some(search_cutoff_ms), + ..self + } + } + + #[must_use] + pub fn with_separation_tokens( + self, + separator_tokens: impl IntoIterator>, + ) -> Settings { + Settings { + separator_tokens: Some( + separator_tokens + .into_iter() + .map(|v| v.as_ref().to_string()) + .collect(), + ), + ..self + } + } + + #[must_use] + pub fn with_non_separation_tokens( + self, + non_separator_tokens: impl IntoIterator>, + ) -> Settings { + Settings { + non_separator_tokens: Some( + non_separator_tokens + .into_iter() + .map(|v| v.as_ref().to_string()) + .collect(), + ), + ..self + } + } + + #[must_use] + pub fn with_localized_attributes( + self, + localized_attributes: impl IntoIterator, + ) -> Settings { + Settings { + localized_attributes: Some(localized_attributes.into_iter().collect()), + ..self + } + } +} + +impl Index { + /// Get [Settings] of the [Index]. + /// + /// # Example + /// + /// ``` + /// # use meilisearch_sdk::{client::*, indexes::*}; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// # client.create_index("get_settings", None).await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// let index = client.index("get_settings"); + /// + /// let settings = index.get_settings().await.unwrap(); + /// # index.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// # }); + /// ``` + pub async fn get_settings(&self) -> Result { + self.client + .http_client + .request::<(), (), Settings>( + &format!("{}/indexes/{}/settings", self.client.host, self.uid), + Method::Get { query: () }, + 200, + ) + .await + } + + /// Get [synonyms](https://www.meilisearch.com/docs/reference/api/settings#get-synonyms) of the [Index]. + /// + /// # Example + /// + /// + /// ``` + /// # use meilisearch_sdk::{client::*, indexes::*}; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// # client.create_index("get_synonyms", None).await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// let index = client.index("get_synonyms"); + /// + /// let synonyms = index.get_synonyms().await.unwrap(); + /// # index.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// # }); + /// ``` + pub async fn get_synonyms(&self) -> Result>, Error> { + self.client + .http_client + .request::<(), (), HashMap>>( + &format!( + "{}/indexes/{}/settings/synonyms", + self.client.host, self.uid + ), + Method::Get { query: () }, + 200, + ) + .await + } + + /// Get [pagination](https://www.meilisearch.com/docs/reference/api/settings#pagination) of the [Index]. + /// + /// # Example + /// + /// + /// ``` + /// # use meilisearch_sdk::{client::*, indexes::*}; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// # client.create_index("get_pagination", None).await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// let index = client.index("get_pagination"); + /// + /// let pagination = index.get_pagination().await.unwrap(); + /// # index.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// # }); + /// ``` + pub async fn get_pagination(&self) -> Result { + self.client + .http_client + .request::<(), (), PaginationSetting>( + &format!( + "{}/indexes/{}/settings/pagination", + self.client.host, self.uid + ), + Method::Get { query: () }, + 200, + ) + .await + } + + /// Get [stop-words](https://www.meilisearch.com/docs/reference/api/settings#stop-words) of the [Index]. + /// + /// # Example + /// + /// ``` + /// # use meilisearch_sdk::{client::*, indexes::*}; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// # client.create_index("get_stop_words", None).await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// let index = client.index("get_stop_words"); + /// + /// let stop_words = index.get_stop_words().await.unwrap(); + /// # index.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// # }); + /// ``` + pub async fn get_stop_words(&self) -> Result, Error> { + self.client + .http_client + .request::<(), (), Vec>( + &format!( + "{}/indexes/{}/settings/stop-words", + self.client.host, self.uid + ), + Method::Get { query: () }, + 200, + ) + .await + } + + /// Get [ranking rules](https://www.meilisearch.com/docs/reference/api/settings#ranking-rules) of the [Index]. + /// + /// # Example + /// + /// + /// ``` + /// # use meilisearch_sdk::{client::*, indexes::*}; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// # client.create_index("get_ranking_rules", None).await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// let index = client.index("get_ranking_rules"); + /// + /// let ranking_rules = index.get_ranking_rules().await.unwrap(); + /// # index.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// # }); + /// ``` + pub async fn get_ranking_rules(&self) -> Result, Error> { + self.client + .http_client + .request::<(), (), Vec>( + &format!( + "{}/indexes/{}/settings/ranking-rules", + self.client.host, self.uid + ), + Method::Get { query: () }, + 200, + ) + .await + } + + /// Get [filterable attributes](https://www.meilisearch.com/docs/reference/api/settings#filterable-attributes) of the [Index]. + /// + /// # Example + /// + /// + /// ``` + /// # use meilisearch_sdk::{client::*, indexes::*}; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// # client.create_index("get_filterable_attributes", None).await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// let index = client.index("get_filterable_attributes"); + /// + /// let filterable_attributes = index.get_filterable_attributes().await.unwrap(); + /// # index.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// # }); + /// ``` + pub async fn get_filterable_attributes(&self) -> Result, Error> { + self.client + .http_client + .request::<(), (), Vec>( + &format!( + "{}/indexes/{}/settings/filterable-attributes", + self.client.host, self.uid + ), + Method::Get { query: () }, + 200, + ) + .await + } + + /// Get [sortable attributes](https://www.meilisearch.com/docs/reference/api/settings#sortable-attributes) of the [Index]. + /// + /// # Example + /// + /// + /// ``` + /// # use meilisearch_sdk::{client::*, indexes::*}; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// # client.create_index("get_sortable_attributes", None).await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// let index = client.index("get_sortable_attributes"); + /// + /// let sortable_attributes = index.get_sortable_attributes().await.unwrap(); + /// # index.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// # }); + /// ``` + pub async fn get_sortable_attributes(&self) -> Result, Error> { + self.client + .http_client + .request::<(), (), Vec>( + &format!( + "{}/indexes/{}/settings/sortable-attributes", + self.client.host, self.uid + ), + Method::Get { query: () }, + 200, + ) + .await + } + + /// Get the [distinct attribute](https://www.meilisearch.com/docs/reference/api/settings#distinct-attribute) of the [Index]. + /// + /// # Example + /// + /// + /// ``` + /// # use meilisearch_sdk::{client::*, indexes::*}; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// # client.create_index("get_distinct_attribute", None).await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// let index = client.index("get_distinct_attribute"); + /// + /// let distinct_attribute = index.get_distinct_attribute().await.unwrap(); + /// # index.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// # }); + /// ``` + pub async fn get_distinct_attribute(&self) -> Result, Error> { + self.client + .http_client + .request::<(), (), Option>( + &format!( + "{}/indexes/{}/settings/distinct-attribute", + self.client.host, self.uid + ), + Method::Get { query: () }, + 200, + ) + .await + } + + /// Get [searchable attributes](https://www.meilisearch.com/docs/reference/api/settings#searchable-attributes) of the [Index]. + /// + /// # Example + /// + /// + /// ``` + /// # use meilisearch_sdk::{client::*, indexes::*}; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// # client.create_index("get_searchable_attributes", None).await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// let index = client.index("get_searchable_attributes"); + /// + /// let searchable_attributes = index.get_searchable_attributes().await.unwrap(); + /// # index.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// # }); + /// ``` + pub async fn get_searchable_attributes(&self) -> Result, Error> { + self.client + .http_client + .request::<(), (), Vec>( + &format!( + "{}/indexes/{}/settings/searchable-attributes", + self.client.host, self.uid + ), + Method::Get { query: () }, + 200, + ) + .await + } + + /// Get [displayed attributes](https://www.meilisearch.com/docs/reference/api/settings#displayed-attributes) of the [Index]. + /// + /// # Example + /// + /// + /// ``` + /// # use meilisearch_sdk::{client::*, indexes::*}; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// # client.create_index("get_displayed_attributes", None).await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// let index = client.index("get_displayed_attributes"); + /// + /// let displayed_attributes = index.get_displayed_attributes().await.unwrap(); + /// # index.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// # }); + /// ``` + pub async fn get_displayed_attributes(&self) -> Result, Error> { + self.client + .http_client + .request::<(), (), Vec>( + &format!( + "{}/indexes/{}/settings/displayed-attributes", + self.client.host, self.uid + ), + Method::Get { query: () }, + 200, + ) + .await + } + + /// Get [faceting](https://www.meilisearch.com/docs/reference/api/settings#faceting) settings of the [Index]. + /// + /// # Example + /// + /// + /// ``` + /// # use meilisearch_sdk::{client::*, indexes::*}; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// # client.create_index("get_faceting", None).await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// let index = client.index("get_faceting"); + /// + /// let faceting = index.get_faceting().await.unwrap(); + /// # index.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// # }); + /// ``` + pub async fn get_faceting(&self) -> Result { + self.client + .http_client + .request::<(), (), FacetingSettings>( + &format!( + "{}/indexes/{}/settings/faceting", + self.client.host, self.uid + ), + Method::Get { query: () }, + 200, + ) + .await + } + + /// Get [dictionary](https://www.meilisearch.com/docs/reference/api/settings#dictionary) of the [Index]. + /// + /// # Example + /// + /// ``` + /// # use meilisearch_sdk::{client::*, indexes::*}; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// # client.create_index("get_dictionary", None).await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// let index = client.index("get_dictionary"); + /// + /// let dictionary = index.get_dictionary().await.unwrap(); + /// # index.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// # }); + /// ``` + pub async fn get_dictionary(&self) -> Result, Error> { + self.client + .http_client + .request::<(), (), Vec>( + &format!( + "{}/indexes/{}/settings/dictionary", + self.client.host, self.uid + ), + Method::Get { query: () }, + 200, + ) + .await + } + + /// Get [proximity_precision](https://www.meilisearch.com/docs/reference/api/settings#proximity-precision) of the [Index]. + /// + /// # Example + /// + /// ``` + /// # use meilisearch_sdk::{client::*, indexes::*}; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// # client.create_index("get_proximity_precision", None).await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// let index = client.index("get_proximity_precision"); + /// + /// let proximity_precision = index.get_proximity_precision().await.unwrap(); + /// # index.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// # }); + /// ``` + pub async fn get_proximity_precision(&self) -> Result { + self.client + .http_client + .request::<(), (), String>( + &format!( + "{}/indexes/{}/settings/proximity-precision", + self.client.host, self.uid + ), + Method::Get { query: () }, + 200, + ) + .await + } + + /// Get [typo tolerance](https://www.meilisearch.com/docs/learn/configuration/typo_tolerance#typo-tolerance) of the [Index]. + /// + /// ``` + /// # use meilisearch_sdk::{client::*, indexes::*}; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// # client.create_index("get_typo_tolerance", None).await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// let index = client.index("get_typo_tolerance"); + /// + /// let typo_tolerance = index.get_typo_tolerance().await.unwrap(); + /// # index.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// # }); + /// ``` + pub async fn get_typo_tolerance(&self) -> Result { + self.client + .http_client + .request::<(), (), TypoToleranceSettings>( + &format!( + "{}/indexes/{}/settings/typo-tolerance", + self.client.host, self.uid + ), + Method::Get { query: () }, + 200, + ) + .await + } + + /// Get [search cutoff](https://www.meilisearch.com/docs/reference/api/settings#search-cutoff) settings of the [Index]. + /// + /// # Example + /// + /// ``` + /// # use meilisearch_sdk::{client::*, indexes::*, settings::Settings}; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// # client.create_index("get_search_cutoff_ms", None).await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// let mut index = client.index("get_search_cutoff_ms"); + /// + /// let task = index.get_search_cutoff_ms().await.unwrap(); + /// # index.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// # }); + /// ``` + pub async fn get_search_cutoff_ms(&self) -> Result, Error> { + self.client + .http_client + .request::<(), (), Option>( + &format!( + "{}/indexes/{}/settings/search-cutoff-ms", + self.client.host, self.uid + ), + Method::Get { query: () }, + 200, + ) + .await + } + + /// Get [separator token](https://www.meilisearch.com/docs/reference/api/settings#separator-tokens) of the [Index]. + /// + /// ``` + /// # use meilisearch_sdk::{client::*, indexes::*}; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// # client.create_index("get_separator_tokens", None).await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// let index = client.index("get_separator_tokens"); + /// + /// let separator_tokens = index.get_separator_tokens().await.unwrap(); + /// # index.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// # }); + /// ``` + pub async fn get_separator_tokens(&self) -> Result, Error> { + self.client + .http_client + .request::<(), (), Vec>( + &format!( + "{}/indexes/{}/settings/separator-tokens", + self.client.host, self.uid + ), + Method::Get { query: () }, + 200, + ) + .await + } + + /// Get [non separator token](https://www.meilisearch.com/docs/reference/api/settings#non-separator-tokens) of the [Index]. + /// + /// ``` + /// # use meilisearch_sdk::{client::*, indexes::*}; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// # client.create_index("get_non_separator_tokens", None).await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// let index = client.index("get_non_separator_tokens"); + /// + /// let non_separator_tokens = index.get_non_separator_tokens().await.unwrap(); + /// # index.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// # }); + /// ``` + pub async fn get_non_separator_tokens(&self) -> Result, Error> { + self.client + .http_client + .request::<(), (), Vec>( + &format!( + "{}/indexes/{}/settings/non-separator-tokens", + self.client.host, self.uid + ), + Method::Get { query: () }, + 200, + ) + .await + } + + /// Get [localized attributes](https://www.meilisearch.com/docs/reference/api/settings#localized-attributes-object) settings of the [Index]. + /// + /// ``` + /// # use meilisearch_sdk::{client::*, indexes::*, settings::LocalizedAttributes}; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// # client.create_index("get_localized_attributes", None).await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// let index = client.index("get_localized_attributes"); + /// + /// let localized_attributes = index.get_localized_attributes().await.unwrap(); + /// # index.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// # }); + /// ``` + pub async fn get_localized_attributes( + &self, + ) -> Result>, Error> { + self.client + .http_client + .request::<(), (), Option>>( + &format!( + "{}/indexes/{}/settings/localized-attributes", + self.client.host, self.uid + ), + Method::Get { query: () }, + 200, + ) + .await + } + + /// Update [settings](../settings/struct.Settings) of the [Index]. + /// + /// Updates in the settings are partial. This means that any parameters corresponding to a `None` value will be left unchanged. + /// + /// # Example + /// + /// ``` + /// # use meilisearch_sdk::{client::*, indexes::*, settings::*}; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// # client.create_index("set_settings", None).await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// let mut index = client.index("set_settings"); + /// + /// let stop_words = vec![String::from("a"), String::from("the"), String::from("of")]; + /// let settings = Settings::new() + /// .with_stop_words(stop_words.clone()) + /// .with_pagination(PaginationSetting {max_total_hits: 100} + /// ); + /// + /// let task = index.set_settings(&settings).await.unwrap(); + /// # index.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// # }); + /// ``` + pub async fn set_settings(&self, settings: &Settings) -> Result { + self.client + .http_client + .request::<(), &Settings, TaskInfo>( + &format!("{}/indexes/{}/settings", self.client.host, self.uid), + Method::Patch { + query: (), + body: settings, + }, + 202, + ) + .await + } + + /// Update [synonyms](https://www.meilisearch.com/docs/reference/api/settings#synonyms) of the [Index]. + /// + /// # Example + /// + /// ``` + /// # use meilisearch_sdk::{client::*, indexes::*, settings::Settings}; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// # client.create_index("set_synonyms", None).await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// let mut index = client.index("set_synonyms"); + /// + /// let mut synonyms = std::collections::HashMap::new(); + /// synonyms.insert(String::from("wolverine"), vec![String::from("xmen"), String::from("logan")]); + /// synonyms.insert(String::from("logan"), vec![String::from("xmen"), String::from("wolverine")]); + /// synonyms.insert(String::from("wow"), vec![String::from("world of warcraft")]); + /// + /// let task = index.set_synonyms(&synonyms).await.unwrap(); + /// # index.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// # }); + /// ``` + pub async fn set_synonyms( + &self, + synonyms: &HashMap>, + ) -> Result { + self.client + .http_client + .request::<(), &HashMap>, TaskInfo>( + &format!( + "{}/indexes/{}/settings/synonyms", + self.client.host, self.uid + ), + Method::Put { + query: (), + body: synonyms, + }, + 202, + ) + .await + } + + /// Update [pagination](https://www.meilisearch.com/docs/reference/api/settings#pagination) of the [Index]. + /// + /// # Example + /// + /// ``` + /// # use meilisearch_sdk::{client::*, indexes::*, settings::*}; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// # client.create_index("set_pagination", None).await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// let mut index = client.index("set_pagination"); + /// + /// let pagination = PaginationSetting {max_total_hits:100}; + /// let task = index.set_pagination(pagination).await.unwrap(); + /// # index.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// # }); + /// ``` + pub async fn set_pagination(&self, pagination: PaginationSetting) -> Result { + self.client + .http_client + .request::<(), &PaginationSetting, TaskInfo>( + &format!( + "{}/indexes/{}/settings/pagination", + self.client.host, self.uid + ), + Method::Patch { + query: (), + body: &pagination, + }, + 202, + ) + .await + } + + /// Update [stop-words](https://www.meilisearch.com/docs/reference/api/settings#stop-words) of the [Index]. + /// + /// # Example + /// + /// ``` + /// # use meilisearch_sdk::{client::*, indexes::*, settings::Settings}; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// # client.create_index("set_stop_words", None).await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// let mut index = client.index("set_stop_words"); + /// + /// let stop_words = ["the", "of", "to"]; + /// let task = index.set_stop_words(&stop_words).await.unwrap(); + /// # index.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// # }); + /// ``` + pub async fn set_stop_words( + &self, + stop_words: impl IntoIterator>, + ) -> Result { + self.client + .http_client + .request::<(), Vec, TaskInfo>( + &format!( + "{}/indexes/{}/settings/stop-words", + self.client.host, self.uid + ), + Method::Put { + query: (), + body: stop_words + .into_iter() + .map(|v| v.as_ref().to_string()) + .collect(), + }, + 202, + ) + .await + } + + /// Update [ranking rules](https://www.meilisearch.com/docs/reference/api/settings#ranking-rules) of the [Index]. + /// + /// # Example + /// + /// ``` + /// # use meilisearch_sdk::{client::*, indexes::*, settings::Settings}; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// # client.create_index("set_ranking_rules", None).await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// let mut index = client.index("set_ranking_rules"); + /// + /// let ranking_rules = [ + /// "words", + /// "typo", + /// "proximity", + /// "attribute", + /// "sort", + /// "exactness", + /// "release_date:asc", + /// "rank:desc", + /// ]; + /// let task = index.set_ranking_rules(ranking_rules).await.unwrap(); + /// # index.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// # }); + /// ``` + pub async fn set_ranking_rules( + &self, + ranking_rules: impl IntoIterator>, + ) -> Result { + self.client + .http_client + .request::<(), Vec, TaskInfo>( + &format!( + "{}/indexes/{}/settings/ranking-rules", + self.client.host, self.uid + ), + Method::Put { + query: (), + body: ranking_rules + .into_iter() + .map(|v| v.as_ref().to_string()) + .collect(), + }, + 202, + ) + .await + } + + /// Update [filterable attributes](https://www.meilisearch.com/docs/reference/api/settings#filterable-attributes) of the [Index]. + /// + /// # Example + /// + /// ``` + /// # use meilisearch_sdk::{client::*, indexes::*, settings::Settings}; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// # client.create_index("set_filterable_attributes", None).await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// let mut index = client.index("set_filterable_attributes"); + /// + /// let filterable_attributes = ["genre", "director"]; + /// let task = index.set_filterable_attributes(&filterable_attributes).await.unwrap(); + /// # index.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// # }); + /// ``` + pub async fn set_filterable_attributes( + &self, + filterable_attributes: impl IntoIterator>, + ) -> Result { + self.client + .http_client + .request::<(), Vec, TaskInfo>( + &format!( + "{}/indexes/{}/settings/filterable-attributes", + self.client.host, self.uid + ), + Method::Put { + query: (), + body: filterable_attributes + .into_iter() + .map(|v| v.as_ref().to_string()) + .collect(), + }, + 202, + ) + .await + } + + /// Update [sortable attributes](https://www.meilisearch.com/docs/reference/api/settings#sortable-attributes) of the [Index]. + /// + /// # Example + /// + /// ``` + /// # use meilisearch_sdk::{client::*, indexes::*, settings::Settings}; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// # client.create_index("set_sortable_attributes", None).await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// let mut index = client.index("set_sortable_attributes"); + /// + /// let sortable_attributes = ["genre", "director"]; + /// let task = index.set_sortable_attributes(&sortable_attributes).await.unwrap(); + /// # index.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// # }); + /// ``` + pub async fn set_sortable_attributes( + &self, + sortable_attributes: impl IntoIterator>, + ) -> Result { + self.client + .http_client + .request::<(), Vec, TaskInfo>( + &format!( + "{}/indexes/{}/settings/sortable-attributes", + self.client.host, self.uid + ), + Method::Put { + query: (), + body: sortable_attributes + .into_iter() + .map(|v| v.as_ref().to_string()) + .collect(), + }, + 202, + ) + .await + } + + /// Update the [distinct attribute](https://www.meilisearch.com/docs/reference/api/settings#distinct-attribute) of the [Index]. + /// + /// # Example + /// + /// ``` + /// # use meilisearch_sdk::{client::*, indexes::*, settings::Settings}; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// # client.create_index("set_distinct_attribute", None).await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// let mut index = client.index("set_distinct_attribute"); + /// + /// let task = index.set_distinct_attribute("movie_id").await.unwrap(); + /// # index.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// # }); + /// ``` + pub async fn set_distinct_attribute( + &self, + distinct_attribute: impl AsRef, + ) -> Result { + self.client + .http_client + .request::<(), String, TaskInfo>( + &format!( + "{}/indexes/{}/settings/distinct-attribute", + self.client.host, self.uid + ), + Method::Put { + query: (), + body: distinct_attribute.as_ref().to_string(), + }, + 202, + ) + .await + } + + /// Update [searchable attributes](https://www.meilisearch.com/docs/reference/api/settings#searchable-attributes) of the [Index]. + /// + /// # Example + /// + /// ``` + /// # use meilisearch_sdk::{client::*, indexes::*, settings::Settings}; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// # client.create_index("set_searchable_attributes", None).await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// let mut index = client.index("set_searchable_attributes"); + /// + /// let task = index.set_searchable_attributes(["title", "description", "uid"]).await.unwrap(); + /// # index.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// # }); + /// ``` + pub async fn set_searchable_attributes( + &self, + searchable_attributes: impl IntoIterator>, + ) -> Result { + self.client + .http_client + .request::<(), Vec, TaskInfo>( + &format!( + "{}/indexes/{}/settings/searchable-attributes", + self.client.host, self.uid + ), + Method::Put { + query: (), + body: searchable_attributes + .into_iter() + .map(|v| v.as_ref().to_string()) + .collect(), + }, + 202, + ) + .await + } + + /// Update [displayed attributes](https://www.meilisearch.com/docs/reference/api/settings#displayed-attributes) of the [Index]. + /// + /// # Example + /// + /// ``` + /// # use meilisearch_sdk::{client::*, indexes::*, settings::Settings}; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// # client.create_index("set_displayed_attributes", None).await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// let mut index = client.index("set_displayed_attributes"); + /// + /// let task = index.set_displayed_attributes(["title", "description", "release_date", "rank", "poster"]).await.unwrap(); + /// # index.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// # }); + /// ``` + pub async fn set_displayed_attributes( + &self, + displayed_attributes: impl IntoIterator>, + ) -> Result { + self.client + .http_client + .request::<(), Vec, TaskInfo>( + &format!( + "{}/indexes/{}/settings/displayed-attributes", + self.client.host, self.uid + ), + Method::Put { + query: (), + body: displayed_attributes + .into_iter() + .map(|v| v.as_ref().to_string()) + .collect(), + }, + 202, + ) + .await + } + + /// Update [faceting](https://www.meilisearch.com/docs/reference/api/settings#faceting) settings of the [Index]. + /// + /// # Example + /// + /// ``` + /// # use meilisearch_sdk::{client::*, indexes::*, settings::Settings, settings::FacetingSettings}; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// # client.create_index("set_faceting", None).await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// let mut index = client.index("set_faceting"); + /// + /// let mut faceting = FacetingSettings { + /// max_values_per_facet: 12, + /// }; + /// + /// let task = index.set_faceting(&faceting).await.unwrap(); + /// # index.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// # }); + /// ``` + pub async fn set_faceting(&self, faceting: &FacetingSettings) -> Result { + self.client + .http_client + .request::<(), &FacetingSettings, TaskInfo>( + &format!( + "{}/indexes/{}/settings/faceting", + self.client.host, self.uid + ), + Method::Patch { + query: (), + body: faceting, + }, + 202, + ) + .await + } + + /// Update [dictionary](https://www.meilisearch.com/docs/reference/api/settings#dictionary) of the [Index]. + /// + /// # Example + /// + /// ``` + /// # use meilisearch_sdk::{client::*, indexes::*, settings::Settings}; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// # client.create_index("set_dictionary", None).await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// let mut index = client.index("set_dictionary"); + /// + /// let task = index.set_dictionary(["J. K.", "J. R. R."]).await.unwrap(); + /// # index.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// # }); + /// ``` + pub async fn set_dictionary( + &self, + dictionary: impl IntoIterator>, + ) -> Result { + self.client + .http_client + .request::<(), Vec, TaskInfo>( + &format!( + "{}/indexes/{}/settings/dictionary", + self.client.host, self.uid + ), + Method::Put { + query: (), + body: dictionary + .into_iter() + .map(|v| v.as_ref().to_string()) + .collect(), + }, + 202, + ) + .await + } + + /// Update [typo tolerance](https://www.meilisearch.com/docs/learn/configuration/typo_tolerance#typo-tolerance) settings of the [Index]. + /// + /// # Example + /// + /// ``` + /// # use meilisearch_sdk::{client::*, indexes::*, settings::Settings, settings::{TypoToleranceSettings, MinWordSizeForTypos}}; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// # client.create_index("set_typo_tolerance", None).await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// let mut index = client.index("set_typo_tolerance"); + /// + /// let typo_tolerance = TypoToleranceSettings{ + /// enabled: Some(true), + /// disable_on_attributes: Some(vec!["title".to_string()]), + /// disable_on_words: Some(vec![]), + /// min_word_size_for_typos: Some(MinWordSizeForTypos::default()), + /// }; + /// + /// let task = index.set_typo_tolerance(&typo_tolerance).await.unwrap(); + /// # index.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// # }); + /// ``` + pub async fn set_typo_tolerance( + &self, + typo_tolerance: &TypoToleranceSettings, + ) -> Result { + self.client + .http_client + .request::<(), &TypoToleranceSettings, TaskInfo>( + &format!( + "{}/indexes/{}/settings/typo-tolerance", + self.client.host, self.uid + ), + Method::Patch { + query: (), + body: typo_tolerance, + }, + 202, + ) + .await + } + + /// Update [separator tokens](https://www.meilisearch.com/docs/reference/api/settings#separator-tokens) settings of the [Index]. + /// + /// # Example + /// + /// ``` + /// # use meilisearch_sdk::{client::*, indexes::*, settings::Settings, settings::{TypoToleranceSettings, MinWordSizeForTypos}}; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// # client.create_index("set_separator_tokens", None).await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// let mut index = client.index("set_separator_tokens"); + /// + /// let separator_token: Vec = vec!["@".to_string(), "#".to_string()]; + /// + /// let task = index.set_separator_tokens(&separator_token).await.unwrap(); + /// # index.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// # }); + /// ``` + pub async fn set_separator_tokens( + &self, + separator_token: &Vec, + ) -> Result { + self.client + .http_client + .request::<(), &Vec, TaskInfo>( + &format!( + "{}/indexes/{}/settings/separator-tokens", + self.client.host, self.uid + ), + Method::Put { + query: (), + body: separator_token, + }, + 202, + ) + .await + } + + /// Update [non separator tokens](https://www.meilisearch.com/docs/reference/api/settings#non-separator-tokens) settings of the [Index]. + /// + /// # Example + /// + /// ``` + /// # use meilisearch_sdk::{client::*, indexes::*, settings::Settings, settings::{TypoToleranceSettings, MinWordSizeForTypos}}; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// # client.create_index("set_non_separator_tokens", None).await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// let mut index = client.index("set_non_separator_tokens"); + /// + /// let non_separator_token: Vec = vec!["@".to_string(), "#".to_string()]; + /// + /// let task = index.set_non_separator_tokens(&non_separator_token).await.unwrap(); + /// # index.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// # }); + /// ``` + pub async fn set_non_separator_tokens( + &self, + non_separator_token: &Vec, + ) -> Result { + self.client + .http_client + .request::<(), &Vec, TaskInfo>( + &format!( + "{}/indexes/{}/settings/non-separator-tokens", + self.client.host, self.uid + ), + Method::Put { + query: (), + body: non_separator_token, + }, + 202, + ) + .await + } + + /// Update [proximity-precision](https://www.meilisearch.com/docs/learn/configuration/proximity-precision) settings of the [Index]. + /// + /// # Example + /// + /// ``` + /// # use meilisearch_sdk::{client::*, indexes::*, settings::Settings}; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// # client.create_index("set_proximity_precision", None).await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// let mut index = client.index("set_proximity_precision"); + /// + /// let task = index.set_proximity_precision("byWord".to_string()).await.unwrap(); + /// # index.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// # }); + /// ``` + pub async fn set_proximity_precision( + &self, + proximity_precision: String, + ) -> Result { + self.client + .http_client + .request::<(), String, TaskInfo>( + &format!( + "{}/indexes/{}/settings/proximity-precision", + self.client.host, self.uid + ), + Method::Put { + query: (), + body: proximity_precision, + }, + 202, + ) + .await + } + + /// Update [search cutoff](https://www.meilisearch.com/docs/reference/api/settings#search-cutoff) settings of the [Index]. + /// + /// # Example + /// + /// ``` + /// # use meilisearch_sdk::{client::*, indexes::*, settings::Settings}; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// # client.create_index("update_search_cutoff_ms", None).await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// let mut index = client.index("update_search_cutoff_ms"); + /// + /// let task = index.set_search_cutoff_ms(Some(150)).await.unwrap(); + /// # index.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// # }); + /// ``` + pub async fn set_search_cutoff_ms(&self, ms: Option) -> Result { + self.client + .http_client + .request::<(), Option, TaskInfo>( + &format!( + "{}/indexes/{}/settings/search-cutoff-ms", + self.client.host, self.uid + ), + Method::Put { + body: ms, + query: (), + }, + 202, + ) + .await + } + + /// Update [localized attributes](https://www.meilisearch.com/docs/reference/api/settings#localized-attributes-object) settings of the [Index]. + /// + /// # Example + /// + /// ``` + /// # use meilisearch_sdk::{client::*, indexes::*, settings::Settings, settings::{LocalizedAttributes}}; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// # client.create_index("set_localized_attributes", None).await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// let mut index = client.index("set_localized_attributes"); + /// + /// let localized_attributes = vec![LocalizedAttributes { + /// locales: vec!["jpn".to_string()], + /// attribute_patterns: vec!["*_ja".to_string()], + /// }]; + /// + /// let task = index.set_localized_attributes(&localized_attributes).await.unwrap(); + /// # index.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// # }); + /// ``` + pub async fn set_localized_attributes( + &self, + localized_attributes: &Vec, + ) -> Result { + self.client + .http_client + .request::<(), &Vec, TaskInfo>( + &format!( + "{}/indexes/{}/settings/localized-attributes", + self.client.host, self.uid + ), + Method::Put { + query: (), + body: localized_attributes, + }, + 202, + ) + .await + } + + /// Reset [Settings] of the [Index]. + /// + /// All settings will be reset to their [default value](https://www.meilisearch.com/docs/reference/api/settings#reset-settings). + /// + /// # Example + /// + /// ``` + /// # use meilisearch_sdk::{client::*, indexes::*, settings::Settings}; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// # client.create_index("reset_settings", None).await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// let mut index = client.index("reset_settings"); + /// + /// let task = index.reset_settings().await.unwrap(); + /// # index.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// # }); + /// ``` + pub async fn reset_settings(&self) -> Result { + self.client + .http_client + .request::<(), (), TaskInfo>( + &format!("{}/indexes/{}/settings", self.client.host, self.uid), + Method::Delete { query: () }, + 202, + ) + .await + } + + /// Reset [synonyms](https://www.meilisearch.com/docs/reference/api/settings#synonyms) of the [Index]. + /// + /// # Example + /// + /// ``` + /// # use meilisearch_sdk::{client::*, indexes::*, settings::Settings}; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// # client.create_index("reset_synonyms", None).await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// let mut index = client.index("reset_synonyms"); + /// + /// let task = index.reset_synonyms().await.unwrap(); + /// # index.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// # }); + /// ``` + pub async fn reset_synonyms(&self) -> Result { + self.client + .http_client + .request::<(), (), TaskInfo>( + &format!( + "{}/indexes/{}/settings/synonyms", + self.client.host, self.uid + ), + Method::Delete { query: () }, + 202, + ) + .await + } + + /// Reset [pagination](https://www.meilisearch.com/docs/learn/configuration/settings#pagination) of the [Index]. + /// + /// # Example + /// + /// ``` + /// # use meilisearch_sdk::{client::*, indexes::*, settings::Settings}; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// # client.create_index("reset_pagination", None).await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// let mut index = client.index("reset_pagination"); + /// + /// let task = index.reset_pagination().await.unwrap(); + /// # index.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// # }); + /// ``` + pub async fn reset_pagination(&self) -> Result { + self.client + .http_client + .request::<(), (), TaskInfo>( + &format!( + "{}/indexes/{}/settings/pagination", + self.client.host, self.uid + ), + Method::Delete { query: () }, + 202, + ) + .await + } + /// Reset [stop-words](https://www.meilisearch.com/docs/reference/api/settings#stop-words) of the [Index]. + /// + /// # Example + /// + /// ``` + /// # use meilisearch_sdk::{client::*, indexes::*, settings::Settings}; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// # client.create_index("reset_stop_words", None).await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// let mut index = client.index("reset_stop_words"); + /// + /// let task = index.reset_stop_words().await.unwrap(); + /// # index.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// # }); + /// ``` + pub async fn reset_stop_words(&self) -> Result { + self.client + .http_client + .request::<(), (), TaskInfo>( + &format!( + "{}/indexes/{}/settings/stop-words", + self.client.host, self.uid + ), + Method::Delete { query: () }, + 202, + ) + .await + } + + /// Reset [ranking rules](https://www.meilisearch.com/docs/learn/core_concepts/relevancy#ranking-rules) of the [Index] to default value. + /// + /// **Default value: `["words", "typo", "proximity", "attribute", "sort", "exactness"]`.** + /// + /// # Example + /// + /// ``` + /// # use meilisearch_sdk::{client::*, indexes::*, settings::Settings}; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// # client.create_index("reset_ranking_rules", None).await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// let mut index = client.index("reset_ranking_rules"); + /// + /// let task = index.reset_ranking_rules().await.unwrap(); + /// # index.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// # }); + /// ``` + pub async fn reset_ranking_rules(&self) -> Result { + self.client + .http_client + .request::<(), (), TaskInfo>( + &format!( + "{}/indexes/{}/settings/ranking-rules", + self.client.host, self.uid + ), + Method::Delete { query: () }, + 202, + ) + .await + } + + /// Reset [filterable attributes](https://www.meilisearch.com/docs/reference/api/settings#filterable-attributes) of the [Index]. + /// + /// # Example + /// + /// ``` + /// # use meilisearch_sdk::{client::*, indexes::*, settings::Settings}; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// # client.create_index("reset_filterable_attributes", None).await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// let mut index = client.index("reset_filterable_attributes"); + /// + /// let task = index.reset_filterable_attributes().await.unwrap(); + /// # index.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// # }); + /// ``` + pub async fn reset_filterable_attributes(&self) -> Result { + self.client + .http_client + .request::<(), (), TaskInfo>( + &format!( + "{}/indexes/{}/settings/filterable-attributes", + self.client.host, self.uid + ), + Method::Delete { query: () }, + 202, + ) + .await + } + + /// Reset [sortable attributes](https://www.meilisearch.com/docs/reference/api/settings#sortable-attributes) of the [Index]. + /// + /// # Example + /// + /// ``` + /// # use meilisearch_sdk::{client::*, indexes::*, settings::Settings}; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// # client.create_index("reset_sortable_attributes", None).await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// let mut index = client.index("reset_sortable_attributes"); + /// + /// let task = index.reset_sortable_attributes().await.unwrap(); + /// # index.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// # }); + /// ``` + pub async fn reset_sortable_attributes(&self) -> Result { + self.client + .http_client + .request::<(), (), TaskInfo>( + &format!( + "{}/indexes/{}/settings/sortable-attributes", + self.client.host, self.uid + ), + Method::Delete { query: () }, + 202, + ) + .await + } + + /// Reset the [distinct attribute](https://www.meilisearch.com/docs/reference/api/settings#distinct-attribute) of the [Index]. + /// + /// # Example + /// + /// ``` + /// # use meilisearch_sdk::{client::*, indexes::*, settings::Settings}; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// # client.create_index("reset_distinct_attribute", None).await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// let mut index = client.index("reset_distinct_attribute"); + /// + /// let task = index.reset_distinct_attribute().await.unwrap(); + /// # index.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// # }); + /// ``` + pub async fn reset_distinct_attribute(&self) -> Result { + self.client + .http_client + .request::<(), (), TaskInfo>( + &format!( + "{}/indexes/{}/settings/distinct-attribute", + self.client.host, self.uid + ), + Method::Delete { query: () }, + 202, + ) + .await + } + + /// Reset [searchable attributes](https://www.meilisearch.com/docs/learn/configuration/displayed_searchable_attributes#searchable-fields) of + /// the [Index] (enable all attributes). + /// + /// # Example + /// + /// ``` + /// # use meilisearch_sdk::{client::*, indexes::*, settings::Settings}; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// # client.create_index("reset_searchable_attributes", None).await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// let mut index = client.index("reset_searchable_attributes"); + /// + /// let task = index.reset_searchable_attributes().await.unwrap(); + /// # index.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// # }); + /// ``` + pub async fn reset_searchable_attributes(&self) -> Result { + self.client + .http_client + .request::<(), (), TaskInfo>( + &format!( + "{}/indexes/{}/settings/searchable-attributes", + self.client.host, self.uid + ), + Method::Delete { query: () }, + 202, + ) + .await + } + + /// Reset [displayed attributes](https://www.meilisearch.com/docs/reference/api/settings#displayed-attributes) of the [Index] (enable all attributes). + /// + /// # Example + /// + /// ``` + /// # use meilisearch_sdk::{client::*, indexes::*, settings::Settings}; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// # client.create_index("reset_displayed_attributes", None).await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// let mut index = client.index("reset_displayed_attributes"); + /// + /// let task = index.reset_displayed_attributes().await.unwrap(); + /// # index.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// # }); + /// ``` + pub async fn reset_displayed_attributes(&self) -> Result { + self.client + .http_client + .request::<(), (), TaskInfo>( + &format!( + "{}/indexes/{}/settings/displayed-attributes", + self.client.host, self.uid + ), + Method::Delete { query: () }, + 202, + ) + .await + } + + /// Reset [faceting](https://www.meilisearch.com/docs/reference/api/settings#faceting) settings of the [Index]. + /// + /// # Example + /// + /// ``` + /// # use meilisearch_sdk::{client::*, indexes::*, settings::Settings}; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// # client.create_index("reset_faceting", None).await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// let mut index = client.index("reset_faceting"); + /// + /// let task = index.reset_faceting().await.unwrap(); + /// # index.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// # }); + /// ``` + pub async fn reset_faceting(&self) -> Result { + self.client + .http_client + .request::<(), (), TaskInfo>( + &format!( + "{}/indexes/{}/settings/faceting", + self.client.host, self.uid + ), + Method::Delete { query: () }, + 202, + ) + .await + } + + /// Reset [dictionary](https://www.meilisearch.com/docs/reference/api/settings#dictionary) of the [Index]. + /// + /// # Example + /// + /// ``` + /// # use meilisearch_sdk::{client::*, indexes::*, settings::Settings}; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// # client.create_index("reset_dictionary", None).await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// let mut index = client.index("reset_dictionary"); + /// + /// let task = index.reset_dictionary().await.unwrap(); + /// # index.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// # }); + /// ``` + pub async fn reset_dictionary(&self) -> Result { + self.client + .http_client + .request::<(), (), TaskInfo>( + &format!( + "{}/indexes/{}/settings/dictionary", + self.client.host, self.uid + ), + Method::Delete { query: () }, + 202, + ) + .await + } + + /// Reset [typo tolerance](https://www.meilisearch.com/docs/learn/configuration/typo_tolerance#typo-tolerance) settings of the [Index]. + /// + /// # Example + /// + /// ``` + /// # use meilisearch_sdk::{client::*, indexes::*, settings::Settings}; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// # client.create_index("reset_typo_tolerance", None).await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// let mut index = client.index("reset_typo_tolerance"); + /// + /// let task = index.reset_typo_tolerance().await.unwrap(); + /// # index.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// # }); + /// ``` + pub async fn reset_typo_tolerance(&self) -> Result { + self.client + .http_client + .request::<(), (), TaskInfo>( + &format!( + "{}/indexes/{}/settings/typo-tolerance", + self.client.host, self.uid + ), + Method::Delete { query: () }, + 202, + ) + .await + } + + /// Reset [proximity precision](https://www.meilisearch.com/docs/learn/configuration/typo_tolerance#typo-tolerance) settings of the [Index]. + /// + /// # Example + /// + /// ``` + /// # use meilisearch_sdk::{client::*, indexes::*, settings::Settings}; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// # client.create_index("reset_proximity_precision", None).await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// let mut index = client.index("reset_proximity_precision"); + /// + /// let task = index.reset_proximity_precision().await.unwrap(); + /// # index.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// # }); + /// ``` + pub async fn reset_proximity_precision(&self) -> Result { + self.client + .http_client + .request::<(), (), TaskInfo>( + &format!( + "{}/indexes/{}/settings/proximity-precision", + self.client.host, self.uid + ), + Method::Delete { query: () }, + 202, + ) + .await + } + + /// Reset [search cutoff](https://www.meilisearch.com/docs/reference/api/settings#search-cutoff) settings of the [Index]. + /// + /// # Example + /// + /// ``` + /// # use meilisearch_sdk::{client::*, indexes::*, settings::Settings}; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// # client.create_index("reset_search_cutoff_ms", None).await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// let mut index = client.index("reset_search_cutoff_ms"); + /// + /// let task = index.reset_search_cutoff_ms().await.unwrap(); + /// # index.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// # }); + /// ``` + pub async fn reset_search_cutoff_ms(&self) -> Result { + self.client + .http_client + .request::<(), (), TaskInfo>( + &format!( + "{}/indexes/{}/settings/search-cutoff-ms", + self.client.host, self.uid + ), + Method::Delete { query: () }, + 202, + ) + .await + } + + /// Reset [search cutoff](https://www.meilisearch.com/docs/reference/api/settings#search-cutoff) settings of the [Index]. + /// + /// # Example + /// + /// ``` + /// # use meilisearch_sdk::{client::*, indexes::*, settings::Settings}; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// # client.create_index("reset_separator_tokens", None).await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// let mut index = client.index("reset_separator_tokens"); + /// + /// let task = index.reset_separator_tokens().await.unwrap(); + /// # index.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// # }); + /// ``` + pub async fn reset_separator_tokens(&self) -> Result { + self.client + .http_client + .request::<(), (), TaskInfo>( + &format!( + "{}/indexes/{}/settings/separator-tokens", + self.client.host, self.uid + ), + Method::Delete { query: () }, + 202, + ) + .await + } + + /// Reset [non separator tokens](https://www.meilisearch.com/docs/reference/api/settings#non-separator-tokens) settings of the [Index]. + /// + /// # Example + /// + /// ``` + /// # use meilisearch_sdk::{client::*, indexes::*, settings::Settings}; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// # client.create_index("reset_non_separator_tokens", None).await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// let mut index = client.index("reset_non_separator_tokens"); + /// + /// let task = index.reset_non_separator_tokens().await.unwrap(); + /// # index.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// # }); + /// ``` + pub async fn reset_non_separator_tokens(&self) -> Result { + self.client + .http_client + .request::<(), (), TaskInfo>( + &format!( + "{}/indexes/{}/settings/non-separator-tokens", + self.client.host, self.uid + ), + Method::Delete { query: () }, + 202, + ) + .await + } + + /// Reset [localized attributes](https://www.meilisearch.com/docs/reference/api/settings#localized-attributes-object) settings of the [Index]. + /// + /// # Example + /// + /// ``` + /// # use meilisearch_sdk::{client::*, indexes::*, settings::Settings}; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// # client.create_index("reset_localized_attributes", None).await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// let index = client.index("reset_localized_attributes"); + /// + /// let task = index.reset_localized_attributes().await.unwrap(); + /// # index.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// # }); + /// ``` + pub async fn reset_localized_attributes(&self) -> Result { + self.client + .http_client + .request::<(), (), TaskInfo>( + &format!( + "{}/indexes/{}/settings/localized-attributes", + self.client.host, self.uid + ), + Method::Delete { query: () }, + 202, + ) + .await + } +} + +#[cfg(test)] +mod tests { + use super::*; + + use crate::client::*; + use meilisearch_test_macro::meilisearch_test; + + #[meilisearch_test] + async fn test_set_faceting_settings(client: Client, index: Index) { + let faceting = FacetingSettings { + max_values_per_facet: 5, + }; + let settings = Settings::new().with_faceting(&faceting); + + let task_info = index.set_settings(&settings).await.unwrap(); + client.wait_for_task(task_info, None, None).await.unwrap(); + + let res = index.get_faceting().await.unwrap(); + + assert_eq!(faceting, res); + } + + #[meilisearch_test] + async fn test_get_faceting(index: Index) { + let faceting = FacetingSettings { + max_values_per_facet: 100, + }; + + let res = index.get_faceting().await.unwrap(); + + assert_eq!(faceting, res); + } + + #[meilisearch_test] + async fn test_set_faceting(client: Client, index: Index) { + let faceting = FacetingSettings { + max_values_per_facet: 5, + }; + let task_info = index.set_faceting(&faceting).await.unwrap(); + client.wait_for_task(task_info, None, None).await.unwrap(); + + let res = index.get_faceting().await.unwrap(); + + assert_eq!(faceting, res); + } + + #[meilisearch_test] + async fn test_reset_faceting(client: Client, index: Index) { + let task_info = index.reset_faceting().await.unwrap(); + client.wait_for_task(task_info, None, None).await.unwrap(); + let faceting = FacetingSettings { + max_values_per_facet: 100, + }; + + let res = index.get_faceting().await.unwrap(); + + assert_eq!(faceting, res); + } + + #[meilisearch_test] + async fn test_get_dictionary(index: Index) { + let dictionary: Vec = vec![]; + + let res = index.get_dictionary().await.unwrap(); + + assert_eq!(dictionary, res); + } + + #[meilisearch_test] + async fn test_set_dictionary(client: Client, index: Index) { + let dictionary: Vec<&str> = vec!["J. K.", "J. R. R."]; + let task_info = index.set_dictionary(&dictionary).await.unwrap(); + client.wait_for_task(task_info, None, None).await.unwrap(); + + let res = index.get_dictionary().await.unwrap(); + + assert_eq!(dictionary, res); + } + + #[meilisearch_test] + async fn test_set_empty_dictionary(client: Client, index: Index) { + let dictionary: Vec<&str> = vec![]; + let task_info = index.set_dictionary(&dictionary).await.unwrap(); + client.wait_for_task(task_info, None, None).await.unwrap(); + + let res = index.get_dictionary().await.unwrap(); + + assert_eq!(dictionary, res); + } + + #[meilisearch_test] + async fn test_reset_dictionary(client: Client, index: Index) { + let dictionary: Vec<&str> = vec![]; + let task_info = index.reset_dictionary().await.unwrap(); + client.wait_for_task(task_info, None, None).await.unwrap(); + + let res = index.get_dictionary().await.unwrap(); + + assert_eq!(dictionary, res); + } + + #[meilisearch_test] + async fn test_get_pagination(index: Index) { + let pagination = PaginationSetting { + max_total_hits: 1000, + }; + + let res = index.get_pagination().await.unwrap(); + + assert_eq!(pagination, res); + } + + #[meilisearch_test] + async fn test_set_pagination(index: Index) { + let pagination = PaginationSetting { max_total_hits: 11 }; + let task = index.set_pagination(pagination).await.unwrap(); + index.wait_for_task(task, None, None).await.unwrap(); + + let res = index.get_pagination().await.unwrap(); + + assert_eq!(pagination, res); + } + + #[meilisearch_test] + async fn test_reset_pagination(index: Index) { + let pagination = PaginationSetting { max_total_hits: 10 }; + let default = PaginationSetting { + max_total_hits: 1000, + }; + + let task = index.set_pagination(pagination).await.unwrap(); + index.wait_for_task(task, None, None).await.unwrap(); + + let reset_task = index.reset_pagination().await.unwrap(); + index.wait_for_task(reset_task, None, None).await.unwrap(); + + let res = index.get_pagination().await.unwrap(); + + assert_eq!(default, res); + } + + #[meilisearch_test] + async fn test_get_typo_tolerance(index: Index) { + let expected = TypoToleranceSettings { + enabled: Some(true), + disable_on_attributes: Some(vec![]), + disable_on_words: Some(vec![]), + min_word_size_for_typos: Some(MinWordSizeForTypos { + one_typo: Some(5), + two_typos: Some(9), + }), + }; + + let res = index.get_typo_tolerance().await.unwrap(); + + assert_eq!(expected, res); + } + + #[meilisearch_test] + async fn test_set_typo_tolerance(client: Client, index: Index) { + let expected = TypoToleranceSettings { + enabled: Some(true), + disable_on_attributes: Some(vec!["title".to_string()]), + disable_on_words: Some(vec![]), + min_word_size_for_typos: Some(MinWordSizeForTypos { + one_typo: Some(5), + two_typos: Some(9), + }), + }; + + let typo_tolerance = TypoToleranceSettings { + disable_on_attributes: Some(vec!["title".to_string()]), + ..Default::default() + }; + + let task_info = index.set_typo_tolerance(&typo_tolerance).await.unwrap(); + client.wait_for_task(task_info, None, None).await.unwrap(); + + let res = index.get_typo_tolerance().await.unwrap(); + + assert_eq!(expected, res); + } + + #[meilisearch_test] + async fn test_reset_typo_tolerance(index: Index) { + let expected = TypoToleranceSettings { + enabled: Some(true), + disable_on_attributes: Some(vec![]), + disable_on_words: Some(vec![]), + min_word_size_for_typos: Some(MinWordSizeForTypos { + one_typo: Some(5), + two_typos: Some(9), + }), + }; + + let typo_tolerance = TypoToleranceSettings { + disable_on_attributes: Some(vec!["title".to_string()]), + ..Default::default() + }; + + let task = index.set_typo_tolerance(&typo_tolerance).await.unwrap(); + index.wait_for_task(task, None, None).await.unwrap(); + + let reset_task = index.reset_typo_tolerance().await.unwrap(); + index.wait_for_task(reset_task, None, None).await.unwrap(); + + let default = index.get_typo_tolerance().await.unwrap(); + + assert_eq!(expected, default); + } + + #[meilisearch_test] + async fn test_get_proximity_precision(index: Index) { + let expected = "byWord".to_string(); + + let res = index.get_proximity_precision().await.unwrap(); + + assert_eq!(expected, res); + } + + #[meilisearch_test] + async fn test_set_proximity_precision(client: Client, index: Index) { + let expected = "byAttribute".to_string(); + + let task_info = index + .set_proximity_precision("byAttribute".to_string()) + .await + .unwrap(); + client.wait_for_task(task_info, None, None).await.unwrap(); + + let res = index.get_proximity_precision().await.unwrap(); + + assert_eq!(expected, res); + } + + #[meilisearch_test] + async fn test_reset_proximity_precision(index: Index) { + let expected = "byWord".to_string(); + + let task = index + .set_proximity_precision("byAttribute".to_string()) + .await + .unwrap(); + index.wait_for_task(task, None, None).await.unwrap(); + + let reset_task = index.reset_proximity_precision().await.unwrap(); + index.wait_for_task(reset_task, None, None).await.unwrap(); + + let default = index.get_proximity_precision().await.unwrap(); + + assert_eq!(expected, default); + } + + #[meilisearch_test] + async fn test_get_search_cutoff_ms(index: Index) { + let expected = None; + + let res = index.get_search_cutoff_ms().await.unwrap(); + + assert_eq!(expected, res); + } + + #[meilisearch_test] + async fn test_set_search_cutoff_ms(client: Client, index: Index) { + let expected = Some(150); + + let task_info = index.set_search_cutoff_ms(Some(150)).await.unwrap(); + client.wait_for_task(task_info, None, None).await.unwrap(); + + let res = index.get_search_cutoff_ms().await.unwrap(); + + assert_eq!(expected, res); + } + + #[meilisearch_test] + async fn test_get_separator_tokens(index: Index) { + let separator: Vec<&str> = vec![]; + let res = index.get_separator_tokens().await.unwrap(); + + assert_eq!(separator, res); + } + + #[meilisearch_test] + async fn test_set_separator_tokens(client: Client, index: Index) { + let expected: Vec = vec!["#".to_string(), "@".to_string()]; + + let task_info = index.set_separator_tokens(&expected).await.unwrap(); + client.wait_for_task(task_info, None, None).await.unwrap(); + + let res = index.get_separator_tokens().await.unwrap(); + + assert_eq!(expected, res); + } + + #[meilisearch_test] + async fn test_reset_search_cutoff_ms(index: Index) { + let expected = None; + + let task = index.set_search_cutoff_ms(Some(150)).await.unwrap(); + index.wait_for_task(task, None, None).await.unwrap(); + + let reset_task = index.reset_search_cutoff_ms().await.unwrap(); + index.wait_for_task(reset_task, None, None).await.unwrap(); + + let default = index.get_search_cutoff_ms().await.unwrap(); + + assert_eq!(expected, default); + } + + #[meilisearch_test] + async fn test_reset_separator_tokens(client: Client, index: Index) { + let separator: Vec<&str> = vec![]; + let task_info = index.reset_separator_tokens().await.unwrap(); + client.wait_for_task(task_info, None, None).await.unwrap(); + + let res = index.get_dictionary().await.unwrap(); + assert_eq!(separator, res); + } + + #[meilisearch_test] + async fn test_get_non_separator_tokens(index: Index) { + let separator: Vec<&str> = vec![]; + let res = index.get_non_separator_tokens().await.unwrap(); + + assert_eq!(separator, res); + } + + #[meilisearch_test] + async fn test_set_non_separator_tokens(client: Client, index: Index) { + let expected: Vec = vec!["#".to_string(), "@".to_string()]; + + let task_info = index.set_non_separator_tokens(&expected).await.unwrap(); + client.wait_for_task(task_info, None, None).await.unwrap(); + + let res = index.get_non_separator_tokens().await.unwrap(); + + assert_eq!(expected, res); + } + + #[meilisearch_test] + async fn test_reset_non_separator_tokens(client: Client, index: Index) { + let separator: Vec<&str> = vec![]; + let task_info = index.reset_non_separator_tokens().await.unwrap(); + client.wait_for_task(task_info, None, None).await.unwrap(); + + let res = index.get_dictionary().await.unwrap(); + assert_eq!(separator, res); + } + + #[meilisearch_test] + async fn test_get_localized_attributes(index: Index) { + let res = index.get_localized_attributes().await.unwrap(); + assert_eq!(None, res); + } + + #[meilisearch_test] + async fn test_set_localized_attributes(client: Client, index: Index) { + let localized_attributes = vec![LocalizedAttributes { + locales: vec!["jpn".to_string()], + attribute_patterns: vec!["*_ja".to_string()], + }]; + let task_info = index + .set_localized_attributes(&localized_attributes) + .await + .unwrap(); + client.wait_for_task(task_info, None, None).await.unwrap(); + + let res = index.get_localized_attributes().await.unwrap(); + assert_eq!(Some(localized_attributes), res); + } + + #[meilisearch_test] + async fn test_reset_localized_attributes(client: Client, index: Index) { + let localized_attributes = vec![LocalizedAttributes { + locales: vec!["jpn".to_string()], + attribute_patterns: vec!["*_ja".to_string()], + }]; + let task_info = index + .set_localized_attributes(&localized_attributes) + .await + .unwrap(); + client.wait_for_task(task_info, None, None).await.unwrap(); + + let reset_task = index.reset_localized_attributes().await.unwrap(); + client.wait_for_task(reset_task, None, None).await.unwrap(); + + let res = index.get_localized_attributes().await.unwrap(); + assert_eq!(None, res); + } +} diff --git a/backend/vendor/meilisearch-sdk/src/snapshots.rs b/backend/vendor/meilisearch-sdk/src/snapshots.rs new file mode 100644 index 000000000..271322772 --- /dev/null +++ b/backend/vendor/meilisearch-sdk/src/snapshots.rs @@ -0,0 +1,116 @@ +//! The `snapshots` module allows the creation of database snapshots. +//! +//! - snapshots are `.snapshots` files that can be used to launch Meilisearch. +//! +//! - snapshots are not compatible between Meilisearch versions. +//! +//! # Example +//! +//! ``` +//! # use meilisearch_sdk::{client::*, errors::*, snapshots::*, snapshots::*, task_info::*, tasks::*}; +//! # use futures_await_test::async_test; +//! # use std::{thread::sleep, time::Duration}; +//! # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { +//! # +//! # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); +//! # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); +//! # +//! # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); +//! +//! // Create a snapshot +//! let task_info = client.create_snapshot().await.unwrap(); +//! assert!(matches!( +//! task_info, +//! TaskInfo { +//! update_type: TaskType::SnapshotCreation { .. }, +//! .. +//! } +//! )); +//! # }); +//! ``` + +use crate::{client::Client, errors::Error, request::*, task_info::TaskInfo}; + +/// Snapshots related methods. +/// See the [snapshots](crate::snapshots) module. +impl Client { + /// Triggers a snapshots creation process. + /// + /// Once the process is complete, a snapshots is created in the [snapshots directory]. + /// If the snapshots directory does not exist yet, it will be created. + /// + /// # Example + /// + /// ``` + /// # use meilisearch_sdk::{client::*, errors::*, snapshots::*, snapshots::*, task_info::*, tasks::*}; + /// # use futures_await_test::async_test; + /// # use std::{thread::sleep, time::Duration}; + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// # + /// let task_info = client.create_snapshot().await.unwrap(); + /// + /// assert!(matches!( + /// task_info, + /// TaskInfo { + /// update_type: TaskType::SnapshotCreation { .. }, + /// .. + /// } + /// )); + /// # }); + /// ``` + pub async fn create_snapshot(&self) -> Result { + self.http_client + .request::<(), (), TaskInfo>( + &format!("{}/snapshots", self.host), + Method::Post { + query: (), + body: (), + }, + 202, + ) + .await + } +} + +/// Alias for [`create_snapshot`](Client::create_snapshot). +pub async fn create_snapshot(client: &Client) -> Result { + client.create_snapshot().await +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{client::*, tasks::*}; + use meilisearch_test_macro::meilisearch_test; + + #[meilisearch_test] + async fn test_snapshot_success_creation(client: Client) -> Result<(), Error> { + let task = client + .create_snapshot() + .await? + .wait_for_completion(&client, None, None) + .await?; + + assert!(matches!(task, Task::Succeeded { .. })); + Ok(()) + } + + #[meilisearch_test] + async fn test_snapshot_correct_update_type(client: Client) -> Result<(), Error> { + let task_info = client.create_snapshot().await.unwrap(); + + assert!(matches!( + task_info, + TaskInfo { + update_type: TaskType::SnapshotCreation { .. }, + .. + } + )); + Ok(()) + } +} diff --git a/backend/vendor/meilisearch-sdk/src/task_info.rs b/backend/vendor/meilisearch-sdk/src/task_info.rs new file mode 100644 index 000000000..c65c2186a --- /dev/null +++ b/backend/vendor/meilisearch-sdk/src/task_info.rs @@ -0,0 +1,181 @@ +use serde::Deserialize; +use std::time::Duration; +use time::OffsetDateTime; + +use crate::{client::Client, errors::Error, request::HttpClient, tasks::*}; + +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct TaskInfo { + #[serde(with = "time::serde::rfc3339")] + pub enqueued_at: OffsetDateTime, + pub index_uid: Option, + pub status: String, + #[serde(flatten)] + pub update_type: TaskType, + pub task_uid: u32, +} + +impl AsRef for TaskInfo { + fn as_ref(&self) -> &u32 { + &self.task_uid + } +} + +impl TaskInfo { + #[must_use] + pub fn get_task_uid(&self) -> u32 { + self.task_uid + } + + /// Wait until Meilisearch processes a task provided by [`TaskInfo`], and get its status. + /// + /// `interval` = The frequency at which the server should be polled. **Default = 50ms** + /// + /// `timeout` = The maximum time to wait for processing to complete. **Default = 5000ms** + /// + /// If the waited time exceeds `timeout` then an [`Error::Timeout`] will be returned. + /// + /// See also [`Client::wait_for_task`, `Index::wait_for_task`]. + /// + /// # Example + /// + /// ``` + /// # use meilisearch_sdk::{client::*, indexes::*, tasks::*}; + /// # use serde::{Serialize, Deserialize}; + /// # + /// # #[derive(Debug, Serialize, Deserialize, PartialEq)] + /// # struct Document { + /// # id: usize, + /// # value: String, + /// # kind: String, + /// # } + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// let movies = client.index("movies_wait_for_completion"); + /// + /// let status = movies.add_documents(&[ + /// Document { id: 0, kind: "title".into(), value: "The Social Network".to_string() }, + /// Document { id: 1, kind: "title".into(), value: "Harry Potter and the Sorcerer's Stone".to_string() }, + /// ], None) + /// .await + /// .unwrap() + /// .wait_for_completion(&client, None, None) + /// .await + /// .unwrap(); + /// + /// assert!(matches!(status, Task::Succeeded { .. })); + /// # movies.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// # }); + /// ``` + pub async fn wait_for_completion( + self, + client: &Client, + interval: Option, + timeout: Option, + ) -> Result { + client.wait_for_task(self, interval, timeout).await + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::{ + client::*, + errors::{ErrorCode, ErrorType}, + indexes::Index, + }; + use big_s::S; + use meilisearch_test_macro::meilisearch_test; + use serde::{Deserialize, Serialize}; + use std::time::Duration; + + #[derive(Debug, Serialize, Deserialize, PartialEq)] + struct Document { + id: usize, + value: String, + kind: String, + } + + #[test] + fn test_deserialize_task_info() { + let datetime = OffsetDateTime::parse( + "2022-02-03T13:02:38.369634Z", + &time::format_description::well_known::Rfc3339, + ) + .unwrap(); + + let task_info: TaskInfo = serde_json::from_str( + r#" +{ + "enqueuedAt": "2022-02-03T13:02:38.369634Z", + "indexUid": "meili", + "status": "enqueued", + "type": "documentAdditionOrUpdate", + "taskUid": 12 +}"#, + ) + .unwrap(); + + assert!(matches!( + task_info, + TaskInfo { + enqueued_at, + index_uid: Some(index_uid), + task_uid: 12, + update_type: TaskType::DocumentAdditionOrUpdate { details: None }, + status, + } + if enqueued_at == datetime && index_uid == "meili" && status == "enqueued")); + } + + #[meilisearch_test] + async fn test_wait_for_task_with_args(client: Client, movies: Index) -> Result<(), Error> { + let task_info = movies + .add_documents( + &[ + Document { + id: 0, + kind: "title".into(), + value: S("The Social Network"), + }, + Document { + id: 1, + kind: "title".into(), + value: S("Harry Potter and the Sorcerer's Stone"), + }, + ], + None, + ) + .await?; + + let task = client + .get_task(task_info) + .await? + .wait_for_completion( + &client, + Some(Duration::from_millis(1)), + Some(Duration::from_millis(6000)), + ) + .await?; + + assert!(matches!(task, Task::Succeeded { .. })); + Ok(()) + } + + #[meilisearch_test] + async fn test_failing_task(client: Client, index: Index) -> Result<(), Error> { + let task_info = client.create_index(index.uid, None).await.unwrap(); + let task = client.wait_for_task(task_info, None, None).await?; + + let error = task.unwrap_failure(); + assert_eq!(error.error_code, ErrorCode::IndexAlreadyExists); + assert_eq!(error.error_type, ErrorType::InvalidRequest); + Ok(()) + } +} diff --git a/backend/vendor/meilisearch-sdk/src/tasks.rs b/backend/vendor/meilisearch-sdk/src/tasks.rs new file mode 100644 index 000000000..37babd2f4 --- /dev/null +++ b/backend/vendor/meilisearch-sdk/src/tasks.rs @@ -0,0 +1,1143 @@ +use serde::{Deserialize, Deserializer, Serialize}; +use std::time::Duration; +use time::OffsetDateTime; + +use crate::{ + client::Client, client::SwapIndexes, errors::Error, errors::MeilisearchError, indexes::Index, + request::HttpClient, settings::Settings, task_info::TaskInfo, +}; + +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase", tag = "type")] +pub enum TaskType { + Customs, + DocumentAdditionOrUpdate { + details: Option, + }, + DocumentDeletion { + details: Option, + }, + IndexCreation { + details: Option, + }, + IndexUpdate { + details: Option, + }, + IndexDeletion { + details: Option, + }, + SettingsUpdate { + details: Box>, + }, + DumpCreation { + details: Option, + }, + IndexSwap { + details: Option, + }, + TaskCancelation { + details: Option, + }, + TaskDeletion { + details: Option, + }, + SnapshotCreation { + details: Option, + }, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct TasksResults { + pub results: Vec, + pub total: u64, + pub limit: u32, + pub from: Option, + pub next: Option, +} + +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct DocumentAdditionOrUpdate { + pub indexed_documents: Option, + pub received_documents: usize, +} + +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct DocumentDeletion { + pub provided_ids: Option, + pub deleted_documents: Option, + pub original_filter: Option, +} + +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct IndexCreation { + pub primary_key: Option, +} + +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct IndexUpdate { + pub primary_key: Option, +} + +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct IndexDeletion { + pub deleted_documents: Option, +} + +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SnapshotCreation {} + +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct DumpCreation { + pub dump_uid: Option, +} + +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct IndexSwap { + pub swaps: Vec, +} + +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct TaskCancelation { + pub matched_tasks: usize, + pub canceled_tasks: usize, + pub original_filter: String, +} + +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct TaskDeletion { + pub matched_tasks: usize, + pub deleted_tasks: usize, + pub original_filter: String, +} + +#[derive(Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct FailedTask { + pub error: MeilisearchError, + #[serde(flatten)] + pub task: SucceededTask, +} + +impl AsRef for FailedTask { + fn as_ref(&self) -> &u32 { + &self.task.uid + } +} + +fn deserialize_duration<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + let s = String::deserialize(deserializer)?; + let iso_duration = iso8601::duration(&s).map_err(serde::de::Error::custom)?; + Ok(iso_duration.into()) +} + +#[derive(Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct SucceededTask { + #[serde(deserialize_with = "deserialize_duration")] + pub duration: Duration, + #[serde(with = "time::serde::rfc3339")] + pub enqueued_at: OffsetDateTime, + #[serde(with = "time::serde::rfc3339")] + pub started_at: OffsetDateTime, + #[serde(with = "time::serde::rfc3339")] + pub finished_at: OffsetDateTime, + pub canceled_by: Option, + pub index_uid: Option, + pub error: Option, + #[serde(flatten)] + pub update_type: TaskType, + pub uid: u32, +} + +impl AsRef for SucceededTask { + fn as_ref(&self) -> &u32 { + &self.uid + } +} + +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct EnqueuedTask { + #[serde(with = "time::serde::rfc3339")] + pub enqueued_at: OffsetDateTime, + pub index_uid: Option, + #[serde(flatten)] + pub update_type: TaskType, + pub uid: u32, +} + +impl AsRef for EnqueuedTask { + fn as_ref(&self) -> &u32 { + &self.uid + } +} + +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ProcessingTask { + #[serde(with = "time::serde::rfc3339")] + pub enqueued_at: OffsetDateTime, + #[serde(with = "time::serde::rfc3339")] + pub started_at: OffsetDateTime, + pub index_uid: Option, + #[serde(flatten)] + pub update_type: TaskType, + pub uid: u32, +} + +impl AsRef for ProcessingTask { + fn as_ref(&self) -> &u32 { + &self.uid + } +} + +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase", tag = "status")] +pub enum Task { + Enqueued { + #[serde(flatten)] + content: EnqueuedTask, + }, + Processing { + #[serde(flatten)] + content: ProcessingTask, + }, + Failed { + #[serde(flatten)] + content: FailedTask, + }, + Succeeded { + #[serde(flatten)] + content: SucceededTask, + }, +} + +impl Task { + #[must_use] + pub fn get_uid(&self) -> u32 { + match self { + Self::Enqueued { content } => *content.as_ref(), + Self::Processing { content } => *content.as_ref(), + Self::Failed { content } => *content.as_ref(), + Self::Succeeded { content } => *content.as_ref(), + } + } + + /// Wait until Meilisearch processes a [Task], and get its status. + /// + /// `interval` = The frequency at which the server should be polled. **Default = 50ms** + /// + /// `timeout` = The maximum time to wait for processing to complete. **Default = 5000ms** + /// + /// If the waited time exceeds `timeout` then an [`Error::Timeout`] will be returned. + /// + /// See also [`Client::wait_for_task`, `Index::wait_for_task`]. + /// + /// # Example + /// + /// ``` + /// # use meilisearch_sdk::{client::*, indexes::*, tasks::Task}; + /// # use serde::{Serialize, Deserialize}; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # #[derive(Debug, Serialize, Deserialize, PartialEq)] + /// # struct Document { + /// # id: usize, + /// # value: String, + /// # kind: String, + /// # } + /// # + /// # + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// let movies = client.index("movies_wait_for_completion"); + /// + /// let status = movies.add_documents(&[ + /// Document { id: 0, kind: "title".into(), value: "The Social Network".to_string() }, + /// Document { id: 1, kind: "title".into(), value: "Harry Potter and the Sorcerer's Stone".to_string() }, + /// ], None) + /// .await + /// .unwrap() + /// .wait_for_completion(&client, None, None) + /// .await + /// .unwrap(); + /// + /// assert!(matches!(status, Task::Succeeded { .. })); + /// # movies.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// # }); + /// ``` + pub async fn wait_for_completion( + self, + client: &Client, + interval: Option, + timeout: Option, + ) -> Result { + client.wait_for_task(self, interval, timeout).await + } + + /// Extract the [Index] from a successful `IndexCreation` task. + /// + /// If the task failed or was not an `IndexCreation` task it returns itself. + /// + /// # Example + /// + /// ``` + /// # use meilisearch_sdk::{client::*, indexes::*}; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// # // create the client + /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// let task = client.create_index("try_make_index", None).await.unwrap(); + /// let index = client.wait_for_task(task, None, None).await.unwrap().try_make_index(&client).unwrap(); + /// + /// // and safely access it + /// assert_eq!(index.as_ref(), "try_make_index"); + /// # index.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// # }); + /// ``` + #[allow(clippy::result_large_err)] // Since `self` has been consumed, this is not an issue + pub fn try_make_index( + self, + client: &Client, + ) -> Result, Self> { + match self { + Self::Succeeded { + content: + SucceededTask { + index_uid, + update_type: TaskType::IndexCreation { .. }, + .. + }, + } => Ok(client.index(index_uid.unwrap())), + _ => Err(self), + } + } + + /// Unwrap the [`MeilisearchError`] from a [`Self::Failed`] [Task]. + /// + /// Will panic if the task was not [`Self::Failed`]. + /// + /// # Example + /// + /// ``` + /// # use meilisearch_sdk::{client::*, indexes::*, errors::ErrorCode}; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// let task = client.create_index("unwrap_failure", None).await.unwrap(); + /// let task = client + /// .create_index("unwrap_failure", None) + /// .await + /// .unwrap() + /// .wait_for_completion(&client, None, None) + /// .await + /// .unwrap(); + /// + /// assert!(task.is_failure()); + /// + /// let failure = task.unwrap_failure(); + /// + /// assert_eq!(failure.error_code, ErrorCode::IndexAlreadyExists); + /// # client.index("unwrap_failure").delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// # }); + /// ``` + #[must_use] + pub fn unwrap_failure(self) -> MeilisearchError { + match self { + Self::Failed { + content: FailedTask { error, .. }, + } => error, + _ => panic!("Called `unwrap_failure` on a non `Failed` task."), + } + } + + /// Returns `true` if the [Task] is [`Self::Failed`]. + /// + /// # Example + /// + /// ``` + /// # use meilisearch_sdk::{client::*, indexes::*, errors::ErrorCode}; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// let task = client.create_index("is_failure", None).await.unwrap(); + /// // create an index with a conflicting uid + /// let task = client + /// .create_index("is_failure", None) + /// .await + /// .unwrap() + /// .wait_for_completion(&client, None, None) + /// .await + /// .unwrap(); + /// + /// assert!(task.is_failure()); + /// # client.index("is_failure").delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// # }); + /// ``` + #[must_use] + pub fn is_failure(&self) -> bool { + matches!(self, Self::Failed { .. }) + } + + /// Returns `true` if the [Task] is [`Self::Succeeded`]. + /// + /// # Example + /// + /// ``` + /// # use meilisearch_sdk::{client::*, indexes::*, errors::ErrorCode}; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// let task = client + /// .create_index("is_success", None) + /// .await + /// .unwrap() + /// .wait_for_completion(&client, None, None) + /// .await + /// .unwrap(); + /// + /// assert!(task.is_success()); + /// # task.try_make_index(&client).unwrap().delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// # }); + /// ``` + #[must_use] + pub fn is_success(&self) -> bool { + matches!(self, Self::Succeeded { .. }) + } + + /// Returns `true` if the [Task] is pending ([`Self::Enqueued`] or [`Self::Processing`]). + /// + /// # Example + /// ```no_run + /// # // The test is not run because it checks for an enqueued or processed status + /// # // and the task might already be processed when checking the status after the get_task call + /// # use meilisearch_sdk::{client::*, indexes::*, errors::ErrorCode}; + /// # + /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async { + /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap(); + /// let task_info = client + /// .create_index("is_pending", None) + /// .await + /// .unwrap(); + /// let task = client.get_task(task_info).await.unwrap(); + /// + /// assert!(task.is_pending()); + /// # task.wait_for_completion(&client, None, None).await.unwrap().try_make_index(&client).unwrap().delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); + /// # }); + /// ``` + #[must_use] + pub fn is_pending(&self) -> bool { + matches!(self, Self::Enqueued { .. } | Self::Processing { .. }) + } +} + +impl AsRef for Task { + fn as_ref(&self) -> &u32 { + match self { + Self::Enqueued { content } => content.as_ref(), + Self::Processing { content } => content.as_ref(), + Self::Succeeded { content } => content.as_ref(), + Self::Failed { content } => content.as_ref(), + } + } +} + +#[derive(Debug, Serialize, Clone)] +pub struct TasksPaginationFilters { + // Maximum number of tasks to return. + #[serde(skip_serializing_if = "Option::is_none")] + limit: Option, + // The first task uid that should be returned. + #[serde(skip_serializing_if = "Option::is_none")] + from: Option, +} + +#[derive(Debug, Serialize, Clone)] +pub struct TasksCancelFilters {} + +#[derive(Debug, Serialize, Clone)] +pub struct TasksDeleteFilters {} + +pub type TasksSearchQuery<'a, Http> = TasksQuery<'a, TasksPaginationFilters, Http>; +pub type TasksCancelQuery<'a, Http> = TasksQuery<'a, TasksCancelFilters, Http>; +pub type TasksDeleteQuery<'a, Http> = TasksQuery<'a, TasksDeleteFilters, Http>; + +#[derive(Debug, Serialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct TasksQuery<'a, T, Http: HttpClient> { + #[serde(skip_serializing)] + client: &'a Client, + // Index uids array to only retrieve the tasks of the indexes. + #[serde(skip_serializing_if = "Option::is_none")] + index_uids: Option>, + // Statuses array to only retrieve the tasks with these statuses. + #[serde(skip_serializing_if = "Option::is_none")] + statuses: Option>, + // Types array to only retrieve the tasks with these [TaskType]. + #[serde(skip_serializing_if = "Option::is_none", rename = "types")] + task_types: Option>, + // Uids of the tasks to retrieve. + #[serde(skip_serializing_if = "Option::is_none")] + uids: Option>, + // Uids of the tasks that canceled other tasks. + #[serde(skip_serializing_if = "Option::is_none")] + canceled_by: Option>, + // Date to retrieve all tasks that were enqueued before it. + #[serde( + skip_serializing_if = "Option::is_none", + serialize_with = "time::serde::rfc3339::option::serialize" + )] + before_enqueued_at: Option, + // Date to retrieve all tasks that were enqueued after it. + #[serde( + skip_serializing_if = "Option::is_none", + serialize_with = "time::serde::rfc3339::option::serialize" + )] + after_enqueued_at: Option, + // Date to retrieve all tasks that were started before it. + #[serde( + skip_serializing_if = "Option::is_none", + serialize_with = "time::serde::rfc3339::option::serialize" + )] + before_started_at: Option, + // Date to retrieve all tasks that were started after it. + #[serde( + skip_serializing_if = "Option::is_none", + serialize_with = "time::serde::rfc3339::option::serialize" + )] + after_started_at: Option, + // Date to retrieve all tasks that were finished before it. + #[serde( + skip_serializing_if = "Option::is_none", + serialize_with = "time::serde::rfc3339::option::serialize" + )] + before_finished_at: Option, + // Date to retrieve all tasks that were finished after it. + #[serde( + skip_serializing_if = "Option::is_none", + serialize_with = "time::serde::rfc3339::option::serialize" + )] + after_finished_at: Option, + + #[serde(flatten)] + pagination: T, +} + +#[allow(missing_docs)] +impl<'a, T, Http: HttpClient> TasksQuery<'a, T, Http> { + pub fn with_index_uids<'b>( + &'b mut self, + index_uids: impl IntoIterator, + ) -> &'b mut TasksQuery<'a, T, Http> { + self.index_uids = Some(index_uids.into_iter().collect()); + self + } + pub fn with_statuses<'b>( + &'b mut self, + statuses: impl IntoIterator, + ) -> &'b mut TasksQuery<'a, T, Http> { + self.statuses = Some(statuses.into_iter().collect()); + self + } + pub fn with_types<'b>( + &'b mut self, + task_types: impl IntoIterator, + ) -> &'b mut TasksQuery<'a, T, Http> { + self.task_types = Some(task_types.into_iter().collect()); + self + } + pub fn with_uids<'b>( + &'b mut self, + uids: impl IntoIterator, + ) -> &'b mut TasksQuery<'a, T, Http> { + self.uids = Some(uids.into_iter().collect()); + self + } + pub fn with_before_enqueued_at<'b>( + &'b mut self, + before_enqueued_at: &'a OffsetDateTime, + ) -> &'b mut TasksQuery<'a, T, Http> { + self.before_enqueued_at = Some(*before_enqueued_at); + self + } + pub fn with_after_enqueued_at<'b>( + &'b mut self, + after_enqueued_at: &'a OffsetDateTime, + ) -> &'b mut TasksQuery<'a, T, Http> { + self.after_enqueued_at = Some(*after_enqueued_at); + self + } + pub fn with_before_started_at<'b>( + &'b mut self, + before_started_at: &'a OffsetDateTime, + ) -> &'b mut TasksQuery<'a, T, Http> { + self.before_started_at = Some(*before_started_at); + self + } + pub fn with_after_started_at<'b>( + &'b mut self, + after_started_at: &'a OffsetDateTime, + ) -> &'b mut TasksQuery<'a, T, Http> { + self.after_started_at = Some(*after_started_at); + self + } + pub fn with_before_finished_at<'b>( + &'b mut self, + before_finished_at: &'a OffsetDateTime, + ) -> &'b mut TasksQuery<'a, T, Http> { + self.before_finished_at = Some(*before_finished_at); + self + } + pub fn with_after_finished_at<'b>( + &'b mut self, + after_finished_at: &'a OffsetDateTime, + ) -> &'b mut TasksQuery<'a, T, Http> { + self.after_finished_at = Some(*after_finished_at); + self + } + pub fn with_canceled_by<'b>( + &'b mut self, + task_uids: impl IntoIterator, + ) -> &'b mut TasksQuery<'a, T, Http> { + self.canceled_by = Some(task_uids.into_iter().collect()); + self + } +} + +impl<'a, Http: HttpClient> TasksQuery<'a, TasksCancelFilters, Http> { + #[must_use] + pub fn new(client: &'a Client) -> TasksQuery<'a, TasksCancelFilters, Http> { + TasksQuery { + client, + index_uids: None, + statuses: None, + task_types: None, + uids: None, + canceled_by: None, + before_enqueued_at: None, + after_enqueued_at: None, + before_started_at: None, + after_started_at: None, + before_finished_at: None, + after_finished_at: None, + pagination: TasksCancelFilters {}, + } + } + + pub async fn execute(&'a self) -> Result { + self.client.cancel_tasks_with(self).await + } +} + +impl<'a, Http: HttpClient> TasksQuery<'a, TasksDeleteFilters, Http> { + #[must_use] + pub fn new(client: &'a Client) -> TasksQuery<'a, TasksDeleteFilters, Http> { + TasksQuery { + client, + index_uids: None, + statuses: None, + task_types: None, + uids: None, + canceled_by: None, + before_enqueued_at: None, + after_enqueued_at: None, + before_started_at: None, + after_started_at: None, + before_finished_at: None, + after_finished_at: None, + pagination: TasksDeleteFilters {}, + } + } + + pub async fn execute(&'a self) -> Result { + self.client.delete_tasks_with(self).await + } +} + +impl<'a, Http: HttpClient> TasksQuery<'a, TasksPaginationFilters, Http> { + #[must_use] + pub fn new(client: &'a Client) -> TasksQuery<'a, TasksPaginationFilters, Http> { + TasksQuery { + client, + index_uids: None, + statuses: None, + task_types: None, + uids: None, + canceled_by: None, + before_enqueued_at: None, + after_enqueued_at: None, + before_started_at: None, + after_started_at: None, + before_finished_at: None, + after_finished_at: None, + pagination: TasksPaginationFilters { + limit: None, + from: None, + }, + } + } + pub fn with_limit<'b>( + &'b mut self, + limit: u32, + ) -> &'b mut TasksQuery<'a, TasksPaginationFilters, Http> { + self.pagination.limit = Some(limit); + self + } + pub fn with_from<'b>( + &'b mut self, + from: u32, + ) -> &'b mut TasksQuery<'a, TasksPaginationFilters, Http> { + self.pagination.from = Some(from); + self + } + pub async fn execute(&'a self) -> Result { + self.client.get_tasks_with(self).await + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::{ + client::*, + errors::{ErrorCode, ErrorType}, + }; + use big_s::S; + use meilisearch_test_macro::meilisearch_test; + use serde::{Deserialize, Serialize}; + use std::time::Duration; + + #[derive(Debug, Serialize, Deserialize, PartialEq)] + struct Document { + id: usize, + value: String, + kind: String, + } + + #[test] + fn test_deserialize_task() { + let datetime = OffsetDateTime::parse( + "2022-02-03T13:02:38.369634Z", + &time::format_description::well_known::Rfc3339, + ) + .unwrap(); + + let task: Task = serde_json::from_str( + r#" +{ + "enqueuedAt": "2022-02-03T13:02:38.369634Z", + "indexUid": "meili", + "status": "enqueued", + "type": "documentAdditionOrUpdate", + "uid": 12 +}"#, + ) + .unwrap(); + + assert!(matches!( + task, + Task::Enqueued { + content: EnqueuedTask { + enqueued_at, + index_uid: Some(index_uid), + update_type: TaskType::DocumentAdditionOrUpdate { details: None }, + uid: 12, + } + } + if enqueued_at == datetime && index_uid == "meili")); + + let task: Task = serde_json::from_str( + r#" +{ + "details": { + "indexedDocuments": null, + "receivedDocuments": 19547 + }, + "duration": null, + "enqueuedAt": "2022-02-03T15:17:02.801341Z", + "finishedAt": null, + "indexUid": "meili", + "startedAt": "2022-02-03T15:17:02.812338Z", + "status": "processing", + "type": "documentAdditionOrUpdate", + "uid": 14 +}"#, + ) + .unwrap(); + + assert!(matches!( + task, + Task::Processing { + content: ProcessingTask { + started_at, + update_type: TaskType::DocumentAdditionOrUpdate { + details: Some(DocumentAdditionOrUpdate { + received_documents: 19547, + indexed_documents: None, + }) + }, + uid: 14, + .. + } + } + if started_at == OffsetDateTime::parse( + "2022-02-03T15:17:02.812338Z", + &time::format_description::well_known::Rfc3339 + ).unwrap() + )); + + let task: Task = serde_json::from_str( + r#" +{ + "details": { + "indexedDocuments": 19546, + "receivedDocuments": 19547 + }, + "duration": "PT10.848957S", + "enqueuedAt": "2022-02-03T15:17:02.801341Z", + "finishedAt": "2022-02-03T15:17:13.661295Z", + "indexUid": "meili", + "startedAt": "2022-02-03T15:17:02.812338Z", + "status": "succeeded", + "type": "documentAdditionOrUpdate", + "uid": 14 +}"#, + ) + .unwrap(); + + assert!(matches!( + task, + Task::Succeeded { + content: SucceededTask { + update_type: TaskType::DocumentAdditionOrUpdate { + details: Some(DocumentAdditionOrUpdate { + received_documents: 19547, + indexed_documents: Some(19546), + }) + }, + uid: 14, + duration, + .. + } + } + if duration == Duration::from_millis(10_848) + )); + } + + #[meilisearch_test] + async fn test_wait_for_task_with_args(client: Client, movies: Index) -> Result<(), Error> { + let task = movies + .add_documents( + &[ + Document { + id: 0, + kind: "title".into(), + value: S("The Social Network"), + }, + Document { + id: 1, + kind: "title".into(), + value: S("Harry Potter and the Sorcerer's Stone"), + }, + ], + None, + ) + .await? + .wait_for_completion( + &client, + Some(Duration::from_millis(1)), + Some(Duration::from_millis(6000)), + ) + .await?; + + assert!(matches!(task, Task::Succeeded { .. })); + Ok(()) + } + + #[meilisearch_test] + async fn test_get_tasks_no_params() -> Result<(), Error> { + let mut s = mockito::Server::new_async().await; + let mock_server_url = s.url(); + let client = Client::new(mock_server_url, Some("masterKey")).unwrap(); + let path = "/tasks"; + + let mock_res = s.mock("GET", path).with_status(200).create_async().await; + let _ = client.get_tasks().await; + mock_res.assert_async().await; + + Ok(()) + } + + #[meilisearch_test] + async fn test_get_tasks_with_params() -> Result<(), Error> { + let mut s = mockito::Server::new_async().await; + let mock_server_url = s.url(); + let client = Client::new(mock_server_url, Some("masterKey")).unwrap(); + let path = + "/tasks?indexUids=movies,test&statuses=equeued&types=documentDeletion&uids=1&limit=0&from=1"; + + let mock_res = s.mock("GET", path).with_status(200).create_async().await; + + let mut query = TasksSearchQuery::new(&client); + query + .with_index_uids(["movies", "test"]) + .with_statuses(["equeued"]) + .with_types(["documentDeletion"]) + .with_from(1) + .with_limit(0) + .with_uids([&1]); + + let _ = client.get_tasks_with(&query).await; + + mock_res.assert_async().await; + + Ok(()) + } + + #[meilisearch_test] + async fn test_get_tasks_with_date_params() -> Result<(), Error> { + let mut s = mockito::Server::new_async().await; + let mock_server_url = s.url(); + let client = Client::new(mock_server_url, Some("masterKey")).unwrap(); + let path = "/tasks?\ + beforeEnqueuedAt=2022-02-03T13%3A02%3A38.369634Z\ + &afterEnqueuedAt=2023-02-03T13%3A02%3A38.369634Z\ + &beforeStartedAt=2024-02-03T13%3A02%3A38.369634Z\ + &afterStartedAt=2025-02-03T13%3A02%3A38.369634Z\ + &beforeFinishedAt=2026-02-03T13%3A02%3A38.369634Z\ + &afterFinishedAt=2027-02-03T13%3A02%3A38.369634Z"; + + let mock_res = s.mock("GET", path).with_status(200).create_async().await; + + let before_enqueued_at = OffsetDateTime::parse( + "2022-02-03T13:02:38.369634Z", + &time::format_description::well_known::Rfc3339, + ) + .unwrap(); + let after_enqueued_at = OffsetDateTime::parse( + "2023-02-03T13:02:38.369634Z", + &time::format_description::well_known::Rfc3339, + ) + .unwrap(); + let before_started_at = OffsetDateTime::parse( + "2024-02-03T13:02:38.369634Z", + &time::format_description::well_known::Rfc3339, + ) + .unwrap(); + + let after_started_at = OffsetDateTime::parse( + "2025-02-03T13:02:38.369634Z", + &time::format_description::well_known::Rfc3339, + ) + .unwrap(); + + let before_finished_at = OffsetDateTime::parse( + "2026-02-03T13:02:38.369634Z", + &time::format_description::well_known::Rfc3339, + ) + .unwrap(); + + let after_finished_at = OffsetDateTime::parse( + "2027-02-03T13:02:38.369634Z", + &time::format_description::well_known::Rfc3339, + ) + .unwrap(); + + let mut query = TasksSearchQuery::new(&client); + query + .with_before_enqueued_at(&before_enqueued_at) + .with_after_enqueued_at(&after_enqueued_at) + .with_before_started_at(&before_started_at) + .with_after_started_at(&after_started_at) + .with_before_finished_at(&before_finished_at) + .with_after_finished_at(&after_finished_at); + + let _ = client.get_tasks_with(&query).await; + + mock_res.assert_async().await; + + Ok(()) + } + + #[meilisearch_test] + async fn test_get_tasks_on_struct_with_params() -> Result<(), Error> { + let mut s = mockito::Server::new_async().await; + let mock_server_url = s.url(); + let client = Client::new(mock_server_url, Some("masterKey")).unwrap(); + let path = + "/tasks?indexUids=movies,test&statuses=equeued&types=documentDeletion&canceledBy=9"; + + let mock_res = s.mock("GET", path).with_status(200).create_async().await; + + let mut query = TasksSearchQuery::new(&client); + let _ = query + .with_index_uids(["movies", "test"]) + .with_statuses(["equeued"]) + .with_types(["documentDeletion"]) + .with_canceled_by([&9]) + .execute() + .await; + + mock_res.assert_async().await; + + Ok(()) + } + + #[meilisearch_test] + async fn test_get_tasks_with_none_existant_index_uids(client: Client) -> Result<(), Error> { + let mut query = TasksSearchQuery::new(&client); + query.with_index_uids(["no_name"]); + let tasks = client.get_tasks_with(&query).await.unwrap(); + + assert_eq!(tasks.results.len(), 0); + Ok(()) + } + + #[meilisearch_test] + async fn test_get_tasks_with_execute(client: Client) -> Result<(), Error> { + let tasks = TasksSearchQuery::new(&client) + .with_index_uids(["no_name"]) + .execute() + .await + .unwrap(); + + assert_eq!(tasks.results.len(), 0); + Ok(()) + } + + #[meilisearch_test] + async fn test_failing_task(client: Client, index: Index) -> Result<(), Error> { + let task_info = client.create_index(index.uid, None).await.unwrap(); + let task = client.get_task(task_info).await?; + let task = client.wait_for_task(task, None, None).await?; + + let error = task.unwrap_failure(); + assert_eq!(error.error_code, ErrorCode::IndexAlreadyExists); + assert_eq!(error.error_type, ErrorType::InvalidRequest); + Ok(()) + } + + #[meilisearch_test] + async fn test_cancel_tasks_with_params() -> Result<(), Error> { + let mut s = mockito::Server::new_async().await; + let mock_server_url = s.url(); + let client = Client::new(mock_server_url, Some("masterKey")).unwrap(); + let path = + "/tasks/cancel?indexUids=movies,test&statuses=equeued&types=documentDeletion&uids=1"; + + let mock_res = s.mock("POST", path).with_status(200).create_async().await; + + let mut query = TasksCancelQuery::new(&client); + query + .with_index_uids(["movies", "test"]) + .with_statuses(["equeued"]) + .with_types(["documentDeletion"]) + .with_uids([&1]); + + let _ = client.cancel_tasks_with(&query).await; + + mock_res.assert_async().await; + + Ok(()) + } + + #[meilisearch_test] + async fn test_cancel_tasks_with_params_execute() -> Result<(), Error> { + let mut s = mockito::Server::new_async().await; + let mock_server_url = s.url(); + let client = Client::new(mock_server_url, Some("masterKey")).unwrap(); + let path = + "/tasks/cancel?indexUids=movies,test&statuses=equeued&types=documentDeletion&uids=1"; + + let mock_res = s.mock("POST", path).with_status(200).create_async().await; + + let mut query = TasksCancelQuery::new(&client); + let _ = query + .with_index_uids(["movies", "test"]) + .with_statuses(["equeued"]) + .with_types(["documentDeletion"]) + .with_uids([&1]) + .execute() + .await; + + mock_res.assert_async().await; + + Ok(()) + } + + #[meilisearch_test] + async fn test_delete_tasks_with_params() -> Result<(), Error> { + let mut s = mockito::Server::new_async().await; + let mock_server_url = s.url(); + let client = Client::new(mock_server_url, Some("masterKey")).unwrap(); + let path = "/tasks?indexUids=movies,test&statuses=equeued&types=documentDeletion&uids=1"; + + let mock_res = s.mock("DELETE", path).with_status(200).create_async().await; + + let mut query = TasksDeleteQuery::new(&client); + query + .with_index_uids(["movies", "test"]) + .with_statuses(["equeued"]) + .with_types(["documentDeletion"]) + .with_uids([&1]); + + let _ = client.delete_tasks_with(&query).await; + + mock_res.assert_async().await; + + Ok(()) + } + + #[meilisearch_test] + async fn test_delete_tasks_with_params_execute() -> Result<(), Error> { + let mut s = mockito::Server::new_async().await; + let mock_server_url = s.url(); + let client = Client::new(mock_server_url, Some("masterKey")).unwrap(); + let path = "/tasks?indexUids=movies,test&statuses=equeued&types=documentDeletion&uids=1"; + + let mock_res = s.mock("DELETE", path).with_status(200).create_async().await; + + let mut query = TasksDeleteQuery::new(&client); + let _ = query + .with_index_uids(["movies", "test"]) + .with_statuses(["equeued"]) + .with_types(["documentDeletion"]) + .with_uids([&1]) + .execute() + .await; + + mock_res.assert_async().await; + + Ok(()) + } +} diff --git a/backend/vendor/meilisearch-sdk/src/tenant_tokens.rs b/backend/vendor/meilisearch-sdk/src/tenant_tokens.rs new file mode 100644 index 000000000..bb561f712 --- /dev/null +++ b/backend/vendor/meilisearch-sdk/src/tenant_tokens.rs @@ -0,0 +1,177 @@ +use crate::errors::Error; +use jsonwebtoken::{encode, EncodingKey, Header}; +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use time::OffsetDateTime; +#[cfg(not(target_arch = "wasm32"))] +use uuid::Uuid; + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +struct TenantTokenClaim { + api_key_uid: String, + search_rules: Value, + #[serde(with = "time::serde::timestamp::option")] + exp: Option, +} + +pub fn generate_tenant_token( + api_key_uid: String, + search_rules: Value, + api_key: impl AsRef, + expires_at: Option, +) -> Result { + // Validate uuid format + let uid = Uuid::try_parse(&api_key_uid)?; + + // Validate uuid version + if uid.get_version_num() != 4 { + return Err(Error::InvalidUuid4Version); + } + + if expires_at.map_or(false, |expires_at| OffsetDateTime::now_utc() > expires_at) { + return Err(Error::TenantTokensExpiredSignature); + } + + let claims = TenantTokenClaim { + api_key_uid, + exp: expires_at, + search_rules, + }; + + let token = encode( + &Header::default(), + &claims, + &EncodingKey::from_secret(api_key.as_ref().as_bytes()), + ); + + Ok(token?) +} + +#[cfg(test)] +mod tests { + use crate::tenant_tokens::*; + use big_s::S; + use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation}; + use serde_json::json; + use std::collections::HashSet; + + const SEARCH_RULES: [&str; 1] = ["*"]; + const VALID_KEY: &str = "a19b6ec84ee31324efa560cd1f7e6939"; + + fn build_validation() -> Validation { + let mut validation = Validation::new(Algorithm::HS256); + validation.validate_exp = false; + validation.required_spec_claims = HashSet::new(); + + validation + } + + #[test] + fn test_generate_token_with_given_key() { + let api_key_uid = S("76cf8b87-fd12-4688-ad34-260d930ca4f4"); + let token = + generate_tenant_token(api_key_uid, json!(SEARCH_RULES), VALID_KEY, None).unwrap(); + + let valid_key = decode::( + &token, + &DecodingKey::from_secret(VALID_KEY.as_ref()), + &build_validation(), + ); + let invalid_key = decode::( + &token, + &DecodingKey::from_secret("not-the-same-key".as_ref()), + &build_validation(), + ); + + assert!(valid_key.is_ok()); + assert!(invalid_key.is_err()); + } + + #[test] + fn test_generate_token_without_uid() { + let api_key_uid = S(""); + let key = S(""); + let token = generate_tenant_token(api_key_uid, json!(SEARCH_RULES), key, None); + + assert!(token.is_err()); + } + + #[test] + fn test_generate_token_with_expiration() { + let api_key_uid = S("76cf8b87-fd12-4688-ad34-260d930ca4f4"); + let exp = OffsetDateTime::now_utc() + time::Duration::HOUR; + let token = + generate_tenant_token(api_key_uid, json!(SEARCH_RULES), VALID_KEY, Some(exp)).unwrap(); + + let decoded = decode::( + &token, + &DecodingKey::from_secret(VALID_KEY.as_ref()), + &Validation::new(Algorithm::HS256), + ); + + assert!(decoded.is_ok()); + } + + #[test] + fn test_generate_token_with_expires_at_in_the_past() { + let api_key_uid = S("76cf8b87-fd12-4688-ad34-260d930ca4f4"); + let exp = OffsetDateTime::now_utc() - time::Duration::HOUR; + let token = generate_tenant_token(api_key_uid, json!(SEARCH_RULES), VALID_KEY, Some(exp)); + + assert!(token.is_err()); + } + + #[test] + fn test_generate_token_contains_claims() { + let api_key_uid = S("76cf8b87-fd12-4688-ad34-260d930ca4f4"); + let token = + generate_tenant_token(api_key_uid.clone(), json!(SEARCH_RULES), VALID_KEY, None) + .unwrap(); + + let decoded = decode::( + &token, + &DecodingKey::from_secret(VALID_KEY.as_ref()), + &build_validation(), + ) + .expect("Cannot decode the token"); + + assert_eq!(decoded.claims.api_key_uid, api_key_uid); + assert_eq!(decoded.claims.search_rules, json!(SEARCH_RULES)); + } + + #[test] + fn test_generate_token_with_multi_byte_chars() { + let api_key_uid = S("76cf8b87-fd12-4688-ad34-260d930ca4f4"); + let key = "Ëa1ทt9bVcL-vãUทtP3OpXW5qPc%bWH5ทvw09"; + let token = + generate_tenant_token(api_key_uid.clone(), json!(SEARCH_RULES), key, None).unwrap(); + + let decoded = decode::( + &token, + &DecodingKey::from_secret(key.as_ref()), + &build_validation(), + ) + .expect("Cannot decode the token"); + + assert_eq!(decoded.claims.api_key_uid, api_key_uid); + } + + #[test] + fn test_generate_token_with_wrongly_formatted_uid() { + let api_key_uid = S("xxx"); + let key = "Ëa1ทt9bVcL-vãUทtP3OpXW5qPc%bWH5ทvw09"; + let token = generate_tenant_token(api_key_uid, json!(SEARCH_RULES), key, None); + + assert!(token.is_err()); + } + + #[test] + fn test_generate_token_with_wrong_uid_version() { + let api_key_uid = S("6a11eb96-2485-11ed-861d-0242ac120002"); + let key = "Ëa1ทt9bVcL-vãUทtP3OpXW5qPc%bWH5ทvw09"; + let token = generate_tenant_token(api_key_uid, json!(SEARCH_RULES), key, None); + + assert!(token.is_err()); + } +} diff --git a/backend/vendor/meilisearch-sdk/src/utils.rs b/backend/vendor/meilisearch-sdk/src/utils.rs new file mode 100644 index 000000000..65f93ff78 --- /dev/null +++ b/backend/vendor/meilisearch-sdk/src/utils.rs @@ -0,0 +1,45 @@ +use std::time::Duration; + +#[cfg(not(target_arch = "wasm32"))] +pub(crate) async fn async_sleep(interval: Duration) { + let (sender, receiver) = futures::channel::oneshot::channel::<()>(); + std::thread::spawn(move || { + std::thread::sleep(interval); + let _ = sender.send(()); + }); + let _ = receiver.await; +} + +#[cfg(target_arch = "wasm32")] +pub(crate) async fn async_sleep(interval: Duration) { + use std::convert::TryInto; + use wasm_bindgen_futures::JsFuture; + + JsFuture::from(web_sys::js_sys::Promise::new(&mut |yes, _| { + web_sys::window() + .unwrap() + .set_timeout_with_callback_and_timeout_and_arguments_0( + &yes, + interval.as_millis().try_into().unwrap(), + ) + .unwrap(); + })) + .await + .unwrap(); +} + +#[cfg(test)] +mod test { + use super::*; + use meilisearch_test_macro::meilisearch_test; + + #[meilisearch_test] + async fn test_async_sleep() { + let sleep_duration = Duration::from_millis(10); + let now = std::time::Instant::now(); + + async_sleep(sleep_duration).await; + + assert!(now.elapsed() >= sleep_duration); + } +} diff --git a/docs/docs/setup/requirements.md b/docs/docs/setup/requirements.md index 5d1a2bbe9..eebc787ca 100644 --- a/docs/docs/setup/requirements.md +++ b/docs/docs/setup/requirements.md @@ -8,7 +8,7 @@ To run, Tobira requires: - A Unix system. - A **PostgreSQL** (≥12) database (see below for further requirements). -- [**Meilisearch**](https://www.meilisearch.com/) (≥ v1.4). For installation, see [Meili's docs](https://docs.meilisearch.com/learn/getting_started/quick_start.html#step-1-setup-and-installation). +- [**Meilisearch**](https://www.meilisearch.com/) (≥ v1.12). For installation, see [Meili's docs](https://docs.meilisearch.com/learn/getting_started/quick_start.html#step-1-setup-and-installation). - An **Opencast** that satisfies certain condition. See below. diff --git a/frontend/src/routes/Search.tsx b/frontend/src/routes/Search.tsx index ca3cc1a79..d0170a184 100644 --- a/frontend/src/routes/Search.tsx +++ b/frontend/src/routes/Search.tsx @@ -174,6 +174,7 @@ const query = graphql` title { start len } description { start len } seriesTitle { start len } + creators { index span { start len }} } } ... on SearchSeries { @@ -528,6 +529,11 @@ const SearchEvent: React.FC = ({ ? DirectVideoRoute.url({ videoId: id }) : VideoRoute.url({ realmPath: hostRealms[0].path, videoID: id }); + const highlightedCreators = creators.map((c, i) => { + const relevantMatches = matches.creators.filter(m => m.index === i).map(m => m.span); + return <>{highlightText(c, relevantMatches)}; + }); + return ( {{ image: @@ -575,7 +581,7 @@ const SearchEvent: React.FC = ({ - = ({ li: { display: "inline", }, + mark: highlightCss(COLORS.neutral90), }} /> diff --git a/frontend/src/schema.graphql b/frontend/src/schema.graphql index 201cc2cdd..a85df02b0 100644 --- a/frontend/src/schema.graphql +++ b/frontend/src/schema.graphql @@ -230,6 +230,11 @@ type AclItem { info: RoleInfo } +type ArrayMatch { + index: Int! + span: ByteSpan! +} + type AuthorizedEvent implements Node { id: ID! opencastId: String! @@ -740,6 +745,7 @@ type SearchEventMatches { title: [ByteSpan!]! description: [ByteSpan!]! seriesTitle: [ByteSpan!]! + creators: [ArrayMatch!]! } type SearchPlaylist implements Node { diff --git a/frontend/src/ui/Video.tsx b/frontend/src/ui/Video.tsx index f0e4eca3f..77d8ae800 100644 --- a/frontend/src/ui/Video.tsx +++ b/frontend/src/ui/Video.tsx @@ -277,7 +277,7 @@ export const ThumbnailImg: React.FC<{ src: string; alt: string }> = ({ src, alt }; type CreatorsProps = { - creators: readonly string[] | null; + creators: readonly (JSX.Element | string)[] | null; className?: string; }; diff --git a/util/containers/docker-compose.yml b/util/containers/docker-compose.yml index 3983c1a1f..adbded135 100644 --- a/util/containers/docker-compose.yml +++ b/util/containers/docker-compose.yml @@ -34,7 +34,7 @@ services: # A MeiliSearch for Tobira. tobira-meilisearch: - image: getmeili/meilisearch:v1.4 + image: getmeili/meilisearch:v1.12 restart: unless-stopped ports: - 127.0.0.1:7700:7700 diff --git a/util/scripts/check-system.sh b/util/scripts/check-system.sh index e887af598..3c9878df5 100755 --- a/util/scripts/check-system.sh +++ b/util/scripts/check-system.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash -MIN_RUST_VERSION="1.74.0" +MIN_RUST_VERSION="1.80.0" MIN_NPM_VERSION="7.0" has_command() {