diff --git a/.code-samples.meilisearch.yaml b/.code-samples.meilisearch.yaml index d19f3d34..5d571fa8 100644 --- a/.code-samples.meilisearch.yaml +++ b/.code-samples.meilisearch.yaml @@ -6,19 +6,37 @@ get_one_index_1: |- let movies: Index = client.get_index("movies").await.unwrap(); list_all_indexes_1: |- - let indexes: Vec = client.list_all_indexes().await.unwrap(); + let mut indexes = IndexesQuery::new(&client) + .with_limit(3) + .execute() + .await + .unwrap(); create_an_index_1: |- client.create_index("movies", Some("id")).await.unwrap(); update_an_index_1: |- - client.index("movies").update("movie_review_id").await.unwrap(); + let task = IndexUpdater::new("movies", &client) + .with_primary_key("movie_review_id") + .execute() + .await + .unwrap(); delete_an_index_1: |- client.index("movies").delete().await.unwrap(); get_one_document_1: |- - let movie: Movie = client.index("movies").get_document(String::from("25684")).await.unwrap(); + let index = client.index("movies"); + let document = DocumentQuery::new(&index) + .with_fields(["id", "title", "poster", "release_date"]) + .execute::("25684") + .await + .unwrap(); get_documents_1: |- - let documents: Vec = client.index("movies").get_documents(None, Some(2), None).await.unwrap(); + let index = client.index("movies"); + let documents: DocumentsResults = DocumentsQuery::new(&index) + .with_limit(2) + .execute::() + .await + .unwrap(); add_or_replace_documents_1: |- - let task: Task = client.index("movies").add_or_replace(&[ + let task: TaskInfo = client.index("movies").add_or_replace(&[ Movie { id: 287947, title: "Shazam".to_string(), @@ -35,18 +53,18 @@ add_or_update_documents_1: |- title: String } - let task: Task = client.index("movies").add_or_update(&[ + let task: TaskInfo = client.index("movies").add_or_update(&[ IncompleteMovie { id: 287947, title: "Shazam ⚡️".to_string() } ], None).await.unwrap(); delete_all_documents_1: |- - let task: Task = client.index("movies").delete_all_documents().await.unwrap(); + let task: TaskInfo = client.index("movies").delete_all_documents().await.unwrap(); delete_one_document_1: |- - let task: Task = client.index("movies").delete_document(25684).await.unwrap(); + let task: TaskInfo = client.index("movies").delete_document(25684).await.unwrap(); delete_documents_1: |- - let task: Task = client.index("movies").delete_documents(&[23488, 153738, 437035, 363869]).await.unwrap(); + let task: TaskInfo = client.index("movies").delete_documents(&[23488, 153738, 437035, 363869]).await.unwrap(); search_post_1: |- let results: SearchResults = client.index("movies") .search() @@ -54,16 +72,40 @@ search_post_1: |- .execute() .await .unwrap(); -get_task_by_index_1: |- - let task: Task = client.index("movies").get_task(1).await.unwrap(); -get_all_tasks_by_index_1: |- - let tasks: Vec = client.index("movies").get_tasks().await.unwrap(); get_all_tasks_1: |- - let tasks: Vec = client.get_tasks().await.unwrap(); + let tasks: TasksResults = client.get_tasks().await.unwrap(); +get_all_tasks_filtering_1: |- + let mut query = TasksQuery::new(&client) + .with_index_uid(["movies"]) + .execute() + .await + .unwrap(); +get_all_tasks_filtering_2: |- + let mut query = TasksQuery::new(&client) + .with_status(["succeeded", "failed"]) + .with_type(["documentAdditionOrUpdate"]) + .execute() + .await + .unwrap(); +get_all_tasks_paginating_1: |- + let mut query = TasksQuery::new(&client) + .with_limit(2) + .with_from(10) + .execute() + .await + .unwrap(); +get_all_tasks_paginating_2: |- + let mut query = TasksQuery::new(&client) + .with_limit(2) + .from(8) + .execute() + .await + .unwrap(); get_task_1: |- let task: Task = client.get_task(1).await.unwrap(); get_settings_1: |- let settings: Settings = client.index("movies").get_settings().await.unwrap(); +# Cannot be updated until API faceting and pagination are added update_settings_1: |- let mut synonyms = std::collections::HashMap::new(); synonyms.insert(String::from("wolverine"), vec!["xmen", "logan"]); @@ -103,9 +145,9 @@ update_settings_1: |- ]) .with_synonyms(synonyms); - let task: Task = client.index("movies").set_settings(&settings).await.unwrap(); + let task: TaskInfo = client.index("movies").set_settings(&settings).await.unwrap(); reset_settings_1: |- - let task: Task = client.index("movies").reset_settings().await.unwrap(); + let task: TaskInfo = client.index("movies").reset_settings().await.unwrap(); get_synonyms_1: |- let synonyms: HashMap> = client.index("movies").get_synonyms().await.unwrap(); update_synonyms_1: |- @@ -114,16 +156,16 @@ update_synonyms_1: |- 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: Task = client.index("movies").set_synonyms(&synonyms).await.unwrap(); + let task: TaskInfo = client.index("movies").set_synonyms(&synonyms).await.unwrap(); reset_synonyms_1: |- - let task: Task = client.index("movies").reset_synonyms().await.unwrap(); + let task: TaskInfo = client.index("movies").reset_synonyms().await.unwrap(); get_stop_words_1: |- let stop_words: Vec = client.index("movies").get_stop_words().await.unwrap(); update_stop_words_1: |- let stop_words = ["of", "the", "to"]; - let task: Task = client.index("movies").set_stop_words(&stop_words).await.unwrap(); + let task: TaskInfo = client.index("movies").set_stop_words(&stop_words).await.unwrap(); reset_stop_words_1: |- - let task: Task = client.index("movies").reset_stop_words().await.unwrap(); + let task: TaskInfo = client.index("movies").reset_stop_words().await.unwrap(); get_ranking_rules_1: |- let ranking_rules: Vec = client.index("movies").get_ranking_rules().await.unwrap(); update_ranking_rules_1: |- @@ -138,15 +180,15 @@ update_ranking_rules_1: |- "rank:desc", ]; - let task: Task = client.index("movies").set_ranking_rules(&ranking_rules).await.unwrap(); + let task: TaskInfo = client.index("movies").set_ranking_rules(&ranking_rules).await.unwrap(); reset_ranking_rules_1: |- - let task: Task = client.index("movies").reset_ranking_rules().await.unwrap(); + let task: TaskInfo = client.index("movies").reset_ranking_rules().await.unwrap(); get_distinct_attribute_1: |- let distinct_attribute: Option = client.index("shoes").get_distinct_attribute().await.unwrap(); update_distinct_attribute_1: |- - let task: Task = client.index("shoes").set_distinct_attribute("skuid").await.unwrap(); + let task: TaskInfo = client.index("shoes").set_distinct_attribute("skuid").await.unwrap(); reset_distinct_attribute_1: |- - let task: Task = client.index("shoes").reset_distinct_attribute().await.unwrap(); + let task: TaskInfo = client.index("shoes").reset_distinct_attribute().await.unwrap(); get_searchable_attributes_1: |- let searchable_attributes: Vec = client.index("movies").get_searchable_attributes().await.unwrap(); update_searchable_attributes_1: |- @@ -156,9 +198,9 @@ update_searchable_attributes_1: |- "genres" ]; - let task: Task = client.index("movies").set_searchable_attributes(&searchable_attributes).await.unwrap(); + let task: TaskInfo = client.index("movies").set_searchable_attributes(&searchable_attributes).await.unwrap(); reset_searchable_attributes_1: |- - let task: Task = client.index("movies").reset_searchable_attributes().await.unwrap(); + let task: TaskInfo = client.index("movies").reset_searchable_attributes().await.unwrap(); get_filterable_attributes_1: |- let filterable_attributes: Vec = client.index("movies").get_filterable_attributes().await.unwrap(); update_filterable_attributes_1: |- @@ -167,9 +209,9 @@ update_filterable_attributes_1: |- "director" ]; - let task: Task = client.index("movies").set_filterable_attributes(&filterable_attributes).await.unwrap(); + let task: TaskInfo = client.index("movies").set_filterable_attributes(&filterable_attributes).await.unwrap(); reset_filterable_attributes_1: |- - let task: Task = client.index("movies").reset_filterable_attributes().await.unwrap(); + let task: TaskInfo = client.index("movies").reset_filterable_attributes().await.unwrap(); get_displayed_attributes_1: |- let displayed_attributes: Vec = client.index("movies").get_displayed_attributes().await.unwrap(); update_displayed_attributes_1: |- @@ -180,9 +222,9 @@ update_displayed_attributes_1: |- "release_date" ]; - let task: Task = client.index("movies").set_displayed_attributes(&displayed_attributes).await.unwrap(); + let task: TaskInfo = client.index("movies").set_displayed_attributes(&displayed_attributes).await.unwrap(); reset_displayed_attributes_1: |- - let task: Task = client.index("movies").reset_displayed_attributes().await.unwrap(); + let task: TaskInfo = client.index("movies").reset_displayed_attributes().await.unwrap(); get_index_stats_1: |- let stats: IndexStats = client.index("movies").get_stats().await.unwrap(); get_indexes_stats_1: |- @@ -193,7 +235,7 @@ get_health_1: |- get_version_1: |- let version: Version = client.get_version().await.unwrap(); distinct_attribute_guide_1: |- - let task: Task = client.index("jackets").set_distinct_attribute("product_id").await.unwrap(); + let task: TaskInfo = client.index("jackets").set_distinct_attribute("product_id").await.unwrap(); field_properties_guide_searchable_1: |- let searchable_attributes = [ "title", @@ -201,7 +243,7 @@ field_properties_guide_searchable_1: |- "genres" ]; - let task: Task = client.index("movies").set_searchable_attributes(&searchable_attributes).await.unwrap(); + let task: TaskInfo = client.index("movies").set_searchable_attributes(&searchable_attributes).await.unwrap(); field_properties_guide_displayed_1: |- let displayed_attributes = [ "title", @@ -210,7 +252,7 @@ field_properties_guide_displayed_1: |- "release_date" ]; - let task: Task = client.index("movies").set_displayed_attributes(&displayed_attributes).await.unwrap(); + let task: TaskInfo = client.index("movies").set_displayed_attributes(&displayed_attributes).await.unwrap(); filtering_guide_1: |- let results: SearchResults = client.index("movies").search() .with_query("Avengers") @@ -303,16 +345,16 @@ search_parameter_guide_highlight_tag_1: |- // Get the formatted results let formatted_results: Vec<&Movie> = results.hits.iter().map(|r| r.formatted_result.as_ref().unwrap()).collect(); -search_parameter_guide_matches_1: |- +search_parameter_guide_show_matches_position_1: |- let results: SearchResults = client.index("movies").search() .with_query("winter feast") - .with_matches(true) + .with_show_matches_position(true) .execute() .await .unwrap(); // Get the matches info - let matched_info: Vec<&HashMap>> = results.hits.iter().map(|r| r.matches_info.as_ref().unwrap()).collect(); + let matches_position: Vec<&HashMap>> = results.hits.iter().map(|r| r.matches_position.as_ref().unwrap()).collect(); settings_guide_synonyms_1: |- let mut synonyms = HashMap::new(); synonyms.insert(String::from("sweater"), vec![String::from("jumper")]); @@ -330,7 +372,7 @@ settings_guide_stop_words_1: |- "an" ]); - let task = client.index("movies").set_settings(&settings).await.unwrap(); + let task: TaskInfo = client.index("movies").set_settings(&settings).await.unwrap(); settings_guide_filterable_attributes_1: |- let settings = Settings::new() .with_filterable_attributes([ @@ -338,7 +380,7 @@ settings_guide_filterable_attributes_1: |- "genres" ]); - let task: Task = client.index("movies").set_settings(&settings).await.unwrap(); + let task: TaskInfo = client.index("movies").set_settings(&settings).await.unwrap(); settings_guide_ranking_rules_1: |- let settings = Settings::new() .with_ranking_rules([ @@ -352,12 +394,12 @@ settings_guide_ranking_rules_1: |- "rank:desc", ]); - let task = client.index("movies").set_settings(&settings).await.unwrap(); + let task: TaskInfo = client.index("movies").set_settings(&settings).await.unwrap(); settings_guide_distinct_1: |- let settings = Settings::new() .with_distinct_attribute("product_id"); - let task: Task = client.index("jackets").set_settings(&settings).await.unwrap(); + let task: TaskInfo = client.index("jackets").set_settings(&settings).await.unwrap(); settings_guide_searchable_1: |- let settings = Settings::new() .with_searchable_attributes([ @@ -366,7 +408,7 @@ settings_guide_searchable_1: |- "genres" ]); - let task: Task = client.index("movies").set_settings(&settings).await.unwrap(); + let task: TaskInfo = client.index("movies").set_settings(&settings).await.unwrap(); settings_guide_displayed_1: |- let settings = Settings::new() .with_displayed_attributes([ @@ -376,7 +418,7 @@ settings_guide_displayed_1: |- "release_date" ]); - let task: Task = client.index("movies").set_settings(&settings).await.unwrap(); + let task: TaskInfo = client.index("movies").set_settings(&settings).await.unwrap(); settings_guide_sortable_1: |- let settings = Settings::new() .with_sortable_attributes([ @@ -384,7 +426,7 @@ settings_guide_sortable_1: |- "price" ]); - let task: Task = client.index("books").set_settings(&settings).await.unwrap(); + let task: TaskInfo = client.index("books").set_settings(&settings).await.unwrap(); add_movies_json_1: |- use meilisearch_sdk::{ indexes::*, @@ -417,14 +459,18 @@ documents_guide_add_movie_1: |- } // Add a document to our index - let task: Task = client.index("movies").add_documents(&[ + let task: TaskInfo = client.index("movies").add_documents(&[ IncompleteMovie { id: "123sq178".to_string(), title: "Amélie Poulain".to_string(), } ], None).await.unwrap(); primary_field_guide_update_document_primary_key: |- - client.index("books").update("title").await.unwrap(); + let task = IndexUpdater::new("books", &client) + .with_primary_key("title") + .execute() + .await + .unwrap(); primary_field_guide_create_index_primary_key: |- client.create_index("books", Some("reference_number")).await.unwrap(); primary_field_guide_add_document_primary_key: |- @@ -437,7 +483,7 @@ primary_field_guide_add_document_primary_key: |- price: f64 } - let task: Task = client.index("books").add_documents(&[ + let task: TaskInfo = client.index("books").add_documents(&[ Book { reference_number: "287947".to_string(), title: "Diary of a Wimpy Kid".to_string(), @@ -449,7 +495,7 @@ primary_field_guide_add_document_primary_key: |- getting_started_add_documents_md: |- ```toml [dependencies] - meilisearch-sdk = "0.17" + meilisearch-sdk = "0.18" # futures: because we want to block on futures futures = "0.3" # serde: required if you are going to use documents @@ -564,7 +610,7 @@ getting_started_update_stop_words: |- let stop_words = ["the"]; client.index("movies").set_stop_words(&stop_words).await.unwrap(); getting_started_check_task_status: |- - client.index("movies").get_task(0).await.unwrap(); + client.get_task(0).await.unwrap(); getting_started_synonyms: |- let mut synonyms = std::collections::HashMap::new(); synonyms.insert(String::from("winnie"), vec![String::from("piglet")]); @@ -616,7 +662,7 @@ getting_started_configure_settings: |- "mass", "_geo" ]) - let task: Task = client.index("meteorites").set_settings(&settings).await.unwrap(); + let task: TaskInfo = client.index("meteorites").set_settings(&settings).await.unwrap(); getting_started_geo_radius: |- let results: SearchResults = client.index("meteorites").search() .with_filter("_geoRadius(46.9480, 7.4474, 210000)") @@ -643,7 +689,7 @@ getting_started_filtering: |- .await .unwrap(); faceted_search_update_settings_1: |- - let task: Task = client.index("movies").set_filterable_attributes(["director", "genres"]).await.unwrap(); + let task: TaskInfo = client.index("movies").set_filterable_attributes(["director", "genres"]).await.unwrap(); faceted_search_filter_1: |- let results: SearchResults = client.index("movies").search() .with_query("thriller") @@ -651,14 +697,14 @@ faceted_search_filter_1: |- .execute() .await .unwrap(); -faceted_search_facets_distribution_1: |- +faceted_search_facets_1: |- let results: SearchResults = client.index("movies").search() .with_query("Batman") - .with_facets_distribution(Selectors::Some(&["genres"])) + .with_facets(Selectors::Some(&["genres"])) .execute() .await .unwrap(); - let genres: &HashMap = results.facets_distribution.unwrap().get("genres").unwrap(); + let genres: &HashMap = results.facet_distribution.unwrap().get("genres").unwrap(); faceted_search_walkthrough_filter_1: |- let results: SearchResults = client.index("movies").search() .with_query("thriller") @@ -668,8 +714,6 @@ faceted_search_walkthrough_filter_1: |- .unwrap(); post_dump_1: |- client.create_dump().await.unwrap(); -get_dump_status_1: |- - client.get_dump_status("20201101-110357260").await.unwrap(); phrase_search_1: |- let results: SearchResults = client.index("movies") .search() @@ -683,7 +727,7 @@ sorting_guide_update_sortable_attributes_1: |- "price" ]; - let task: Task = client.index("books").set_sortable_attributes(&sortable_attributes).await.unwrap(); + let task: TaskInfo = client.index("books").set_sortable_attributes(&sortable_attributes).await.unwrap(); sorting_guide_update_ranking_rules_1: |- let ranking_rules = [ "words", @@ -694,7 +738,7 @@ sorting_guide_update_ranking_rules_1: |- "exactness" ]; - let task: Task = client.index("books").set_ranking_rules(&ranking_rules).await.unwrap(); + let task: TaskInfo = client.index("books").set_ranking_rules(&ranking_rules).await.unwrap(); sorting_guide_sort_parameter_1: |- let results: SearchResults = client.index("books").search() .with_query("science fiction") @@ -717,9 +761,9 @@ update_sortable_attributes_1: |- "author" ]; - let task: Task = client.index("books").set_sortable_attributes(&sortable_attributes).await.unwrap(); + let task: TaskInfo = client.index("books").set_sortable_attributes(&sortable_attributes).await.unwrap(); reset_sortable_attributes_1: |- - let task: Task = client.index("books").reset_sortable_attributes().await.unwrap(); + let task: TaskInfo = client.index("books").reset_sortable_attributes().await.unwrap(); search_parameter_guide_sort_1: |- let results: SearchResults = client.index("books").search() .with_query("science fiction") @@ -728,7 +772,7 @@ search_parameter_guide_sort_1: |- .await .unwrap(); geosearch_guide_filter_settings_1: |- - let task: Task = client.index("restaurants").set_filterable_attributes(&["_geo"]).await.unwrap(); + let task: TaskInfo = client.index("restaurants").set_filterable_attributes(&["_geo"]).await.unwrap(); geosearch_guide_filter_usage_1: |- let results: SearchResults = client.index("restaurants").search() .with_filter("_geoRadius(45.472735, 9.184019, 2000)") @@ -742,7 +786,7 @@ geosearch_guide_filter_usage_2: |- .await .unwrap(); geosearch_guide_sort_settings_1: |- - let task: Task = client.index("restaurants").set_sortable_attributes(&["_geo"]).await.unwrap(); + let task: TaskInfo = client.index("restaurants").set_sortable_attributes(&["_geo"]).await.unwrap(); geosearch_guide_sort_usage_1: |- let results: SearchResults = client.index("restaurants").search() .with_sort(&["_geoPoint(48.8561446, 2.2978204):asc"]) @@ -756,26 +800,31 @@ geosearch_guide_sort_usage_2: |- .await .unwrap(); get_one_key_1: |- - let key = client.get_key("d0552b41536279a0ad88bd595327b96f01176a60c2243e906c52ac02375f9bc4").await.unwrap(); + let key = client.get_key("6062abda-a5aa-4414-ac91-ecd7944c0f8d").await.unwrap(); get_all_keys_1: |- - let keys = client.get_keys().await.unwrap(); + let mut query = KeysQuery::new() + .with_limit(3) + .execute(&client) + .await + .unwrap(); create_a_key_1: |- let mut key_options = KeyBuilder::new("Add documents: Products API key"); - key_options.with_action(Action::DocumentsAdd) - .with_expires_at(time::macros::datetime!(2042 - 04 - 02 00:42:42 UTC)) - .with_index("products"); + key_options + .with_action(Action::DocumentsAdd) + .with_expires_at(time::macros::datetime!(2042 - 04 - 02 00:42:42 UTC)) + .with_index("products"); let new_key = client.create_key(key_options).await.unwrap(); update_a_key_1: |- - let mut key = client.get_key("d0552b41536279a0ad88bd595327b96f01176a60c2243e906c52ac02375f9bc4").await.unwrap(); + let mut key = client.get_key("6062abda-a5aa-4414-ac91-ecd7944c0f8d").await.unwrap(); key .with_description("Manage documents: Products/Reviews API key".to_string()) - .with_actions(vec![Action::DocumentsAdd, Action::DocumentsDelete]) - .with_indexes(vec!["products".to_string(), "reviews".to_string()]) - .with_expires_at(time::macros::datetime!(2042 - 04 - 02 00:42:42 UTC)) - .update(&client); + .with_name("Products/Reviews API key".to_string()) + .update(&client) + .await + .unwrap(); delete_a_key_1: |- - let key = client.get_key("d0552b41536279a0ad88bd595327b96f01176a60c2243e906c52ac02375f9bc4").await.unwrap(); - client.delete_key(&key); + let key = client.get_key("6062abda-a5aa-4414-ac91-ecd7944c0f8d").await.unwrap(); + client.delete_key(&key).await?; authorization_header_1: let client = Client::new("http://localhost:7700", "masterKey"); let keys = client.get_keys().await.unwrap(); @@ -784,22 +833,23 @@ security_guide_search_key_1: |- let result = client.index("patient_medical_records").search().execute().await.unwrap(); security_guide_update_key_1: |- let client = Client::new("http://localhost:7700", "masterKey"); - let mut key = client.get_key("d0552b41536279a0ad88bd595327b96f01176a60c2243e906c52ac02375f9bc4").await.unwrap(); - key.with_indexes(vec!["doctors".to_string()]).update(&client); + let mut key = client.get_key("74c9c733-3368-4738-bbe5-1d18a5fecb37").await.unwrap(); + key.with_description("Default Search API key".to_string()).update(&client); security_guide_create_key_1: |- let client = Client::new("http://localhost:7700", "masterKey"); let mut key_options = KeyBuilder::new("Search patient records key"); - key_options.with_action(Action::Search) - .with_expires_at(time::macros::datetime!(2023 - 01 - 01 00:00:00 UTC)) - .with_index("patient_medical_records"); + key_options + .with_action(Action::Search) + .with_expires_at(time::macros::datetime!(2023 - 01 - 01 00:00:00 UTC)) + .with_index("patient_medical_records"); let new_key = client.create_key(key_options).await.unwrap(); security_guide_list_keys_1: |- let client = Client::new("http://localhost:7700", "masterKey"); let keys = client.get_keys().await.unwrap(); security_guide_delete_key_1: |- let client = Client::new("http://localhost:7700", "masterKey"); - let key = client.get_key("d0552b41536279a0ad88bd595327b96f01176a60c2243e906c52ac02375f9bc4").await.unwrap(); - client.delete_key(&key); + let key = client.get_key("ac5cd97d-5a4b-4226-a868-2d0eb6d197ab").await.unwrap(); + client.delete_key(&key).await?; landing_getting_started_1: |- let client = Client::new("http://localhost:7700", "masterKey"); @@ -819,10 +869,11 @@ landing_getting_started_1: |- ], Some("reference_number")).await.unwrap(); tenant_token_guide_generate_sdk_1: |- let api_key = "B5KdX2MY2jV6EXfUs6scSfmC..."; + let api_key_uid = "6062abda-a5aa-4414-ac91-ecd7944c0f8d"; let expires_at = time::macros::datetime!(2025 - 12 - 20 00:00:00 UTC); let search_rules = json!({ "patient_medical_records": { "filter": "user_id = 1" } }); - let token = client.generate_tenant_token(search_rules, api_key, expires_at).unwrap(); + let token = client.generate_tenant_token(api_key_uid, search_rules, api_key, expires_at).unwrap(); tenant_token_guide_search_sdk_1: |- let front_end_client = Client::new("http://127.0.0.1:7700", token); let results: SearchResults = front_end_client.index("patient_medical_records") diff --git a/Cargo.toml b/Cargo.toml index 078f256d..179e3bc1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "meilisearch-sdk" -version = "0.17.0" +version = "0.18.0" 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." @@ -24,6 +24,7 @@ yaup = "0.2.0" [target.'cfg(not(target_arch = "wasm32"))'.dependencies] futures = "0.3" isahc = { version = "1.0", features = ["http2", "text-decoding"], default_features = false } +uuid = { version = "1.1.2", features = ["v4"] } [target.'cfg(target_arch = "wasm32")'.dependencies] js-sys = "0.3.47" diff --git a/README.md b/README.md index dd948de0..5bea30c3 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,7 @@ To use `meilisearch-sdk`, add this to your `Cargo.toml`: ```toml [dependencies] -meilisearch-sdk = "0.17.0" +meilisearch-sdk = "0.18.0" ``` The following optional dependencies may also be useful: @@ -224,7 +224,7 @@ Json output: ], "offset": 0, "limit": 20, - "nbHits": 1, + "estimatedTotalHits": 1, "processingTimeMs": 0, "query": "wonder" } @@ -242,7 +242,7 @@ WARNING: `meilisearch-sdk` will panic if no Window is available (ex: Web extensi ## 🤖 Compatibility with Meilisearch -This package only guarantees the compatibility with the [version v0.27.0 of Meilisearch](https://github.com/meilisearch/meilisearch/releases/tag/v0.27.0). +This package only guarantees the compatibility with the [version v0.28.0 of Meilisearch](https://github.com/meilisearch/meilisearch/releases/tag/v0.28.0). ## ⚙️ Development Workflow and Contributing diff --git a/README.tpl b/README.tpl index 04e98565..213408fb 100644 --- a/README.tpl +++ b/README.tpl @@ -50,7 +50,7 @@ To use `meilisearch-sdk`, add this to your `Cargo.toml`: ```toml [dependencies] -meilisearch-sdk = "0.17.0" +meilisearch-sdk = "0.18.0" ``` The following optional dependencies may also be useful: @@ -97,7 +97,7 @@ WARNING: `meilisearch-sdk` will panic if no Window is available (ex: Web extensi ## 🤖 Compatibility with Meilisearch -This package only guarantees the compatibility with the [version v0.27.0 of Meilisearch](https://github.com/meilisearch/meilisearch/releases/tag/v0.27.0). +This package only guarantees the compatibility with the [version v0.28.0 of Meilisearch](https://github.com/meilisearch/meilisearch/releases/tag/v0.28.0). ## ⚙️ Development Workflow and Contributing diff --git a/meilisearch-test-macro/README.md b/meilisearch-test-macro/README.md index fb1796ab..95d09d4a 100644 --- a/meilisearch-test-macro/README.md +++ b/meilisearch-test-macro/README.md @@ -25,7 +25,7 @@ async fn test_get_tasks() -> Result<(), Error> { let tasks = index.get_tasks().await?; // The only task is the creation of the index - assert_eq!(status.len(), 1); + assert_eq!(status.results.len(), 1); index.delete() .await? @@ -52,7 +52,7 @@ With this macro, all these problems are solved. See a rewrite of this 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.len(), 1); + assert_eq!(status.results.len(), 1); } ``` diff --git a/src/client.rs b/src/client.rs index 18f581b0..e87e0df8 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1,9 +1,11 @@ use crate::{ errors::*, indexes::*, - key::{Key, KeyBuilder}, + key::{Key, KeyBuilder, KeyUpdater, KeysQuery, KeysResults}, request::*, - tasks::{async_sleep, Task}, + task_info::TaskInfo, + tasks::{Task, TasksQuery, TasksResults}, + utils::async_sleep, }; use serde::Deserialize; use serde_json::{json, Value}; @@ -39,7 +41,23 @@ impl Client { } } - /// List all [Index]es and returns values as instances of [Index]. + fn parse_indexes_results_from_value(&self, value: Value) -> Result { + let raw_indexes = value["results"].as_array().unwrap(); + + let indexes_results = IndexesResults { + limit: value["limit"].as_u64().unwrap() as u32, + offset: value["offset"].as_u64().unwrap() as u32, + total: value["total"].as_u64().unwrap() as u32, + results: raw_indexes + .iter() + .map(|raw_index| Index::from_value(raw_index.clone(), self.clone())) + .collect::>()?, + }; + + Ok(indexes_results) + } + + /// List all [Index]es with query parameters and returns values as instances of [Index]. /// /// # Example /// @@ -53,16 +71,44 @@ impl Client { /// // create the client /// let client = Client::new(MEILISEARCH_HOST, MEILISEARCH_API_KEY); /// - /// let indexes: Vec = 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> { - self.list_all_indexes_raw() - .await? - .into_iter() - .map(|index| Index::from_value(index, self.clone())) - .collect() + pub async fn list_all_indexes(&self) -> Result { + let value = self.list_all_indexes_raw().await?; + let indexes_results = self.parse_indexes_results_from_value(value)?; + Ok(indexes_results) + } + + /// List all [Index]es and returns values as instances of [Index]. + /// + /// # Example + /// + /// ``` + /// # use meilisearch_sdk::{client::*, indexes::*}; + /// # + /// # let MEILISEARCH_HOST = option_env!("MEILISEARCH_HOST").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # futures::executor::block_on(async move { + /// // create the client + /// let client = Client::new(MEILISEARCH_HOST, MEILISEARCH_API_KEY); + /// 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<'_>, + ) -> Result { + 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 [Index]es and returns as Json. @@ -83,8 +129,8 @@ impl Client { /// println!("{:?}", json_indexes); /// # }); /// ``` - pub async fn list_all_indexes_raw(&self) -> Result, Error> { - let json_indexes = request::<(), Vec>( + pub async fn list_all_indexes_raw(&self) -> Result { + let json_indexes = request::<(), Value>( &format!("{}/indexes", self.host), &self.api_key, Method::Get(()), @@ -95,6 +141,42 @@ impl Client { Ok(json_indexes) } + /// List all [Index]es with query parameters and returns as Json. + /// + /// # Example + /// + /// ``` + /// # use meilisearch_sdk::{client::*, indexes::*}; + /// # + /// # let MEILISEARCH_HOST = option_env!("MEILISEARCH_HOST").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # futures::executor::block_on(async move { + /// // create the client + /// let client = Client::new(MEILISEARCH_HOST, MEILISEARCH_API_KEY); + /// + /// 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<'_>, + ) -> Result { + let json_indexes = request::<&IndexesQuery, Value>( + &format!("{}/indexes", self.host), + &self.api_key, + Method::Get(indexes_query), + 200, + ) + .await?; + + Ok(json_indexes) + } + /// Get an [Index], this index should already exist. /// /// # Example @@ -156,13 +238,7 @@ impl Client { /// Create a corresponding object of an [Index] without any check or doing an HTTP call. pub fn index(&self, uid: impl Into) -> Index { - Index { - uid: Arc::new(uid.into()), - client: self.clone(), - primary_key: None, - created_at: None, - updated_at: None, - } + Index::new(uid, self.clone()) } /// Create an [Index]. @@ -197,8 +273,8 @@ impl Client { &self, uid: impl AsRef, primary_key: Option<&str>, - ) -> Result { - request::( + ) -> Result { + request::( &format!("{}/indexes", self.host), &self.api_key, Method::Post(json!({ @@ -212,8 +288,8 @@ impl Client { /// 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 { - request::<(), Task>( + pub async fn delete_index(&self, uid: impl AsRef) -> Result { + request::<(), TaskInfo>( &format!("{}/indexes/{}", self.host, uid.as_ref()), &self.api_key, Method::Delete, @@ -223,15 +299,31 @@ impl Client { } /// Alias for [Client::list_all_indexes]. - pub async fn get_indexes(&self) -> Result, Error> { + pub async fn get_indexes(&self) -> Result { self.list_all_indexes().await } + /// Alias for [Client::list_all_indexes_with]. + pub async fn get_indexes_with( + &self, + indexes_query: &IndexesQuery<'_>, + ) -> Result { + self.list_all_indexes_with(indexes_query).await + } + /// Alias for [Client::list_all_indexes_raw]. - pub async fn get_indexes_raw(&self) -> Result, Error> { + 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<'_>, + ) -> Result { + self.list_all_indexes_raw_with(indexes_query).await + } + /// Get stats of all indexes. /// /// # Example @@ -307,6 +399,40 @@ impl Client { } } + /// Get the API [Key]s from Meilisearch with parameters. + /// See the [meilisearch documentation](https://docs.meilisearch.com/reference/api/keys.html#get-all-keys). + /// + /// See also [Client::create_key] and [Client::get_key]. + /// + /// # Example + /// + /// ``` + /// # use meilisearch_sdk::{client::*, errors::Error, key::KeysQuery}; + /// # + /// # let MEILISEARCH_HOST = option_env!("MEILISEARCH_HOST").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # futures::executor::block_on(async move { + /// let client = Client::new(MEILISEARCH_HOST, MEILISEARCH_API_KEY); + /// 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 = request::<&KeysQuery, KeysResults>( + &format!("{}/keys", self.host), + &self.api_key, + Method::Get(keys_query), + 200, + ) + .await?; + + Ok(keys) + } + /// Get the API [Key]s from Meilisearch. /// See the [meilisearch documentation](https://docs.meilisearch.com/reference/api/keys.html#get-all-keys). /// @@ -323,18 +449,12 @@ impl Client { /// # futures::executor::block_on(async move { /// let client = Client::new(MEILISEARCH_HOST, MEILISEARCH_API_KEY); /// let keys = client.get_keys().await.unwrap(); - /// assert!(keys.len() >= 2); + /// + /// assert_eq!(keys.limit, 20); /// # }); /// ``` - pub async fn get_keys(&self) -> Result, Error> { - #[derive(Deserialize)] - #[serde(rename_all = "camelCase")] - pub struct Keys { - #[serde(rename = "results")] - pub inner: Vec, - } - - let keys = request::<(), Keys>( + pub async fn get_keys(&self) -> Result { + let keys = request::<(), KeysResults>( &format!("{}/keys", self.host), &self.api_key, Method::Get(()), @@ -342,7 +462,7 @@ impl Client { ) .await?; - Ok(keys.inner) + Ok(keys) } /// Get one API [Key] from Meilisearch. @@ -360,11 +480,13 @@ impl Client { /// # /// # futures::executor::block_on(async move { /// let client = Client::new(MEILISEARCH_HOST, MEILISEARCH_API_KEY); - /// # let key = client.get_keys().await.unwrap().into_iter().find(|k| k.description.starts_with("Default Search API Key")).unwrap(); - /// let key_id = // enter your API key here, for the example we'll say we entered our search API key. - /// # key.key; - /// let key = client.get_key(key_id).await.unwrap(); - /// assert_eq!(key.description, "Default Search API Key (Use it to search from the frontend)"); + /// # 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"))) + /// .unwrap(); + /// + /// let key = client.get_key(key).await.unwrap(); + /// + /// assert_eq!(key.name, Some("Default Search API Key".to_string())); /// # }); /// ``` pub async fn get_key(&self, key: impl AsRef) -> Result { @@ -392,14 +514,14 @@ impl Client { /// # /// # futures::executor::block_on(async move { /// let client = Client::new(MEILISEARCH_HOST, MEILISEARCH_API_KEY); - /// let key = KeyBuilder::new("delete_key"); + /// 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.iter().all(|key| key.key != inner_key)); + /// assert!(keys.results.iter().all(|key| key.key != inner_key)); /// # }); /// ``` pub async fn delete_key(&self, key: impl AsRef) -> Result<(), Error> { @@ -427,10 +549,12 @@ impl Client { /// # /// # futures::executor::block_on(async move { /// let client = Client::new(MEILISEARCH_HOST, MEILISEARCH_API_KEY); - /// let mut key = KeyBuilder::new("create_key"); - /// key.with_index("*").with_action(Action::DocumentsAdd); + /// 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.description, "create_key"); + /// assert_eq!(key.name, Some(name)); /// # client.delete_key(key).await.unwrap(); /// # }); /// ``` @@ -452,25 +576,26 @@ impl Client { /// # Example /// /// ``` - /// # use meilisearch_sdk::{client::*, errors::Error, key::KeyBuilder}; + /// # use meilisearch_sdk::{client::*, errors::Error, key::KeyBuilder, key::KeyUpdater}; /// # /// # let MEILISEARCH_HOST = option_env!("MEILISEARCH_HOST").unwrap_or("http://localhost:7700"); /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); /// # /// # futures::executor::block_on(async move { /// let client = Client::new(MEILISEARCH_HOST, MEILISEARCH_API_KEY); - /// let key = KeyBuilder::new("update_key"); - /// let mut key = client.create_key(key).await.unwrap(); - /// assert!(key.indexes.is_empty()); + /// let new_key = KeyBuilder::new(); + /// let name = "my name".to_string(); + /// let mut new_key = client.create_key(new_key).await.unwrap(); + /// let mut key_update = KeyUpdater::new(new_key); + /// key_update.with_name(&name); /// - /// key.indexes = vec!["*".to_string()]; - /// let key = client.update_key(key).await.unwrap(); - /// assert_eq!(key.indexes, vec!["*"]); + /// 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 { - request::<&Key, Key>( + pub async fn update_key(&self, key: impl AsRef) -> Result { + request::<&KeyUpdater, Key>( &format!("{}/keys/{}", self.host, key.as_ref().key), &self.api_key, Method::Patch(key.as_ref()), @@ -511,7 +636,7 @@ impl Client { /// /// If the waited time exceeds `timeout` then an [Error::Timeout] will be returned. /// - /// See also [Index::wait_for_task, Task::wait_for_completion]. + /// See also [Index::wait_for_task, Task::wait_for_completion, TaskInfo::wait_for_completion]. /// /// # Example /// @@ -548,7 +673,7 @@ impl Client { /// ``` pub async fn wait_for_task( &self, - task_id: impl AsRef, + task_id: impl AsRef, interval: Option, timeout: Option, ) -> Result { @@ -560,7 +685,6 @@ impl Client { while timeout > elapsed_time { task_result = self.get_task(&task_id).await; - match task_result { Ok(status) => match status { Task::Failed { .. } | Task::Succeeded { .. } => { @@ -596,7 +720,7 @@ impl Client { /// # index.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); /// # }); /// ``` - pub async fn get_task(&self, task_id: impl AsRef) -> Result { + pub async fn get_task(&self, task_id: impl AsRef) -> Result { request::<(), Task>( &format!("{}/tasks/{}", self.host, task_id.as_ref()), &self.api_key, @@ -606,6 +730,39 @@ impl Client { .await } + /// Get all tasks with query parameters from the server. + /// + /// # Example + /// + /// ``` + /// # use meilisearch_sdk::*; + /// # + /// # let MEILISEARCH_HOST = option_env!("MEILISEARCH_HOST").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # futures::executor::block_on(async move { + /// # let client = client::Client::new(MEILISEARCH_HOST, MEILISEARCH_API_KEY); + /// + /// let mut query = tasks::TasksQuery::new(&client); + /// query.with_index_uid(["get_tasks_with"]); + /// let tasks = client.get_tasks_with(&query).await.unwrap(); + /// # }); + /// ``` + pub async fn get_tasks_with( + &self, + tasks_query: &TasksQuery<'_>, + ) -> Result { + let tasks = request::<&TasksQuery, TasksResults>( + &format!("{}/tasks", self.host), + &self.api_key, + Method::Get(tasks_query), + 200, + ) + .await?; + + Ok(tasks) + } + /// Get all tasks from the server. /// /// # Example @@ -619,15 +776,13 @@ impl Client { /// # futures::executor::block_on(async move { /// # let client = client::Client::new(MEILISEARCH_HOST, MEILISEARCH_API_KEY); /// let tasks = client.get_tasks().await.unwrap(); + /// + /// # assert!(tasks.results.len() > 0); + /// # client.index("get_tasks").delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); /// # }); /// ``` - pub async fn get_tasks(&self) -> Result, Error> { - #[derive(Deserialize)] - struct Tasks { - pub results: Vec, - } - - let tasks = request::<(), Tasks>( + pub async fn get_tasks(&self) -> Result { + let tasks = request::<(), TasksResults>( &format!("{}/tasks", self.host), &self.api_key, Method::Get(()), @@ -635,7 +790,7 @@ impl Client { ) .await?; - Ok(tasks.results) + Ok(tasks) } /// Generates a new tenant token. @@ -650,19 +805,22 @@ impl Client { /// # /// # futures::executor::block_on(async move { /// # let client = client::Client::new(MEILISEARCH_HOST, MEILISEARCH_API_KEY); - /// let token = client.generate_tenant_token(serde_json::json!(["*"]), None, None).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::Client::new(MEILISEARCH_HOST, token); /// # }); /// ``` + #[cfg(not(target_arch = "wasm32"))] pub fn generate_tenant_token( &self, + api_key_uid: String, search_rules: serde_json::Value, api_key: Option<&str>, expires_at: Option, ) -> Result { let api_key = api_key.unwrap_or(&self.api_key); - crate::tenant_tokens::generate_tenant_token(search_rules, api_key, expires_at) + crate::tenant_tokens::generate_tenant_token(api_key_uid, search_rules, api_key, expires_at) } } @@ -769,30 +927,46 @@ mod tests { } } + #[meilisearch_test] + async fn test_get_tasks(client: Client) { + let tasks = client.get_tasks().await.unwrap(); + assert!(tasks.results.len() >= 2); + } + + #[meilisearch_test] + async fn test_get_tasks_with_params(client: Client) { + let query = TasksQuery::new(&client); + let tasks = client.get_tasks_with(&query).await.unwrap(); + + assert!(tasks.results.len() >= 2); + } + #[meilisearch_test] async fn test_get_keys(client: Client) { let keys = client.get_keys().await.unwrap(); - assert!(keys.len() >= 2); - assert!(keys.iter().any( - |k| k.description != "Default Search API Key (Use it to search from the frontend)" - )); - assert!(keys.iter().any( - |k| k.description != "Default Admin API Key (Use it for all other operations. Caution! Do not use it on a public frontend)" - )); + + assert!(keys.results.len() >= 2); } #[meilisearch_test] - async fn test_delete_key(client: Client, description: String) { - let key = KeyBuilder::new(description); + 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 = client.get_keys().await.unwrap(); - assert!(keys.iter().all(|k| k.key != key.key)); + 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(mut client: Client, description: String) { + + async fn test_error_delete_key(mut client: Client, name: String) { // ==> accessing a key that does not exist let error = client.delete_key("invalid_key").await.unwrap_err(); assert!(matches!( @@ -805,9 +979,10 @@ mod tests { )); // ==> executing the action without enough right - let key = KeyBuilder::new(description); - let key = client.create_key(key).await.unwrap(); + let mut key = KeyBuilder::new(); + key.with_name(&name); + let key = client.create_key(key).await.unwrap(); let master_key = client.api_key.clone(); // this key has no right client.api_key = Arc::new(key.key.clone()); @@ -838,16 +1013,18 @@ mod tests { } #[meilisearch_test] - async fn test_create_key(client: Client, description: String) { + async fn test_create_key(client: Client, name: String) { let expires_at = OffsetDateTime::now_utc() + time::Duration::HOUR; - let mut key = KeyBuilder::new(description.clone()); + let mut key = KeyBuilder::new(); key.with_action(Action::DocumentsAdd) + .with_name(&name) .with_expires_at(expires_at.clone()) + .with_description("a description") .with_index("*"); let key = client.create_key(key).await.unwrap(); assert_eq!(key.actions, vec![Action::DocumentsAdd]); - assert_eq!(key.description, description); + assert_eq!(&key.name, &Some(name)); // We can't compare the two timestamp directly because of some nanoseconds imprecision with the floats assert_eq!( key.expires_at.unwrap().unix_timestamp(), @@ -855,27 +1032,14 @@ mod tests { ); assert_eq!(key.indexes, vec!["*".to_string()]); - let keys = client.get_keys().await.unwrap(); - - let remote_key = keys.iter().find(|k| k.key == key.key).unwrap(); - - assert_eq!(remote_key.actions, vec![Action::DocumentsAdd]); - assert_eq!(remote_key.description, description); - // We can't compare the two timestamp directly because of some nanoseconds imprecision with the floats - assert_eq!( - remote_key.expires_at.unwrap().unix_timestamp(), - expires_at.unix_timestamp() - ); - assert_eq!(remote_key.indexes, vec!["*".to_string()]); - client.delete_key(key).await.unwrap(); } #[meilisearch_test] - async fn test_error_create_key(mut client: Client, description: String) { + async fn test_error_create_key(mut 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(&description); + let mut key = KeyBuilder::new(); key.with_index("invalid index # / \\name with spaces"); let error = client.create_key(key).await.unwrap_err(); @@ -890,14 +1054,16 @@ mod tests { */ // ==> executing the action without enough right - let no_right_key = KeyBuilder::new(&description); + 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(); client.api_key = Arc::new(no_right_key.key.clone()); - let key = KeyBuilder::new(&description); + let mut key = KeyBuilder::new(); + key.with_name(format!("{name}_2")); let error = client.create_key(key).await.unwrap_err(); assert!(matches!( @@ -915,84 +1081,22 @@ mod tests { #[meilisearch_test] async fn test_update_key(client: Client, description: String) { - let expires_at = OffsetDateTime::now_utc() + time::Duration::HOUR; - let key = KeyBuilder::new(description.clone()); + let mut key = KeyBuilder::new(); + key.with_name("test_update_key"); let mut key = client.create_key(key).await.unwrap(); - key.actions = vec![Action::DocumentsAdd]; - key.expires_at = Some(expires_at); - key.indexes = vec!["*".to_string()]; - - let key = client.update_key(key).await.unwrap(); - - assert_eq!(key.actions, vec![Action::DocumentsAdd]); - assert_eq!(key.description, description); - // We can't compare the two timestamp 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!["*".to_string()]); - - let keys = client.get_keys().await.unwrap(); + let name = "new name".to_string(); + key.with_description(&description); + key.with_name(&name); - let remote_key = keys.iter().find(|k| k.key == key.key).unwrap(); + let key = key.update(&client).await.unwrap(); - assert_eq!(remote_key.actions, vec![Action::DocumentsAdd]); - assert_eq!(remote_key.description, description); - // We can't compare the two timestamp directly because of some nanoseconds imprecision with the floats - assert_eq!( - remote_key.expires_at.unwrap().unix_timestamp(), - expires_at.unix_timestamp() - ); - assert_eq!(remote_key.indexes, vec!["*".to_string()]); + assert_eq!(key.description, Some(description)); + assert_eq!(key.name, Some(name)); client.delete_key(key).await.unwrap(); } - #[meilisearch_test] - async fn test_error_update_key(mut client: Client, description: String) { - let key = KeyBuilder::new(description.clone()); - let key = client.create_key(key).await.unwrap(); - - // ==> Invalid index name - /* TODO: uncomment once meilisearch fix this bug: https://github.com/meilisearch/meilisearch/issues/2158 - key.indexes = vec!["invalid index # / \\name with spaces".to_string()]; - let error = client.update_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 no_right_key = KeyBuilder::new(&description); - 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(); - client.api_key = Arc::new(no_right_key.key.clone()); - - let error = client.update_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).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?; @@ -1047,19 +1151,39 @@ mod tests { } #[meilisearch_test] - async fn test_list_all_indexes(client: Client, index: Index) { + async fn test_list_all_indexes(client: Client) { let all_indexes = client.list_all_indexes().await.unwrap(); - assert!(all_indexes.len() > 0); - assert!(all_indexes.iter().any(|idx| idx.uid == index.uid)); + + assert_eq!(all_indexes.limit, 20); + assert_eq!(all_indexes.offset, 0); } #[meilisearch_test] - async fn test_list_all_indexes_raw(client: Client, index: Index) { + 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!(all_indexes_raw.len() > 0); - assert!(all_indexes_raw - .iter() - .any(|idx| idx["uid"] == json!(index.uid.to_string()))); + + 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] diff --git a/src/documents.rs b/src/documents.rs new file mode 100644 index 00000000..5be9866b --- /dev/null +++ b/src/documents.rs @@ -0,0 +1,319 @@ +use crate::{errors::Error, indexes::Index}; +use serde::{de::DeserializeOwned, Deserialize, Serialize}; + +#[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> { + #[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> DocumentQuery<'a> { + 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_HOST = option_env!("MEILISEARCH_HOST").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # let client = Client::new(MEILISEARCH_HOST, MEILISEARCH_API_KEY); + /// 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> { + 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_HOST = option_env!("MEILISEARCH_HOST").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # let client = Client::new(MEILISEARCH_HOST, MEILISEARCH_API_KEY); + /// + /// # futures::executor::block_on(async move { + /// #[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> { + #[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>, +} + +impl<'a> DocumentsQuery<'a> { + pub fn new(index: &Index) -> DocumentsQuery { + DocumentsQuery { + index, + offset: None, + limit: None, + fields: None, + } + } + + /// Specify the offset. + /// + /// # Example + /// + /// ``` + /// # use meilisearch_sdk::{client::*, indexes::*, documents::*}; + /// # + /// # let MEILISEARCH_HOST = option_env!("MEILISEARCH_HOST").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # let client = Client::new(MEILISEARCH_HOST, MEILISEARCH_API_KEY); + /// 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> { + self.offset = Some(offset); + self + } + + /// Specify the limit. + /// + /// # Example + /// + /// ``` + /// # use meilisearch_sdk::{client::*, indexes::*, documents::*}; + /// # + /// # let MEILISEARCH_HOST = option_env!("MEILISEARCH_HOST").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # let client = Client::new(MEILISEARCH_HOST, MEILISEARCH_API_KEY); + /// 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> { + self.limit = Some(limit); + self + } + + /// Specify the fields to return in the documents. + /// + /// # Example + /// + /// ``` + /// # use meilisearch_sdk::{client::*, indexes::*, documents::*}; + /// # + /// # let MEILISEARCH_HOST = option_env!("MEILISEARCH_HOST").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # let client = Client::new(MEILISEARCH_HOST, MEILISEARCH_API_KEY); + /// 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> { + self.fields = Some(fields.into_iter().collect()); + self + } + + /// Execute the get documents query. + /// + /// # Example + /// + /// ``` + /// # use meilisearch_sdk::{client::*, indexes::*, documents::*}; + /// # use serde::{Deserialize, Serialize}; + /// # + /// # let MEILISEARCH_HOST = option_env!("MEILISEARCH_HOST").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # let client = Client::new(MEILISEARCH_HOST, MEILISEARCH_API_KEY); + /// + /// # futures::executor::block_on(async move { + /// # 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 + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{client::*, indexes::*}; + use meilisearch_test_macro::meilisearch_test; + use serde::{Deserialize, Serialize}; + + #[derive(Debug, Serialize, Deserialize, PartialEq)] + struct MyObject { + id: Option, + kind: String, + } + + 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 = index.get_documents(None, None, None).await.unwrap(); + 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_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(()) + } +} diff --git a/src/dumps.rs b/src/dumps.rs index 74206a9f..67b9e63b 100644 --- a/src/dumps.rs +++ b/src/dumps.rs @@ -15,7 +15,7 @@ //! # Example //! //! ```no_run -//! # use meilisearch_sdk::{client::*, errors::*, dumps::*}; +//! # use meilisearch_sdk::{client::*, errors::*, dumps::*, dumps::*, task_info::*, tasks::*}; //! # use futures_await_test::async_test; //! # use std::{thread::sleep, time::Duration}; //! # futures::executor::block_on(async move { @@ -26,46 +26,18 @@ //! let client = Client::new(MEILISEARCH_HOST, MEILISEARCH_API_KEY); //! //! // Create a dump -//! let dump_info = client.create_dump().await.unwrap(); -//! assert!(matches!(dump_info.status, DumpStatus::InProgress)); -//! -//! // Wait for Meilisearch to proceed -//! sleep(Duration::from_secs(5)); -//! -//! // Check the status of the dump -//! let dump_info = client.get_dump_status(&dump_info.uid).await.unwrap(); -//! assert!(matches!(dump_info.status, DumpStatus::Done)); +//! let task_info = client.create_dump().await.unwrap(); +//! assert!(matches!( +//! task_info, +//! TaskInfo { +//! update_type: TaskType::DumpCreation { .. }, +//! .. +//! } +//!)); //! # }); //! ``` -use crate::{client::Client, errors::Error, request::*}; -use serde::Deserialize; - -/// The status of a dump.\ -/// Contained in [`DumpInfo`]. -#[derive(Debug, Deserialize, Clone, PartialEq)] -#[serde(rename_all = "snake_case")] -pub enum DumpStatus { - /// Dump creation is in progress. - Done, - /// Dump creation is in progress. - InProgress, - /// An error occured during dump process, and the task was aborted. - Failed, -} - -/// Limited informations about a dump.\ -/// Can be obtained with [create_dump](Client::create_dump) and [get_dump_status](Client::get_dump_status) methods. -#[derive(Debug, Deserialize, Clone)] -#[serde(rename_all = "camelCase")] -pub struct DumpInfo { - pub uid: String, - pub status: DumpStatus, - pub error: Option, - pub started_at: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub finished_at: Option, -} +use crate::{client::Client, errors::Error, request::*, task_info::TaskInfo}; /// Dump related methods.\ /// See the [dumps](crate::dumps) module. @@ -77,7 +49,7 @@ impl Client { /// # Example /// /// ```no_run - /// # use meilisearch_sdk::{client::*, errors::*, dumps::*}; + /// # use meilisearch_sdk::{client::*, errors::*, dumps::*, dumps::*, task_info::*, tasks::*}; /// # use futures_await_test::async_test; /// # use std::{thread::sleep, time::Duration}; /// # futures::executor::block_on(async move { @@ -87,12 +59,18 @@ impl Client { /// # /// # let client = Client::new(MEILISEARCH_HOST, MEILISEARCH_API_KEY); /// # - /// let dump_info = client.create_dump().await.unwrap(); - /// assert!(matches!(dump_info.status, DumpStatus::InProgress)); + /// let task_info = client.create_dump().await.unwrap(); + /// assert!(matches!( + /// task_info, + /// TaskInfo { + /// update_type: TaskType::DumpCreation { .. }, + /// .. + /// } + /// )); /// # }); /// ``` - pub async fn create_dump(&self) -> Result { - request::<(), DumpInfo>( + pub async fn create_dump(&self) -> Result { + request::<(), TaskInfo>( &format!("{}/dumps", self.host), &self.api_key, Method::Post(()), @@ -100,72 +78,47 @@ impl Client { ) .await } - - /// Get the status of a dump creation process using [the uid](DumpInfo::uid) returned after calling the [dump creation method](Client::create_dump). - /// - /// # Example - /// - /// ```no_run - /// # use meilisearch_sdk::{client::*, errors::*, dumps::*}; - /// # use futures_await_test::async_test; - /// # use std::{thread::sleep, time::Duration}; - /// # - /// # let MEILISEARCH_HOST = option_env!("MEILISEARCH_HOST").unwrap_or("http://localhost:7700"); - /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); - /// # - /// # futures::executor::block_on(async move { - /// # - /// # let client = Client::new(MEILISEARCH_HOST, MEILISEARCH_API_KEY); - /// # let dump_info = client.create_dump().await.unwrap(); - /// # sleep(Duration::from_secs(5)); - /// # - /// let dump_info = client.get_dump_status(&dump_info.uid).await.unwrap(); - /// # }); - /// ``` - pub async fn get_dump_status(&self, dump_uid: impl AsRef) -> Result { - request::<(), DumpInfo>( - &format!("{}/dumps/{}/status", self.host, dump_uid.as_ref()), - &self.api_key, - Method::Get(()), - 200, - ) - .await - } } /// Alias for [create_dump](Client::create_dump). -pub async fn create_dump(client: &Client) -> Result { +pub async fn create_dump(client: &Client) -> Result { client.create_dump().await } -/// Alias for [get_dump_status](Client::get_dump_status). -pub async fn get_dump_status( - client: &Client, - dump_uid: impl AsRef, -) -> Result { - client.get_dump_status(dump_uid).await -} - #[cfg(test)] mod tests { use super::*; - use crate::client::*; + use crate::{client::*, tasks::*}; use meilisearch_test_macro::meilisearch_test; - use std::{thread::sleep, time::Duration}; + use std::time::Duration; #[meilisearch_test] - async fn test_dumps(client: Client) { - // Create a dump - let dump_info = client.create_dump().await.unwrap(); - assert!(matches!(dump_info.status, DumpStatus::InProgress)); + 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(()) + } - // Wait for Meilisearch to do the dump - sleep(Duration::from_secs(5)); + #[meilisearch_test] + async fn test_dumps_correct_update_type(client: Client) -> Result<(), Error> { + let task_info = client.create_dump().await.unwrap(); - // Assert that the dump was successful - let new_dump_info = client.get_dump_status(&dump_info.uid).await.unwrap(); - assert!(matches!(new_dump_info.status, DumpStatus::Done)); - assert!(new_dump_info.finished_at.is_some()); - assert!(new_dump_info.started_at.is_some()); + assert!(matches!( + task_info, + TaskInfo { + update_type: TaskType::DumpCreation { .. }, + .. + } + )); + Ok(()) } } diff --git a/src/errors.rs b/src/errors.rs index 68f78650..bbca6cec 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -36,6 +36,11 @@ pub enum Error { HttpError(String), // The library formating the query parameters encountered an error. Yaup(yaup::Error), + // The library validating the format of an uuid. + #[cfg(not(target_arch = "wasm32"))] + Uuid(uuid::Error), + // Error thrown in case the version of the Uuid is not v4. + InvalidUuid4Version, } #[derive(Debug, Clone, Deserialize)] @@ -75,6 +80,13 @@ impl From for Error { } } +#[cfg(not(target_arch = "wasm32"))] +impl From for Error { + fn from(error: uuid::Error) -> Error { + Error::Uuid(error) + } +} + /// The type of error that was encountered. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] @@ -194,7 +206,10 @@ impl std::fmt::Display for Error { Error::TenantTokensInvalidApiKey => write!(fmt, "The provided api_key is invalid."), Error::TenantTokensExpiredSignature => write!(fmt, "The provided expires_at is already expired."), Error::InvalidTenantToken(e) => write!(fmt, "Impossible to generate the token, jsonwebtoken encountered an error: {}", e), - Error::Yaup(e) => write!(fmt, "Internal Error: could not parse the query parameters: {}", e) + Error::Yaup(e) => write!(fmt, "Internal Error: could not parse the query parameters: {}", e), + #[cfg(not(target_arch = "wasm32"))] + Error::Uuid(e) => write!(fmt, "The uid of the token has bit an uuid4 format: {}", e), + Error::InvalidUuid4Version => write!(fmt, "The uid provided to the token is not of version uuidv4") } } } diff --git a/src/indexes.rs b/src/indexes.rs index 2e0dad4d..afc99614 100644 --- a/src/indexes.rs +++ b/src/indexes.rs @@ -1,7 +1,14 @@ -use crate::{client::Client, errors::Error, request::*, search::*, tasks::*}; +use crate::{ + client::Client, + documents::{DocumentQuery, DocumentsQuery, DocumentsResults}, + errors::Error, + request::*, + search::*, + task_info::TaskInfo, + tasks::*, +}; use serde::{de::DeserializeOwned, Deserialize, Serialize}; -use serde_json::json; -use std::{collections::HashMap, fmt::Display, sync::Arc, time::Duration}; +use std::{collections::HashMap, fmt::Display, time::Duration}; use time::OffsetDateTime; /// An index containing [Document]s. @@ -37,7 +44,7 @@ use time::OffsetDateTime; /// # }); /// ``` /// -/// Or, if you know the index already exist remotely you can create an `Index` with the [Client::index] function. +/// Or, if you know the index already exist remotely you can create an [Index] with its builder. /// ``` /// # use meilisearch_sdk::{client::*, indexes::*}; /// # @@ -47,27 +54,39 @@ use time::OffsetDateTime; /// # futures::executor::block_on(async move { /// let client = Client::new(MEILISEARCH_HOST, MEILISEARCH_API_KEY); /// -/// // use the implicit index creation if the index already exist or /// // 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 = client.index("index"); +/// let movies = Index::new("movies", client); /// -/// // do something with the index +/// assert_eq!(movies.uid, "movies"); /// # }); /// ``` -#[derive(Debug, Clone)] +#[derive(Debug, Serialize, Clone)] +#[serde(rename_all = "camelCase")] pub struct Index { - pub(crate) uid: Arc, - pub(crate) client: Client, - pub(crate) primary_key: Option, - pub created_at: Option, + #[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(v: serde_json::Value, client: Client) -> Result { + pub(crate) fn from_value(raw_index: serde_json::Value, client: Client) -> Result { #[derive(Deserialize, Debug)] #[allow(non_snake_case)] struct IndexFromSerde { @@ -79,10 +98,10 @@ impl Index { primaryKey: Option, } - let i: IndexFromSerde = serde_json::from_value(v).map_err(Error::ParseError)?; + let i: IndexFromSerde = serde_json::from_value(raw_index).map_err(Error::ParseError)?; Ok(Index { - uid: Arc::new(i.uid), + uid: i.uid, client, created_at: i.createdAt, updated_at: i.updatedAt, @@ -90,18 +109,50 @@ impl Index { }) } - /// Set the primary key of the index. + /// Update an [Index]. /// - /// If you prefer, you can use the method [Index::set_primary_key], which is an alias. - pub async fn update(&self, primary_key: impl AsRef) -> Result<(), Error> { - request::( - &format!("{}/indexes/{}", self.client.host, self.uid), - &self.client.api_key, - Method::Put(json!({ "primaryKey": primary_key.as_ref() })), - 200, - ) - .await?; - Ok(()) + /// # Example + /// + /// ``` + /// # use meilisearch_sdk::{client::*, indexes::*, task_info::*, tasks::{Task, SucceededTask}}; + /// # + /// # let MEILISEARCH_HOST = option_env!("MEILISEARCH_HOST").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # futures::executor::block_on(async move { + /// # let client = Client::new(MEILISEARCH_HOST, MEILISEARCH_API_KEY); + /// # 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. @@ -124,8 +175,8 @@ impl Index { /// client.wait_for_task(task, None, None).await.unwrap(); /// # }); /// ``` - pub async fn delete(self) -> Result { - request::<(), Task>( + pub async fn delete(self) -> Result { + request::<(), TaskInfo>( &format!("{}/indexes/{}", self.client.host, self.uid), &self.client.api_key, Method::Delete, @@ -234,14 +285,12 @@ impl Index { /// # let MEILISEARCH_HOST = option_env!("MEILISEARCH_HOST").unwrap_or("http://localhost:7700"); /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); /// # - /// #[derive(Serialize, Deserialize, Debug)] - /// # #[derive(PartialEq)] + /// #[derive(Serialize, Deserialize, Debug, PartialEq)] /// struct Movie { /// name: String, - /// description: String, + /// description: String /// } /// - /// /// # futures::executor::block_on(async move { /// let client = Client::new(MEILISEARCH_HOST, MEILISEARCH_API_KEY); /// let movies = client.index("get_document"); @@ -252,30 +301,76 @@ impl Index { /// /// 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.") + /// 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, uid: &str) -> Result { - request::<(), T>( - &format!( - "{}/indexes/{}/documents/{}", - self.client.host, self.uid, uid - ), - &self.client.api_key, - Method::Get(()), - 200, - ) - .await + pub async fn get_document( + &self, + document_id: &str, + ) -> Result { + let url = format!( + "{}/indexes/{}/documents/{}", + self.client.host, self.uid, document_id + ); + + request::<(), T>(&url, &self.client.api_key, Method::Get(()), 200).await } - /// Get [Document]s by batch. + /// Get one document with parameters. /// - /// Using the optional parameters offset and limit, you can browse through all your documents. - /// If None, offset will be set to 0, limit to 20, and all attributes will be retrieved. + /// # Example /// - /// *Note: Documents are ordered by Meilisearch depending on the hash of their id.* + /// ``` + /// # use meilisearch_sdk::{client::*, indexes::*, documents::*}; + /// # use serde::{Deserialize, Serialize}; + /// # + /// # let MEILISEARCH_HOST = option_env!("MEILISEARCH_HOST").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # let client = Client::new(MEILISEARCH_HOST, MEILISEARCH_API_KEY); + /// + /// # futures::executor::block_on(async move { + /// #[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<'_>, + ) -> Result { + let url = format!( + "{}/indexes/{}/documents/{}", + self.client.host, self.uid, document_id + ); + + request::<&DocumentQuery, T>(&url, &self.client.api_key, Method::Get(document_query), 200) + .await + } + + /// Get [Document]s by batch. /// /// # Example /// @@ -303,34 +398,70 @@ impl Index { /// # 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::(None, None, None).await.unwrap(); + /// let movies = movie_index.get_documents::().await.unwrap(); /// - /// assert!(movies.len() > 0); + /// assert!(movies.results.len() > 0); /// # movie_index.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); /// # }); /// ``` pub async fn get_documents( &self, - offset: Option, - limit: Option, - attributes_to_retrieve: Option<&str>, - ) -> Result, Error> { - let mut url = format!("{}/indexes/{}/documents?", self.client.host, self.uid); - if let Some(offset) = offset { - url.push_str("offset="); - url.push_str(offset.to_string().as_str()); - url.push('&'); - } - if let Some(limit) = limit { - url.push_str("limit="); - url.push_str(limit.to_string().as_str()); - url.push('&'); - } - if let Some(attributes_to_retrieve) = attributes_to_retrieve { - url.push_str("attributesToRetrieve="); - url.push_str(attributes_to_retrieve); - } - request::<(), Vec>(&url, &self.client.api_key, Method::Get(()), 200).await + ) -> Result, Error> { + let url = format!("{}/indexes/{}/documents", self.client.host, self.uid); + + request::<(), DocumentsResults>(&url, &self.client.api_key, Method::Get(()), 200).await + } + + /// Get [Document]s by batch with parameters. + /// ``` + /// use serde::{Serialize, Deserialize}; + /// + /// # use meilisearch_sdk::{client::*, indexes::*, documents::*}; + /// # + /// # let MEILISEARCH_HOST = option_env!("MEILISEARCH_HOST").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// + /// #[derive(Serialize, Deserialize, Debug)] + /// # #[derive(PartialEq)] + /// struct Movie { + /// name: String, + /// description: String, + /// } + /// + /// #[derive(Deserialize, Debug, PartialEq)] + /// struct ReturnedMovie { + /// name: String, + /// } + /// + /// # futures::executor::block_on(async move { + /// let client = Client::new(MEILISEARCH_HOST, MEILISEARCH_API_KEY); + /// 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(); + /// + /// 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!(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<'_>, + ) -> Result, Error> { + let url = format!("{}/indexes/{}/documents", self.client.host, self.uid); + request::<&DocumentsQuery, DocumentsResults>( + &url, + &self.client.api_key, + Method::Get(documents_query), + 200, + ) + .await } /// Add a list of [Document]s or replace them if they already exist. @@ -382,8 +513,8 @@ impl Index { /// // 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::(None, None, None).await.unwrap(); - /// assert!(movies.len() >= 3); + /// 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(); /// # }); /// ``` @@ -391,7 +522,7 @@ impl Index { &self, documents: &[T], primary_key: Option<&str>, - ) -> Result { + ) -> Result { let url = if let Some(primary_key) = primary_key { format!( "{}/indexes/{}/documents?primaryKey={}", @@ -400,7 +531,7 @@ impl Index { } else { format!("{}/indexes/{}/documents", self.client.host, self.uid) }; - request::<&[T], Task>(&url, &self.client.api_key, Method::Post(documents), 202).await + request::<&[T], TaskInfo>(&url, &self.client.api_key, Method::Post(documents), 202).await } /// Alias for [Index::add_or_replace]. @@ -408,7 +539,7 @@ impl Index { &self, documents: &[T], primary_key: Option<&str>, - ) -> Result { + ) -> Result { self.add_or_replace(documents, primary_key).await } @@ -460,8 +591,8 @@ impl Index { /// // 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::(None, None, None).await.unwrap(); - /// assert!(movies.len() >= 3); + /// 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(); /// # }); /// ``` @@ -469,7 +600,7 @@ impl Index { &self, documents: &[T], primary_key: Option>, - ) -> Result { + ) -> Result { let url = if let Some(primary_key) = primary_key { format!( "{}/indexes/{}/documents?primaryKey={}", @@ -480,7 +611,7 @@ impl Index { } else { format!("{}/indexes/{}/documents", self.client.host, self.uid) }; - request::<&[T], Task>(&url, &self.client.api_key, Method::Put(documents), 202).await + request::<&[T], TaskInfo>(&url, &self.client.api_key, Method::Put(documents), 202).await } /// Delete all documents in the index. @@ -515,13 +646,13 @@ impl Index { /// .wait_for_completion(&client, None, None) /// .await /// .unwrap(); - /// let movies = movie_index.get_documents::(None, None, None).await.unwrap(); - /// assert_eq!(movies.len(), 0); + /// 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 { - request::<(), Task>( + pub async fn delete_all_documents(&self) -> Result { + request::<(), TaskInfo>( &format!("{}/indexes/{}/documents", self.client.host, self.uid), &self.client.api_key, Method::Delete, @@ -565,8 +696,8 @@ impl Index { /// # movies.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); /// # }); /// ``` - pub async fn delete_document(&self, uid: T) -> Result { - request::<(), Task>( + pub async fn delete_document(&self, uid: T) -> Result { + request::<(), TaskInfo>( &format!( "{}/indexes/{}/documents/{}", self.client.host, self.uid, uid @@ -617,8 +748,8 @@ impl Index { pub async fn delete_documents( &self, uids: &[T], - ) -> Result { - request::<&[T], Task>( + ) -> Result { + request::<&[T], TaskInfo>( &format!( "{}/indexes/{}/documents/delete-batch", self.client.host, self.uid @@ -631,8 +762,13 @@ impl Index { } /// Alias for the [Index::update] method. - pub async fn set_primary_key(&self, primary_key: impl AsRef) -> Result<(), Error> { - self.update(primary_key).await + 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. @@ -659,7 +795,7 @@ impl Index { /// ``` /// 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. pub async fn fetch_info(&mut self) -> Result<(), Error> { - let v = self.client.get_raw_index(self.uid.as_ref()).await?; + let v = self.client.get_raw_index(&self.uid).await?; *self = Index::from_value(v, self.client.clone())?; Ok(()) } @@ -729,18 +865,13 @@ impl Index { /// Task::Succeeded { content } => content.uid, /// }; /// - /// assert_eq!(task.get_uid(), from_index); + /// 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 { + pub async fn get_task(&self, uid: impl AsRef) -> Result { request::<(), Task>( - &format!( - "{}/indexes/{}/tasks/{}", - self.client.host, - self.uid, - uid.as_ref() - ), + &format!("{}/tasks/{}", self.client.host, uid.as_ref()), &self.client.api_key, Method::Get(()), 200, @@ -763,30 +894,50 @@ impl Index { /// # let client = Client::new(MEILISEARCH_HOST, MEILISEARCH_API_KEY); /// # let index = client.create_index("get_tasks", None).await.unwrap().wait_for_completion(&client, None, None).await.unwrap().try_make_index(&client).unwrap(); /// - /// let status = index.get_tasks().await.unwrap(); - /// assert!(status.len() == 1); // the index was created + /// 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 = TasksQuery::new(&self.client); + query.with_index_uid([self.uid.as_str()]); + + self.client.get_tasks_with(&query).await + } + + /// Get the status of all tasks in a given index. /// - /// index.set_ranking_rules(["wrong_ranking_rule"]).await.unwrap(); + /// # Example /// - /// let status = index.get_tasks().await.unwrap(); - /// assert!(status.len() == 2); + /// ``` + /// # use serde::{Serialize, Deserialize}; + /// # use meilisearch_sdk::{client::*, indexes::*, tasks::*}; + /// # + /// # let MEILISEARCH_HOST = option_env!("MEILISEARCH_HOST").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # futures::executor::block_on(async move { + /// # let client = Client::new(MEILISEARCH_HOST, MEILISEARCH_API_KEY); + /// # 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 = TasksQuery::new(&client); + /// query.with_index_uid(["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(&self) -> Result, Error> { - #[derive(Deserialize)] - struct AllTasks { - results: Vec, - } + pub async fn get_tasks_with( + &self, + tasks_query: &TasksQuery<'_>, + ) -> Result { + let mut query = tasks_query.clone(); + query.with_index_uid([self.uid.as_str()]); - Ok(request::<(), AllTasks>( - &format!("{}/indexes/{}/tasks", self.client.host, self.uid), - &self.client.api_key, - Method::Get(()), - 200, - ) - .await? - .results) + self.client.get_tasks_with(&query).await } /// Get stats of an index. @@ -861,7 +1012,7 @@ impl Index { /// ``` pub async fn wait_for_task( &self, - task_id: impl AsRef, + task_id: impl AsRef, interval: Option, timeout: Option, ) -> Result { @@ -914,8 +1065,8 @@ impl Index { /// /// client.wait_for_task(tasks.last().unwrap(), None, None).await.unwrap(); /// - /// let movies = movie_index.get_documents::(None, None, None).await.unwrap(); - /// assert!(movies.len() >= 3); + /// 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(); /// # }); @@ -925,7 +1076,7 @@ impl Index { documents: &[T], batch_size: Option, primary_key: Option<&str>, - ) -> Result, Error> { + ) -> 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?); @@ -979,8 +1130,8 @@ impl Index { /// /// client.wait_for_task(tasks.last().unwrap(), None, None).await.unwrap(); /// - /// let movies = movie_index.get_documents::(None, None, None).await.unwrap(); - /// assert!(movies.len() >= 3); + /// let movies = movie_index.get_documents::().await.unwrap(); + /// assert!(movies.results.len() >= 3); /// /// let updated_movies = [ /// Movie { @@ -1001,10 +1152,10 @@ impl Index { /// /// client.wait_for_task(tasks.last().unwrap(), None, None).await.unwrap(); /// - /// let movies_updated = movie_index.get_documents::(None, None, None).await.unwrap(); - /// assert!(movies_updated.len() >= 3); + /// let movies_updated = movie_index.get_documents::().await.unwrap(); + /// assert!(movies_updated.results.len() >= 3); /// - /// assert!(&movies_updated[..] == &updated_movies[..]); + /// assert!(&movies_updated.results[..] == &updated_movies[..]); /// /// # movie_index.delete().await.unwrap().wait_for_completion(&client, None, /// None).await.unwrap(); @@ -1015,7 +1166,7 @@ impl Index { documents: &[T], batch_size: Option, primary_key: Option<&str>, - ) -> Result, Error> { + ) -> 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?); @@ -1030,6 +1181,163 @@ impl AsRef for Index { } } +/// An [IndexUpdater] used to update the specifics of an index +/// +/// # Example +/// +/// ``` +/// # use meilisearch_sdk::{client::*, indexes::*, task_info::*, tasks::{Task, SucceededTask}}; +/// # +/// # let MEILISEARCH_HOST = option_env!("MEILISEARCH_HOST").unwrap_or("http://localhost:7700"); +/// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); +/// # +/// # futures::executor::block_on(async move { +/// # let client = Client::new(MEILISEARCH_HOST, MEILISEARCH_API_KEY); +/// # 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> { + #[serde(skip)] + pub client: &'a Client, + #[serde(skip_serializing)] + pub uid: String, + pub primary_key: Option, +} + +impl<'a> IndexUpdater<'a> { + 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_HOST = option_env!("MEILISEARCH_HOST").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # futures::executor::block_on(async move { + /// # let client = Client::new(MEILISEARCH_HOST, MEILISEARCH_API_KEY); + /// # 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 Self { + 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_HOST = option_env!("MEILISEARCH_HOST").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # futures::executor::block_on(async move { + /// # let client = Client::new(MEILISEARCH_HOST, MEILISEARCH_API_KEY); + /// # 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 { + request::<&IndexUpdater, TaskInfo>( + &format!("{}/indexes/{}", self.client.host, self.uid), + &self.client.api_key, + Method::Patch(self), + 202, + ) + .await + } +} + +impl AsRef for IndexUpdater<'_> { + fn as_ref(&self) -> &str { + &self.uid + } +} + +impl<'a> AsRef> for IndexUpdater<'a> { + fn as_ref(&self) -> &IndexUpdater<'a> { + self + } +} + #[derive(Deserialize)] #[serde(rename_all = "camelCase")] pub struct IndexStats { @@ -1038,11 +1346,187 @@ pub struct IndexStats { pub field_distribution: HashMap, } +// An [IndexesQuery] containing filter and pagination parameters when searching for [Index]es +/// +/// # Example +/// +/// ``` +/// # use meilisearch_sdk::{client::*, indexes::*}; +/// # +/// # let MEILISEARCH_HOST = option_env!("MEILISEARCH_HOST").unwrap_or("http://localhost:7700"); +/// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); +/// # +/// # futures::executor::block_on(async move { +/// # let client = Client::new(MEILISEARCH_HOST, MEILISEARCH_API_KEY); +/// # 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> { + #[serde(skip_serializing)] + pub client: &'a Client, + /// The number of [Index]es 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 [Index]es 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> IndexesQuery<'a> { + pub fn new(client: &Client) -> IndexesQuery { + IndexesQuery { + client, + offset: None, + limit: None, + } + } + + /// Specify the offset. + /// + /// # Example + /// + /// ``` + /// # use meilisearch_sdk::{client::*, indexes::*}; + /// # + /// # let MEILISEARCH_HOST = option_env!("MEILISEARCH_HOST").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # futures::executor::block_on(async move { + /// # let client = Client::new(MEILISEARCH_HOST, MEILISEARCH_API_KEY); + /// # 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> { + self.offset = Some(offset); + self + } + + /// Specify the maximum number of [Index]es to return. + /// + /// # Example + /// + /// ``` + /// # use meilisearch_sdk::{client::*, indexes::*}; + /// # + /// # let MEILISEARCH_HOST = option_env!("MEILISEARCH_HOST").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # futures::executor::block_on(async move { + /// # let client = Client::new(MEILISEARCH_HOST, MEILISEARCH_API_KEY); + /// # 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> { + self.limit = Some(limit); + self + } + /// Get [Index]es. + /// + /// # Example + /// + /// ``` + /// # use meilisearch_sdk::{indexes::IndexesQuery, client::Client}; + /// # + /// # let MEILISEARCH_HOST = option_env!("MEILISEARCH_HOST").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # futures::executor::block_on(async move { + /// # let client = Client::new(MEILISEARCH_HOST, MEILISEARCH_API_KEY); + /// # 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 { + 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 meilisearch_test_macro::meilisearch_test; + use serde_json::json; #[meilisearch_test] async fn test_from_value(client: Client) { @@ -1059,7 +1543,7 @@ mod tests { }); let idx = Index { - uid: Arc::new("test_from_value".to_string()), + uid: "test_from_value".to_string(), primary_key: None, created_at: Some(t), updated_at: Some(t), @@ -1086,10 +1570,37 @@ mod tests { } #[meilisearch_test] - async fn test_get_tasks_no_docs(index: Index) { - // The at this point the only task that is supposed to exist is the creation of the index - let status = index.get_tasks().await.unwrap(); - assert_eq!(status.len(), 1); + 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] @@ -1103,10 +1614,42 @@ mod tests { let status = index.get_task(task).await?; match status { - Task::Enqueued { content } => assert_eq!(content.index_uid, *index.uid), - Task::Processing { content } => assert_eq!(content.index_uid, *index.uid), - Task::Failed { content } => assert_eq!(content.task.index_uid, *index.uid), - Task::Succeeded { content } => assert_eq!(content.index_uid, *index.uid), + Task::Enqueued { + content: + EnqueuedTask { + index_uid: Some(index_uid), + .. + }, + } => assert_eq!(index_uid, *index.uid), + Task::Processing { + content: + EnqueuedTask { + index_uid: Some(index_uid), + .. + }, + } => assert_eq!(index_uid, *index.uid), + Task::Failed { + content: + FailedTask { + task: + SucceededTask { + index_uid: Some(index_uid), + .. + }, + .. + }, + } => assert_eq!(index_uid, *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/src/key.rs b/src/key.rs index 56376bb5..2af708cd 100644 --- a/src/key.rs +++ b/src/key.rs @@ -6,20 +6,23 @@ use crate::{client::Client, errors::Error}; /// Represent a [meilisearch key](https://docs.meilisearch.com/reference/api/keys.html#returned-fields) /// You can get a [Key] from the [Client::get_key] method. /// Or you can create a [Key] with the [KeyBuilder::create] or [Client::create_key] methods. -#[derive(Debug, Serialize, Deserialize)] +#[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: String, + 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, } @@ -37,23 +40,23 @@ impl Key { /// # /// # futures::executor::block_on(async move { /// # let client = Client::new(MEILISEARCH_HOST, MEILISEARCH_API_KEY); - /// - /// let mut key = KeyBuilder::new("My little lovely test key") + /// let description = "My not so little lovely test key".to_string(); + /// let mut key = KeyBuilder::new() /// .with_action(Action::DocumentsAdd) /// .with_index("*") - /// .create(&client).await.unwrap(); + /// .with_description(&description) + /// .execute(&client).await.unwrap(); /// - /// key.with_description("My not so little lovely test key"); - /// # assert_eq!(key.description, "My not so little lovely test key".to_string()); + /// # assert_eq!(key.description, Some(description)); /// # client.delete_key(key).await.unwrap(); /// # }); /// ``` pub fn with_description(&mut self, desc: impl AsRef) -> &mut Self { - self.description = desc.as_ref().to_string(); + self.description = Some(desc.as_ref().to_string()); self } - /// Add a set of actions the [Key] will be able to execute. + /// Update the name of the key. /// /// # Example /// @@ -65,179 +68,323 @@ impl Key { /// # /// # futures::executor::block_on(async move { /// # let client = Client::new(MEILISEARCH_HOST, MEILISEARCH_API_KEY); - /// - /// let mut key = KeyBuilder::new("My little lovely test key") + /// let name = "lovely key".to_string(); + /// let mut key = KeyBuilder::new() /// .with_action(Action::DocumentsAdd) /// .with_index("*") - /// .create(&client).await.unwrap(); + /// .execute(&client).await.unwrap(); /// - /// key.with_actions([Action::DocumentsGet, Action::DocumentsDelete]); + /// key.with_name(&name); + /// # assert_eq!(key.name, Some(name)); /// # client.delete_key(key).await.unwrap(); /// # }); /// ``` - pub fn with_actions(&mut self, actions: impl IntoIterator) -> &mut Self { - self.actions.extend(actions); + pub fn with_name(&mut self, desc: impl AsRef) -> &mut Self { + self.name = Some(desc.as_ref().to_string()); self } - /// Add one action the [Key] will be able to execute. + /// Update the [Key]. /// /// # Example /// /// ``` - /// # use meilisearch_sdk::{key::KeyBuilder, key::Action, client::Client}; + /// # use meilisearch_sdk::{key::KeyBuilder, client::Client}; /// # /// # let MEILISEARCH_HOST = option_env!("MEILISEARCH_HOST").unwrap_or("http://localhost:7700"); /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); /// # /// # futures::executor::block_on(async move { - /// # let client = Client::new(MEILISEARCH_HOST, MEILISEARCH_API_KEY); - /// - /// let mut key = KeyBuilder::new("My little lovely test key") - /// .with_action(Action::DocumentsAdd) - /// .with_index("*") - /// .create(&client).await.unwrap(); + /// let client = Client::new(MEILISEARCH_HOST, MEILISEARCH_API_KEY); + /// 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(); /// - /// key.with_action(Action::DocumentsGet); + /// # assert_eq!(key.description, Some(description)); /// # client.delete_key(key).await.unwrap(); /// # }); /// ``` - pub fn with_action(&mut self, action: Action) -> &mut Self { - self.actions.push(action); - self + 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 } - /// Update the expiration date of the [Key]. + /// Delete the [Key]. /// /// # Example /// /// ``` - /// # use meilisearch_sdk::{key::KeyBuilder, key::Action, client::Client}; - /// use time::{OffsetDateTime, Duration}; + /// # use meilisearch_sdk::{key::KeyBuilder, client::Client}; /// # /// # let MEILISEARCH_HOST = option_env!("MEILISEARCH_HOST").unwrap_or("http://localhost:7700"); /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); /// # /// # futures::executor::block_on(async move { - /// # let client = Client::new(MEILISEARCH_HOST, MEILISEARCH_API_KEY); - /// - /// let mut key = KeyBuilder::new("My little lovely test key") - /// .with_action(Action::DocumentsAdd) - /// .with_index("*") - /// .create(&client).await.unwrap(); + /// let client = Client::new(MEILISEARCH_HOST, MEILISEARCH_API_KEY); + /// let mut key = KeyBuilder::new() + /// .execute(&client).await.unwrap(); /// - /// // update the epiry date of the key to two weeks from now - /// key.with_expires_at(OffsetDateTime::now_utc() + Duration::WEEK * 2); - /// # client.delete_key(key).await.unwrap(); + /// client.delete_key(key).await.unwrap(); /// # }); /// ``` - pub fn with_expires_at(&mut self, expires_at: OffsetDateTime) -> &mut Self { - self.expires_at = Some(expires_at); + 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 } +} - /// Update the indexes the [Key] can manage. +#[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::KeyBuilder, key::Action, client::Client}; + /// # use meilisearch_sdk::{key::KeyBuilder, key::Action, client::Client, key::KeyUpdater}; /// # /// # let MEILISEARCH_HOST = option_env!("MEILISEARCH_HOST").unwrap_or("http://localhost:7700"); /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); /// # /// # futures::executor::block_on(async move { /// # let client = Client::new(MEILISEARCH_HOST, MEILISEARCH_API_KEY); - /// - /// let mut key = KeyBuilder::new("My little lovely test key") - /// .with_action(Action::DocumentsAdd) - /// .with_index("*") - /// .create(&client).await.unwrap(); - /// - /// key.with_indexes(vec!["test", "movies"]); - /// # client.delete_key(key).await.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_indexes( - &mut self, - indexes: impl IntoIterator>, - ) -> &mut Self { - self.indexes = indexes - .into_iter() - .map(|index| index.as_ref().to_string()) - .collect(); + pub fn with_description(&mut self, desc: impl AsRef) -> &mut Self { + self.description = Some(desc.as_ref().to_string()); self } - /// Add one index the [Key] can manage. + /// Update the name of the key. /// /// # Example /// /// ``` - /// # use meilisearch_sdk::{key::KeyBuilder, key::Action, client::Client}; + /// # use meilisearch_sdk::{key::KeyBuilder, key::Action, client::Client, key::KeyUpdater}; /// # /// # let MEILISEARCH_HOST = option_env!("MEILISEARCH_HOST").unwrap_or("http://localhost:7700"); /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); /// # /// # futures::executor::block_on(async move { /// # let client = Client::new(MEILISEARCH_HOST, MEILISEARCH_API_KEY); + /// let mut new_key = KeyBuilder::new() + /// .execute(&client) + /// .await + /// .unwrap(); /// - /// let mut key = KeyBuilder::new("My little lovely test key") - /// .with_action(Action::DocumentsAdd) - /// .with_index("*") - /// .create(&client).await.unwrap(); + /// let name = "lovely key".to_string(); /// - /// key.with_index("test"); - /// # client.delete_key(key).await.unwrap(); + /// 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_index(&mut self, index: impl AsRef) -> &mut Self { - self.indexes.push(index.as_ref().to_string()); + pub fn with_name(&mut self, desc: impl AsRef) -> &mut Self { + self.name = Some(desc.as_ref().to_string()); self } - /// Update the [Key]. + /// Update a [Key] using the [KeyUpdater]. /// /// # Example /// /// ``` - /// # use meilisearch_sdk::{key::KeyBuilder, client::Client}; + /// # use meilisearch_sdk::{key::KeyBuilder, key::KeyUpdater, client::Client}; /// # /// # let MEILISEARCH_HOST = option_env!("MEILISEARCH_HOST").unwrap_or("http://localhost:7700"); /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); /// # /// # futures::executor::block_on(async move { /// let client = Client::new(MEILISEARCH_HOST, MEILISEARCH_API_KEY); - /// let mut key = KeyBuilder::new("My little lovely test key") - /// .create(&client).await.unwrap(); - /// - /// # assert_eq!(key.description, "My little lovely test key"); - /// - /// key.with_description("My not so little lovely test key"); - /// let key = key.update(&client).await.unwrap(); + /// let description = "My little lovely test key".to_string(); + /// let key = KeyBuilder::new() + /// .execute(&client).await.unwrap(); /// - /// # assert_eq!(key.description, "My not so little lovely test key".to_string()); + /// 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 update(&self, client: &Client) -> Result { + pub async fn execute(&self, client: &Client) -> Result { client.update_key(self).await } } -impl AsRef for Key { +impl AsRef for KeyUpdater { fn as_ref(&self) -> &str { &self.key } } -impl AsRef for Key { - fn as_ref(&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(); + /// ``` + pub fn new() -> KeysQuery { + Self::default() + } + + /// Specify the offset. + /// + /// # Example + /// + /// ``` + /// # use meilisearch_sdk::{key::KeysQuery, key::Action, client::Client}; + /// # + /// # let MEILISEARCH_HOST = option_env!("MEILISEARCH_HOST").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # futures::executor::block_on(async move { + /// # let client = Client::new(MEILISEARCH_HOST, MEILISEARCH_API_KEY); + /// 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::KeysQuery, key::Action, client::Client}; + /// # + /// # let MEILISEARCH_HOST = option_env!("MEILISEARCH_HOST").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # futures::executor::block_on(async move { + /// # let client = Client::new(MEILISEARCH_HOST, MEILISEARCH_API_KEY); + /// 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::KeysQuery, key::Action, client::Client}; + /// # + /// # let MEILISEARCH_HOST = option_env!("MEILISEARCH_HOST").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # futures::executor::block_on(async move { + /// # let client = Client::new(MEILISEARCH_HOST, MEILISEARCH_API_KEY); + /// 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]. /// @@ -251,42 +398,41 @@ impl AsRef for Key { /// # /// # futures::executor::block_on(async move { /// let client = Client::new(MEILISEARCH_HOST, MEILISEARCH_API_KEY); +/// let description = "My little lovely test key".to_string(); +/// let key = KeyBuilder::new() +/// .with_description(&description) +/// .execute(&client).await.unwrap(); /// -/// let key = KeyBuilder::new("My little lovely test key") -/// .with_action(Action::DocumentsAdd) -/// .with_index("*") -/// .create(&client).await.unwrap(); -/// -/// assert_eq!(key.description, "My little lovely test key"); +/// # assert_eq!(key.description, Some(description)); /// # client.delete_key(key).await.unwrap(); /// # }); /// ``` -#[derive(Debug, Serialize)] +#[derive(Debug, Serialize, Default)] #[serde(rename_all = "camelCase")] pub struct KeyBuilder { pub actions: Vec, - pub description: String, + #[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] with only a description. + /// Create a [KeyBuilder]. /// /// # Example /// /// ``` /// # use meilisearch_sdk::{key::KeyBuilder}; - /// let builder = KeyBuilder::new("My little lovely test key"); - /// ``` - pub fn new(description: impl AsRef) -> KeyBuilder { - Self { - actions: Vec::new(), - description: description.as_ref().to_string(), - expires_at: None, - indexes: Vec::new(), - } + /// let builder = KeyBuilder::new(); + /// ``` + pub fn new() -> KeyBuilder { + Self::default() } /// Declare a set of actions the [Key] will be able to execute. @@ -295,7 +441,7 @@ impl KeyBuilder { /// /// ``` /// # use meilisearch_sdk::key::{KeyBuilder, Action}; - /// let mut builder = KeyBuilder::new("My little lovely test key"); + /// let mut builder = KeyBuilder::new(); /// builder.with_actions(vec![Action::Search, Action::DocumentsAdd]); /// ``` pub fn with_actions(&mut self, actions: impl IntoIterator) -> &mut Self { @@ -309,7 +455,7 @@ impl KeyBuilder { /// /// ``` /// # use meilisearch_sdk::key::{KeyBuilder, Action}; - /// let mut builder = KeyBuilder::new("My little lovely test key"); + /// let mut builder = KeyBuilder::new(); /// builder.with_action(Action::DocumentsAdd); /// ``` pub fn with_action(&mut self, action: Action) -> &mut Self { @@ -324,7 +470,7 @@ impl KeyBuilder { /// ``` /// # use meilisearch_sdk::{key::KeyBuilder}; /// use time::{OffsetDateTime, Duration}; - /// let mut builder = KeyBuilder::new("My little lovely test key"); + /// 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); /// ``` @@ -338,9 +484,22 @@ impl KeyBuilder { /// # Example /// /// ``` - /// # use meilisearch_sdk::{key::KeyBuilder}; - /// let mut builder = KeyBuilder::new("My little lovely test key"); - /// builder.with_indexes(vec!["test", "movies"]); + /// # use meilisearch_sdk::{key::KeyBuilder, client::Client}; + /// # + /// # let MEILISEARCH_HOST = option_env!("MEILISEARCH_HOST").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # futures::executor::block_on(async move { + /// # let client = Client::new(MEILISEARCH_HOST, MEILISEARCH_API_KEY); + /// 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, @@ -359,7 +518,7 @@ impl KeyBuilder { /// /// ``` /// # use meilisearch_sdk::{key::KeyBuilder}; - /// let mut builder = KeyBuilder::new("My little lovely test key"); + /// let mut builder = KeyBuilder::new(); /// builder.with_index("test"); /// ``` pub fn with_index(&mut self, index: impl AsRef) -> &mut Self { @@ -367,6 +526,88 @@ impl KeyBuilder { self } + /// Add a description to the key. + /// + /// # Example + /// + /// ``` + /// # use meilisearch_sdk::{key::KeyBuilder, key::Action, client::Client}; + /// # + /// # let MEILISEARCH_HOST = option_env!("MEILISEARCH_HOST").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # futures::executor::block_on(async move { + /// # let client = Client::new(MEILISEARCH_HOST, MEILISEARCH_API_KEY); + /// 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 Self { + self.description = Some(desc.as_ref().to_string()); + self + } + + /// Add a name to the key. + /// + /// # Example + /// + /// ``` + /// # use meilisearch_sdk::{key::KeyBuilder, key::Action, client::Client}; + /// # + /// # let MEILISEARCH_HOST = option_env!("MEILISEARCH_HOST").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # futures::executor::block_on(async move { + /// # let client = Client::new(MEILISEARCH_HOST, MEILISEARCH_API_KEY); + /// 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 Self { + self.name = Some(desc.as_ref().to_string()); + self + } + + /// Add an uid to the key. + /// + /// # Example + /// + /// ``` + /// # use meilisearch_sdk::{key::KeyBuilder, key::Action, client::Client}; + /// # + /// # let MEILISEARCH_HOST = option_env!("MEILISEARCH_HOST").unwrap_or("http://localhost:7700"); + /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); + /// # + /// # futures::executor::block_on(async move { + /// # let client = Client::new(MEILISEARCH_HOST, MEILISEARCH_API_KEY); + /// 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 Self { + self.uid = Some(desc.as_ref().to_string()); + self + } + /// Create a [Key] from the builder. /// /// # Example @@ -379,14 +620,16 @@ impl KeyBuilder { /// # /// # futures::executor::block_on(async move { /// let client = Client::new(MEILISEARCH_HOST, MEILISEARCH_API_KEY); - /// let key = KeyBuilder::new("My little lovely test key") - /// .create(&client).await.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, "My little lovely test key"); + /// # assert_eq!(key.description, Some(description)); /// # client.delete_key(key).await.unwrap(); /// # }); /// ``` - pub async fn create(&self, client: &Client) -> Result { + pub async fn execute(&self, client: &Client) -> Result { client.create_key(self).await } } @@ -447,4 +690,23 @@ pub enum Action { /// Provides access to the [get Meilisearch version](https://docs.meilisearch.com/reference/api/version.md#get-version-of-meilisearch) endpoint. #[serde(rename = "version")] Version, + /// Provides access to the [get Key](https://docs.meilisearch.com/reference/api/keys.html#get-one-key) and [get Keys](https://docs.meilisearch.com/reference/api/keys.html#get-all-keys) endpoints. + #[serde(rename = "keys.get")] + KeyGet, + /// Provides access to the [create key](https://docs.meilisearch.com/reference/api/keys.html#create-a-key) endpoint. + #[serde(rename = "keys.create")] + KeyCreate, + /// Provides access to the [update key](https://docs.meilisearch.com/reference/api/keys.html#update-a-key) endpoint. + #[serde(rename = "keys.update")] + KeyUpdate, + /// Provides access to the [delete key](https://docs.meilisearch.com/reference/api/keys.html#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/src/lib.rs b/src/lib.rs index 3b5417bc..61b0fa1c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -214,7 +214,7 @@ //! ], //! "offset": 0, //! "limit": 20, -//! "nbHits": 1, +//! "estimatedTotalHits": 1, //! "processingTimeMs": 0, //! "query": "wonder" //! } @@ -225,6 +225,8 @@ /// Module containing the [client::Client] struct. pub mod client; +/// Module representing the [documents] structures. +pub mod documents; /// Module containing the [document::Document] trait. pub mod dumps; /// Module containing the [errors::Error] struct. @@ -238,7 +240,11 @@ mod request; pub mod search; /// Module containing [settings::Settings]. pub mod settings; +/// Module representing the [task_info::TaskInfo]s. +pub mod task_info; /// Module representing the [tasks::Task]s. pub mod tasks; /// Module that generates tenant tokens. mod tenant_tokens; +/// Module containing utilies functions. +mod utils; diff --git a/src/request.rs b/src/request.rs index e748d543..e3028a6b 100644 --- a/src/request.rs +++ b/src/request.rs @@ -112,7 +112,7 @@ pub(crate) async fn request { + Method::Get(query) => { + let query = yaup::to_string(query)?; + + mut_url = if query.is_empty() { + mut_url.to_string() + } else { + format!("{}?{}", mut_url, query) + }; + request.method("GET"); } Method::Delete => { @@ -145,13 +153,14 @@ pub(crate) async fn request Response::from(response), - Err(e) => { - error!("Network error: {:?}", e); - return Err(Error::UnreachableServer); - } - }; + let response = + match JsFuture::from(window.fetch_with_str_and_init(mut_url.as_str(), &request)).await { + Ok(response) => Response::from(response), + Err(e) => { + error!("Network error: {:?}", e); + return Err(Error::UnreachableServer); + } + }; let status = response.status() as u16; let text = match response.text() { Ok(text) => match JsFuture::from(text).await { diff --git a/src/search.rs b/src/search.rs index 986d635f..219310a5 100644 --- a/src/search.rs +++ b/src/search.rs @@ -3,7 +3,7 @@ use serde::{de::DeserializeOwned, Deserialize, Serialize, Serializer}; use serde_json::{Map, Value}; use std::collections::HashMap; -#[derive(Deserialize, Debug, PartialEq)] +#[derive(Deserialize, Debug, Eq, PartialEq)] pub struct MatchRange { pub start: usize, pub length: usize, @@ -20,8 +20,8 @@ pub struct SearchResult { #[serde(rename = "_formatted")] pub formatted_result: Option>, /// The object that contains information about the matches. - #[serde(rename = "_matchesInfo")] - pub matches_info: Option>>, + #[serde(rename = "_matchesPosition")] + pub matches_position: Option>>, } #[derive(Deserialize, Debug)] @@ -35,13 +35,9 @@ pub struct SearchResults { /// Number of results returned pub limit: usize, /// Total number of matches - pub nb_hits: usize, - /// Whether nb_hits is exhaustive - pub exhaustive_nb_hits: bool, + pub estimated_total_hits: usize, /// Distribution of the given facets - pub facets_distribution: Option>>, - /// Whether facet_distribution is exhaustive - pub exhaustive_facets_count: Option, + pub facet_distribution: Option>>, /// Processing time of the query pub processing_time_ms: usize, /// Query originating the response @@ -101,18 +97,40 @@ type AttributeToCrop<'a> = (&'a str, Option); /// # Examples /// /// ``` +/// use serde::{Serialize, Deserialize}; /// # use meilisearch_sdk::{client::Client, search::Query, indexes::Index}; /// # /// # let MEILISEARCH_HOST = option_env!("MEILISEARCH_HOST").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, +/// } +/// +/// # futures::executor::block_on(async move { /// # let client = Client::new(MEILISEARCH_HOST, MEILISEARCH_API_KEY); -/// # let index = client.index("does not matter"); -/// let query = Query::new(&index) +/// # 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 = Query::new(&index) /// .with_query("space") /// .with_offset(42) /// .with_limit(21) -/// .build(); // you can also execute() instead of build() +/// .execute::() +/// .await +/// .unwrap(); +/// +/// assert_eq!(res.limit, 21); +/// # index.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); +/// # }); /// ``` /// /// ``` @@ -122,7 +140,7 @@ type AttributeToCrop<'a> = (&'a str, Option); /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey"); /// # /// # let client = Client::new(MEILISEARCH_HOST, MEILISEARCH_API_KEY); -/// # let index = client.index("does not matter"); +/// # let index = client.index("search_query_builder_build"); /// let query = index.search() /// .with_query("space") /// .with_offset(42) @@ -163,7 +181,7 @@ pub struct Query<'a> { /// Default: all attributes found in the documents. #[serde(skip_serializing_if = "Option::is_none")] #[serde(serialize_with = "serialize_with_wildcard")] - pub facets_distribution: Option>, + pub facets: Option>, /// Attributes to sort. #[serde(skip_serializing_if = "Option::is_none")] pub sort: Option<&'a [&'a str]>, @@ -215,7 +233,7 @@ pub struct Query<'a> { /// /// Default: `false` #[serde(skip_serializing_if = "Option::is_none")] - pub matches: Option, + pub show_matches_position: Option, } #[allow(missing_docs)] @@ -228,7 +246,7 @@ impl<'a> Query<'a> { limit: None, filter: None, sort: None, - facets_distribution: None, + facets: None, attributes_to_retrieve: None, attributes_to_crop: None, crop_length: None, @@ -236,13 +254,14 @@ impl<'a> Query<'a> { attributes_to_highlight: None, highlight_pre_tag: None, highlight_post_tag: None, - matches: None, + show_matches_position: None, } } pub fn with_query<'b>(&'b mut self, query: &'a str) -> &'b mut Query<'a> { self.query = Some(query); self } + pub fn with_offset<'b>(&'b mut self, offset: usize) -> &'b mut Query<'a> { self.offset = Some(offset); self @@ -255,11 +274,8 @@ impl<'a> Query<'a> { self.filter = Some(filter); self } - pub fn with_facets_distribution<'b>( - &'b mut self, - facets_distribution: Selectors<&'a [&'a str]>, - ) -> &'b mut Query<'a> { - self.facets_distribution = Some(facets_distribution); + pub fn with_facets<'b>(&'b mut self, facets: Selectors<&'a [&'a str]>) -> &'b mut Query<'a> { + self.facets = Some(facets); self } pub fn with_sort<'b>(&'b mut self, sort: &'a [&'a str]) -> &'b mut Query<'a> { @@ -309,8 +325,11 @@ impl<'a> Query<'a> { self.highlight_post_tag = Some(highlight_post_tag); self } - pub fn with_matches<'b>(&'b mut self, matches: bool) -> &'b mut Query<'a> { - self.matches = Some(matches); + pub fn with_show_matches_position<'b>( + &'b mut self, + show_matches_position: bool, + ) -> &'b mut Query<'a> { + self.show_matches_position = Some(show_matches_position); self } pub fn build(&mut self) -> Query<'a> { @@ -376,6 +395,19 @@ mod tests { Ok(()) } + #[meilisearch_test] + async fn test_query_builder(_client: Client, index: Index) -> Result<(), Error> { + let mut query = Query::new(&index); + query.with_query("space").with_offset(42).with_limit(21); + + let res = query.execute::().await.unwrap(); + + assert_eq!(res.query, "space".to_string()); + assert_eq!(res.limit, 21); + assert_eq!(res.offset, 42); + Ok(()) + } + #[meilisearch_test] async fn test_query_string(client: Client, index: Index) -> Result<(), Error> { setup_test_index(&client, &index).await?; @@ -450,11 +482,11 @@ mod tests { setup_test_index(&client, &index).await?; let mut query = Query::new(&index); - query.with_facets_distribution(Selectors::All); + query.with_facets(Selectors::All); let results: SearchResults = index.execute_query(&query).await?; assert_eq!( results - .facets_distribution + .facet_distribution .unwrap() .get("kind") .unwrap() @@ -464,11 +496,11 @@ mod tests { ); let mut query = Query::new(&index); - query.with_facets_distribution(Selectors::Some(&["kind"])); + query.with_facets(Selectors::Some(&["kind"])); let results: SearchResults = index.execute_query(&query).await?; assert_eq!( results - .facets_distribution + .facet_distribution .clone() .unwrap() .get("kind") @@ -479,7 +511,7 @@ mod tests { ); assert_eq!( results - .facets_distribution + .facet_distribution .unwrap() .get("kind") .unwrap() @@ -609,7 +641,7 @@ mod tests { assert_eq!( &Document { id: 0, - value: "(ꈍᴗꈍ)consectetur adipiscing elit, sed do eiusmod(ꈍᴗꈍ)".to_string(), + value: "(ꈍᴗꈍ) sed do eiusmod tempor incididunt ut(ꈍᴗꈍ)".to_string(), kind: "text".to_string(), nested: Nested { child: "first".to_string() @@ -634,7 +666,6 @@ mod tests { query.with_highlight_post_tag(" ⊂(´• ω •`⊂)"); let results: SearchResults = index.execute_query(&query).await?; - dbg!(&results); assert_eq!( &Document { id: 2, @@ -689,17 +720,17 @@ mod tests { } #[meilisearch_test] - async fn test_query_matches(client: Client, index: Index) -> Result<(), Error> { + async fn test_query_show_matches_position(client: Client, index: Index) -> Result<(), Error> { setup_test_index(&client, &index).await?; let mut query = Query::new(&index); query.with_query("dolor text"); - query.with_matches(true); + query.with_show_matches_position(true); let results: SearchResults = index.execute_query(&query).await?; - assert_eq!(results.hits[0].matches_info.as_ref().unwrap().len(), 2); + assert_eq!(results.hits[0].matches_position.as_ref().unwrap().len(), 2); assert_eq!( results.hits[0] - .matches_info + .matches_position .as_ref() .unwrap() .get("value") @@ -719,6 +750,7 @@ mod tests { let mut query = Query::new(&index); query.with_query("harry \"of Fire\""); let results: SearchResults = index.execute_query(&query).await?; + assert_eq!(results.hits.len(), 1); Ok(()) } @@ -733,10 +765,10 @@ mod tests { setup_test_index(&client, &index).await?; let meilisearch_host = option_env!("MEILISEARCH_HOST").unwrap_or("http://localhost:7700"); - let key = KeyBuilder::new("key for generate_tenant_token test") + let key = KeyBuilder::new() .with_action(Action::All) .with_index("*") - .create(&client) + .execute(&client) .await .unwrap(); let allowed_client = Client::new(meilisearch_host, key.key); @@ -751,9 +783,10 @@ mod tests { for rules in search_rules { let token = allowed_client - .generate_tenant_token(rules, None, None) + .generate_tenant_token(key.uid.clone(), rules, None, None) .expect("Cannot generate tenant token."); - let new_client = Client::new(meilisearch_host, token); + + let new_client = Client::new(meilisearch_host, token.clone()); let result: SearchResults = new_client .index(index.uid.to_string()) diff --git a/src/settings.rs b/src/settings.rs index 254d17db..e2f33da9 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -2,7 +2,7 @@ use crate::{ errors::Error, indexes::Index, request::{request, Method}, - tasks::Task, + task_info::TaskInfo, }; use serde::{Deserialize, Serialize}; use std::collections::HashMap; @@ -478,11 +478,11 @@ impl Index { /// # index.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); /// # }); /// ``` - pub async fn set_settings(&self, settings: &Settings) -> Result { - request::<&Settings, Task>( + pub async fn set_settings(&self, settings: &Settings) -> Result { + request::<&Settings, TaskInfo>( &format!("{}/indexes/{}/settings", self.client.host, self.uid), &self.client.api_key, - Method::Post(settings), + Method::Patch(settings), 202, ) .await @@ -515,14 +515,14 @@ impl Index { pub async fn set_synonyms( &self, synonyms: &HashMap>, - ) -> Result { - request::<&HashMap>, Task>( + ) -> Result { + request::<&HashMap>, TaskInfo>( &format!( "{}/indexes/{}/settings/synonyms", self.client.host, self.uid ), &self.client.api_key, - Method::Post(synonyms), + Method::Put(synonyms), 202, ) .await @@ -551,14 +551,14 @@ impl Index { pub async fn set_stop_words( &self, stop_words: impl IntoIterator>, - ) -> Result { - request::, Task>( + ) -> Result { + request::, TaskInfo>( &format!( "{}/indexes/{}/settings/stop-words", self.client.host, self.uid ), &self.client.api_key, - Method::Post( + Method::Put( stop_words .into_iter() .map(|v| v.as_ref().to_string()) @@ -601,14 +601,14 @@ impl Index { pub async fn set_ranking_rules( &self, ranking_rules: impl IntoIterator>, - ) -> Result { - request::, Task>( + ) -> Result { + request::, TaskInfo>( &format!( "{}/indexes/{}/settings/ranking-rules", self.client.host, self.uid ), &self.client.api_key, - Method::Post( + Method::Put( ranking_rules .into_iter() .map(|v| v.as_ref().to_string()) @@ -642,14 +642,14 @@ impl Index { pub async fn set_filterable_attributes( &self, filterable_attributes: impl IntoIterator>, - ) -> Result { - request::, Task>( + ) -> Result { + request::, TaskInfo>( &format!( "{}/indexes/{}/settings/filterable-attributes", self.client.host, self.uid ), &self.client.api_key, - Method::Post( + Method::Put( filterable_attributes .into_iter() .map(|v| v.as_ref().to_string()) @@ -683,14 +683,14 @@ impl Index { pub async fn set_sortable_attributes( &self, sortable_attributes: impl IntoIterator>, - ) -> Result { - request::, Task>( + ) -> Result { + request::, TaskInfo>( &format!( "{}/indexes/{}/settings/sortable-attributes", self.client.host, self.uid ), &self.client.api_key, - Method::Post( + Method::Put( sortable_attributes .into_iter() .map(|v| v.as_ref().to_string()) @@ -723,14 +723,14 @@ impl Index { pub async fn set_distinct_attribute( &self, distinct_attribute: impl AsRef, - ) -> Result { - request::( + ) -> Result { + request::( &format!( "{}/indexes/{}/settings/distinct-attribute", self.client.host, self.uid ), &self.client.api_key, - Method::Post(distinct_attribute.as_ref().to_string()), + Method::Put(distinct_attribute.as_ref().to_string()), 202, ) .await @@ -758,14 +758,14 @@ impl Index { pub async fn set_searchable_attributes( &self, searchable_attributes: impl IntoIterator>, - ) -> Result { - request::, Task>( + ) -> Result { + request::, TaskInfo>( &format!( "{}/indexes/{}/settings/searchable-attributes", self.client.host, self.uid ), &self.client.api_key, - Method::Post( + Method::Put( searchable_attributes .into_iter() .map(|v| v.as_ref().to_string()) @@ -798,14 +798,14 @@ impl Index { pub async fn set_displayed_attributes( &self, displayed_attributes: impl IntoIterator>, - ) -> Result { - request::, Task>( + ) -> Result { + request::, TaskInfo>( &format!( "{}/indexes/{}/settings/displayed-attributes", self.client.host, self.uid ), &self.client.api_key, - Method::Post( + Method::Put( displayed_attributes .into_iter() .map(|v| v.as_ref().to_string()) @@ -836,8 +836,8 @@ impl Index { /// # index.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); /// # }); /// ``` - pub async fn reset_settings(&self) -> Result { - request::<(), Task>( + pub async fn reset_settings(&self) -> Result { + request::<(), TaskInfo>( &format!("{}/indexes/{}/settings", self.client.host, self.uid), &self.client.api_key, Method::Delete, @@ -865,8 +865,8 @@ impl Index { /// # index.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); /// # }); /// ``` - pub async fn reset_synonyms(&self) -> Result { - request::<(), Task>( + pub async fn reset_synonyms(&self) -> Result { + request::<(), TaskInfo>( &format!( "{}/indexes/{}/settings/synonyms", self.client.host, self.uid @@ -897,8 +897,8 @@ impl Index { /// # index.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); /// # }); /// ``` - pub async fn reset_stop_words(&self) -> Result { - request::<(), Task>( + pub async fn reset_stop_words(&self) -> Result { + request::<(), TaskInfo>( &format!( "{}/indexes/{}/settings/stop-words", self.client.host, self.uid @@ -930,8 +930,8 @@ impl Index { /// # index.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); /// # }); /// ``` - pub async fn reset_ranking_rules(&self) -> Result { - request::<(), Task>( + pub async fn reset_ranking_rules(&self) -> Result { + request::<(), TaskInfo>( &format!( "{}/indexes/{}/settings/ranking-rules", self.client.host, self.uid @@ -962,8 +962,8 @@ impl Index { /// # index.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); /// # }); /// ``` - pub async fn reset_filterable_attributes(&self) -> Result { - request::<(), Task>( + pub async fn reset_filterable_attributes(&self) -> Result { + request::<(), TaskInfo>( &format!( "{}/indexes/{}/settings/filterable-attributes", self.client.host, self.uid @@ -994,8 +994,8 @@ impl Index { /// # index.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); /// # }); /// ``` - pub async fn reset_sortable_attributes(&self) -> Result { - request::<(), Task>( + pub async fn reset_sortable_attributes(&self) -> Result { + request::<(), TaskInfo>( &format!( "{}/indexes/{}/settings/sortable-attributes", self.client.host, self.uid @@ -1026,8 +1026,8 @@ impl Index { /// # index.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); /// # }); /// ``` - pub async fn reset_distinct_attribute(&self) -> Result { - request::<(), Task>( + pub async fn reset_distinct_attribute(&self) -> Result { + request::<(), TaskInfo>( &format!( "{}/indexes/{}/settings/distinct-attribute", self.client.host, self.uid @@ -1058,8 +1058,8 @@ impl Index { /// # index.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); /// # }); /// ``` - pub async fn reset_searchable_attributes(&self) -> Result { - request::<(), Task>( + pub async fn reset_searchable_attributes(&self) -> Result { + request::<(), TaskInfo>( &format!( "{}/indexes/{}/settings/searchable-attributes", self.client.host, self.uid @@ -1090,8 +1090,8 @@ impl Index { /// # index.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); /// # }); /// ``` - pub async fn reset_displayed_attributes(&self) -> Result { - request::<(), Task>( + pub async fn reset_displayed_attributes(&self) -> Result { + request::<(), TaskInfo>( &format!( "{}/indexes/{}/settings/displayed-attributes", self.client.host, self.uid diff --git a/src/task_info.rs b/src/task_info.rs new file mode 100644 index 00000000..cb970c7d --- /dev/null +++ b/src/task_info.rs @@ -0,0 +1,176 @@ +use serde::Deserialize; +use std::time::Duration; +use time::OffsetDateTime; + +use crate::{client::Client, errors::Error, 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 { + 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::Task, task_info::TaskInfo}; + /// # use serde::{Serialize, Deserialize}; + /// # + /// # #[derive(Debug, Serialize, Deserialize, PartialEq)] + /// # struct Document { + /// # id: usize, + /// # value: String, + /// # kind: String, + /// # } + /// # + /// # + /// # futures::executor::block_on(async move { + /// let client = Client::new("http://localhost:7700", "masterKey"); + /// 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 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": "mieli", + "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 == "mieli" && 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: "The Social Network".to_string(), + }, + Document { + id: 1, + kind: "title".into(), + value: "Harry Potter and the Sorcerer's Stone".to_string(), + }, + ], + 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, movies: Index) -> Result<(), Error> { + let task_info = movies.set_ranking_rules(["wrong_ranking_rule"]).await?; + let task = client.wait_for_task(task_info, None, None).await?; + + let error = task.unwrap_failure(); + assert_eq!(error.error_code, ErrorCode::InvalidRankingRule); + assert_eq!(error.error_type, ErrorType::InvalidRequest); + Ok(()) + } +} diff --git a/src/tasks.rs b/src/tasks.rs index 050b1f83..45418631 100644 --- a/src/tasks.rs +++ b/src/tasks.rs @@ -1,4 +1,4 @@ -use serde::{Deserialize, Deserializer}; +use serde::{Deserialize, Deserializer, Serialize}; use std::time::Duration; use time::OffsetDateTime; @@ -9,20 +9,41 @@ use crate::{ #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase", tag = "type")] pub enum TaskType { - ClearAll, Customs, - DocumentAddition { details: Option }, - DocumentPartial { details: Option }, - DocumentDeletion { details: Option }, - IndexCreation { details: Option }, - IndexUpdate { details: Option }, - IndexDeletion { details: Option }, - SettingsUpdate { details: Option }, + DocumentAdditionOrUpdate { + details: Option, + }, + DocumentDeletion { + details: Option, + }, + IndexCreation { + details: Option, + }, + IndexUpdate { + details: Option, + }, + IndexDeletion { + details: Option, + }, + SettingsUpdate { + details: Option, + }, + DumpCreation { + details: Option, + }, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct TasksResults { + pub results: Vec, + pub limit: u32, + pub from: Option, + pub next: Option, } #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct DocumentAddition { +pub struct DocumentAdditionOrUpdate { pub indexed_documents: Option, pub received_documents: usize, } @@ -51,16 +72,22 @@ pub struct IndexDeletion { pub deleted_documents: Option, } +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct DumpCreation { + pub dump_uid: Option, +} + #[derive(Deserialize, Debug, Clone)] #[serde(rename_all = "camelCase")] pub struct FailedTask { pub error: MeilisearchError, #[serde(flatten)] - pub task: ProcessedTask, + pub task: SucceededTask, } -impl AsRef for FailedTask { - fn as_ref(&self) -> &u64 { +impl AsRef for FailedTask { + fn as_ref(&self) -> &u32 { &self.task.uid } } @@ -76,7 +103,7 @@ where #[derive(Deserialize, Debug, Clone)] #[serde(rename_all = "camelCase")] -pub struct ProcessedTask { +pub struct SucceededTask { #[serde(deserialize_with = "deserialize_duration")] pub duration: Duration, #[serde(with = "time::serde::rfc3339")] @@ -85,14 +112,14 @@ pub struct ProcessedTask { pub started_at: OffsetDateTime, #[serde(with = "time::serde::rfc3339")] pub finished_at: OffsetDateTime, - pub index_uid: String, + pub index_uid: Option, #[serde(flatten)] pub update_type: TaskType, - pub uid: u64, + pub uid: u32, } -impl AsRef for ProcessedTask { - fn as_ref(&self) -> &u64 { +impl AsRef for SucceededTask { + fn as_ref(&self) -> &u32 { &self.uid } } @@ -102,14 +129,14 @@ impl AsRef for ProcessedTask { pub struct EnqueuedTask { #[serde(with = "time::serde::rfc3339")] pub enqueued_at: OffsetDateTime, - pub index_uid: String, + pub index_uid: Option, #[serde(flatten)] pub update_type: TaskType, - pub uid: u64, + pub uid: u32, } -impl AsRef for EnqueuedTask { - fn as_ref(&self) -> &u64 { +impl AsRef for EnqueuedTask { + fn as_ref(&self) -> &u32 { &self.uid } } @@ -131,12 +158,12 @@ pub enum Task { }, Succeeded { #[serde(flatten)] - content: ProcessedTask, + content: SucceededTask, }, } impl Task { - pub fn get_uid(&self) -> u64 { + pub fn get_uid(&self) -> u32 { match self { Self::Enqueued { content } | Self::Processing { content } => *content.as_ref(), Self::Failed { content } => *content.as_ref(), @@ -225,12 +252,12 @@ impl Task { match self { Self::Succeeded { content: - ProcessedTask { + SucceededTask { index_uid, update_type: TaskType::IndexCreation { .. }, .. }, - } => Ok(client.index(index_uid)), + } => Ok(client.index(index_uid.unwrap())), _ => Err(self), } } @@ -252,7 +279,6 @@ impl Task { /// # let task = client.create_index("unwrap_failure", None).await.unwrap(); /// # let index = client.wait_for_task(task, None, None).await.unwrap().try_make_index(&client).unwrap(); /// - /// /// let task = index.set_ranking_rules(["wrong_ranking_rule"]) /// .await /// .unwrap() @@ -303,6 +329,7 @@ impl Task { /// assert!(task.is_failure()); /// # index.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); /// # }); + /// ``` pub fn is_failure(&self) -> bool { matches!(self, Self::Failed { .. }) } @@ -330,6 +357,7 @@ impl Task { /// assert!(task.is_success()); /// # task.try_make_index(&client).unwrap().delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap(); /// # }); + /// ``` pub fn is_success(&self) -> bool { matches!(self, Self::Succeeded { .. }) } @@ -337,8 +365,9 @@ impl Task { /// 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_HOST = option_env!("MEILISEARCH_HOST").unwrap_or("http://localhost:7700"); @@ -346,21 +375,22 @@ impl Task { /// # /// # futures::executor::block_on(async move { /// # let client = Client::new(MEILISEARCH_HOST, MEILISEARCH_API_KEY); - /// let task = client + /// 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(); /// # }); + /// ``` pub fn is_pending(&self) -> bool { matches!(self, Self::Enqueued { .. } | Self::Processing { .. }) } } -impl AsRef for Task { - fn as_ref(&self) -> &u64 { +impl AsRef for Task { + fn as_ref(&self) -> &u32 { match self { Self::Enqueued { content } | Self::Processing { content } => content.as_ref(), Self::Succeeded { content } => content.as_ref(), @@ -369,32 +399,73 @@ impl AsRef for Task { } } -#[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; +#[derive(Debug, Serialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct TasksQuery<'a> { + #[serde(skip_serializing)] + pub client: &'a Client, + // Index uids array to only retrieve the tasks of the indexes. + #[serde(skip_serializing_if = "Option::is_none")] + pub index_uid: Option>, + // Statuses array to only retrieve the tasks with these statuses. + #[serde(skip_serializing_if = "Option::is_none")] + pub status: Option>, + // Types array to only retrieve the tasks with these [TaskType]. + #[serde(skip_serializing_if = "Option::is_none", rename = "type")] + pub task_type: Option>, + // Maximum number of tasks to return + #[serde(skip_serializing_if = "Option::is_none")] + pub limit: Option, + // The first task uid that should be returned + #[serde(skip_serializing_if = "Option::is_none")] + pub from: Option, } -#[cfg(target_arch = "wasm32")] -pub(crate) async fn async_sleep(interval: Duration) { - use std::convert::TryInto; - use wasm_bindgen_futures::JsFuture; - - JsFuture::from(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(); +#[allow(missing_docs)] +impl<'a> TasksQuery<'a> { + pub fn new(client: &'a Client) -> TasksQuery<'a> { + TasksQuery { + client, + index_uid: None, + status: None, + task_type: None, + limit: None, + from: None, + } + } + pub fn with_index_uid<'b>( + &'b mut self, + index_uid: impl IntoIterator, + ) -> &'b mut TasksQuery<'a> { + self.index_uid = Some(index_uid.into_iter().collect()); + self + } + pub fn with_status<'b>( + &'b mut self, + status: impl IntoIterator, + ) -> &'b mut TasksQuery<'a> { + self.status = Some(status.into_iter().collect()); + self + } + pub fn with_type<'b>( + &'b mut self, + task_type: impl IntoIterator, + ) -> &'b mut TasksQuery<'a> { + self.task_type = Some(task_type.into_iter().collect()); + self + } + pub fn with_limit<'b>(&'b mut self, limit: u32) -> &'b mut TasksQuery<'a> { + self.limit = Some(limit); + self + } + pub fn with_from<'b>(&'b mut self, from: u32) -> &'b mut TasksQuery<'a> { + self.from = Some(from); + self + } + + pub async fn execute(&'a self) -> Result { + self.client.get_tasks_with(self).await + } } #[cfg(test)] @@ -405,8 +476,9 @@ mod test { errors::{ErrorCode, ErrorType}, }; use meilisearch_test_macro::meilisearch_test; + use mockito::mock; use serde::{Deserialize, Serialize}; - use std::time::{self, Duration}; + use std::time::Duration; #[derive(Debug, Serialize, Deserialize, PartialEq)] struct Document { @@ -429,7 +501,7 @@ mod test { "enqueuedAt": "2022-02-03T13:02:38.369634Z", "indexUid": "mieli", "status": "enqueued", - "type": "documentAddition", + "type": "documentAdditionOrUpdate", "uid": 12 }"#, ) @@ -440,8 +512,8 @@ mod test { Task::Enqueued { content: EnqueuedTask { enqueued_at, - index_uid, - update_type: TaskType::DocumentAddition { details: None }, + index_uid: Some(index_uid), + update_type: TaskType::DocumentAdditionOrUpdate { details: None }, uid: 12, } } @@ -460,7 +532,7 @@ mod test { "indexUid": "mieli", "startedAt": "2022-02-03T15:17:02.812338Z", "status": "processing", - "type": "documentAddition", + "type": "documentAdditionOrUpdate", "uid": 14 }"#, ) @@ -470,8 +542,8 @@ mod test { task, Task::Processing { content: EnqueuedTask { - update_type: TaskType::DocumentAddition { - details: Some(DocumentAddition { + update_type: TaskType::DocumentAdditionOrUpdate { + details: Some(DocumentAdditionOrUpdate { received_documents: 19547, indexed_documents: None, }) @@ -495,7 +567,7 @@ mod test { "indexUid": "mieli", "startedAt": "2022-02-03T15:17:02.812338Z", "status": "succeeded", - "type": "documentAddition", + "type": "documentAdditionOrUpdate", "uid": 14 }"#, ) @@ -504,9 +576,9 @@ mod test { assert!(matches!( task, Task::Succeeded { - content: ProcessedTask { - update_type: TaskType::DocumentAddition { - details: Some(DocumentAddition { + content: SucceededTask { + update_type: TaskType::DocumentAdditionOrUpdate { + details: Some(DocumentAdditionOrUpdate { received_documents: 19547, indexed_documents: Some(19546), }) @@ -521,11 +593,8 @@ mod test { } #[meilisearch_test] - async fn test_wait_for_pending_updates_with_args( - client: Client, - movies: Index, - ) -> Result<(), Error> { - let status = movies + async fn test_wait_for_task_with_args(client: Client, movies: Index) -> Result<(), Error> { + let task = movies .add_documents( &[ Document { @@ -549,62 +618,97 @@ mod test { ) .await?; - assert!(matches!(status, Task::Succeeded { .. })); + assert!(matches!(task, Task::Succeeded { .. })); Ok(()) } #[meilisearch_test] - async fn test_wait_for_pending_updates_time_out( - client: Client, - movies: Index, - ) -> Result<(), Error> { - 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?; + async fn test_get_tasks_no_params() -> Result<(), Error> { + let mock_server_url = &mockito::server_url(); + let client = Client::new(mock_server_url, "masterKey"); + let path = "/tasks"; - let error = client - .wait_for_task( - task, - Some(Duration::from_millis(1)), - Some(Duration::from_nanos(1)), - ) - .await - .unwrap_err(); + let mock_res = mock("GET", path).with_status(200).create(); + let _ = client.get_tasks().await; + mock_res.assert(); - assert!(matches!(error, Error::Timeout)); Ok(()) } #[meilisearch_test] - async fn test_async_sleep() { - let sleep_duration = time::Duration::from_millis(10); - let now = time::Instant::now(); + async fn test_get_tasks_with_params() -> Result<(), Error> { + let mock_server_url = &mockito::server_url(); + let client = Client::new(mock_server_url, "masterKey"); + let path = + "/tasks?indexUid=movies,test&status=equeued&type=documentDeletion&limit=0&from=1"; + + let mock_res = mock("GET", path).with_status(200).create(); + + let mut query = TasksQuery::new(&client); + query + .with_index_uid(["movies", "test"]) + .with_status(["equeued"]) + .with_type(["documentDeletion"]) + .with_from(1) + .with_limit(0); - async_sleep(sleep_duration).await; + let _ = client.get_tasks_with(&query).await; - assert!(now.elapsed() >= sleep_duration); + mock_res.assert(); + Ok(()) + } + + #[meilisearch_test] + async fn test_get_tasks_on_struct_with_params() -> Result<(), Error> { + let mock_server_url = &mockito::server_url(); + let client = Client::new(mock_server_url, "masterKey"); + let path = "/tasks?indexUid=movies,test&status=equeued&type=documentDeletion"; + + let mock_res = mock("GET", path).with_status(200).create(); + + let mut query = TasksQuery::new(&client); + let _ = query + .with_index_uid(["movies", "test"]) + .with_status(["equeued"]) + .with_type(["documentDeletion"]) + .execute() + .await; + + // let _ = client.get_tasks(&query).await; + mock_res.assert(); + Ok(()) + } + + #[meilisearch_test] + async fn test_get_tasks_with_none_existant_index_uid(client: Client) -> Result<(), Error> { + let mut query = TasksQuery::new(&client); + query.with_index_uid(["no_name"]); + let tasks = client.get_tasks_with(&query).await.unwrap(); + + assert_eq!(tasks.results.len(), 0); + Ok(()) } #[meilisearch_test] - async fn test_failing_update(client: Client, movies: Index) -> Result<(), Error> { - let task = movies.set_ranking_rules(["wrong_ranking_rule"]).await?; - let status = client.wait_for_task(task, None, None).await?; + async fn test_get_tasks_with_execute(client: Client) -> Result<(), Error> { + let tasks = TasksQuery::new(&client) + .with_index_uid(["no_name"]) + .execute() + .await + .unwrap(); + + assert_eq!(tasks.results.len(), 0); + Ok(()) + } + + #[meilisearch_test] + async fn test_failing_task(client: Client, movies: Index) -> Result<(), Error> { + let task_info = movies.set_ranking_rules(["wrong_ranking_rule"]).await?; + + let task = client.get_task(task_info).await?; + let task = client.wait_for_task(task, None, None).await?; - let error = status.unwrap_failure(); + let error = task.unwrap_failure(); assert_eq!(error.error_code, ErrorCode::InvalidRankingRule); assert_eq!(error.error_type, ErrorType::InvalidRequest); Ok(()) diff --git a/src/tenant_tokens.rs b/src/tenant_tokens.rs index 369f401b..fda30e02 100644 --- a/src/tenant_tokens.rs +++ b/src/tenant_tokens.rs @@ -1,34 +1,44 @@ -use crate::{ - errors::* -}; -use serde::{Serialize, Deserialize}; -use jsonwebtoken::{encode, Header, EncodingKey}; -use time::{OffsetDateTime}; +use crate::errors::*; +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")] +#[cfg(not(target_arch = "wasm32"))] +#[serde(rename_all = "camelCase")] struct TenantTokenClaim { - api_key_prefix: String, + api_key_uid: String, search_rules: Value, #[serde(with = "time::serde::timestamp::option")] exp: Option, } -pub fn generate_tenant_token(search_rules: Value, api_key: impl AsRef, expires_at: Option) -> Result { - if api_key.as_ref().chars().count() < 8 { - return Err(Error::TenantTokensInvalidApiKey) +#[cfg(not(target_arch = "wasm32"))] +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) + return Err(Error::TenantTokensExpiredSignature); } - let key_prefix = api_key.as_ref().chars().take(8).collect(); let claims = TenantTokenClaim { - api_key_prefix: key_prefix, + api_key_uid, exp: expires_at, - search_rules + search_rules, }; let token = encode( @@ -42,9 +52,9 @@ pub fn generate_tenant_token(search_rules: Value, api_key: impl AsRef, expi #[cfg(test)] mod tests { - use serde_json::json; use crate::tenant_tokens::*; - use jsonwebtoken::{decode, DecodingKey, Validation, Algorithm}; + use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation}; + use serde_json::json; use std::collections::HashSet; const SEARCH_RULES: [&str; 1] = ["*"]; @@ -60,13 +70,19 @@ mod tests { #[test] fn test_generate_token_with_given_key() { - let token = generate_tenant_token(json!(SEARCH_RULES), VALID_KEY, None).unwrap(); + let api_key_uid = "76cf8b87-fd12-4688-ad34-260d930ca4f4".to_string(); + 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() + &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() + &token, + &DecodingKey::from_secret("not-the-same-key".as_ref()), + &build_validation(), ); assert!(valid_key.is_ok()); @@ -74,20 +90,25 @@ mod tests { } #[test] - fn test_generate_token_without_key() { + fn test_generate_token_without_uid() { + let api_key_uid = "".to_string(); let key = String::from(""); - let token = generate_tenant_token(json!(SEARCH_RULES), &key, None); + 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 = "76cf8b87-fd12-4688-ad34-260d930ca4f4".to_string(); let exp = OffsetDateTime::now_utc() + time::Duration::HOUR; - let token = generate_tenant_token(json!(SEARCH_RULES), VALID_KEY, Some(exp)).unwrap(); + 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) + &token, + &DecodingKey::from_secret(VALID_KEY.as_ref()), + &Validation::new(Algorithm::HS256), ); assert!(decoded.is_ok()); @@ -95,33 +116,63 @@ mod tests { #[test] fn test_generate_token_with_expires_at_in_the_past() { + let api_key_uid = "76cf8b87-fd12-4688-ad34-260d930ca4f4".to_string(); let exp = OffsetDateTime::now_utc() - time::Duration::HOUR; - let token = generate_tenant_token(json!(SEARCH_RULES), VALID_KEY, Some(exp)); + 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 token = generate_tenant_token(json!(SEARCH_RULES), VALID_KEY, None).unwrap(); + let api_key_uid = "76cf8b87-fd12-4688-ad34-260d930ca4f4".to_string(); + 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"); + &token, + &DecodingKey::from_secret(VALID_KEY.as_ref()), + &build_validation(), + ) + .expect("Cannot decode the token"); - assert_eq!(decoded.claims.api_key_prefix, &VALID_KEY[..8]); + 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 = "76cf8b87-fd12-4688-ad34-260d930ca4f4".to_string(); let key = "Ëa1ทt9bVcL-vãUทtP3OpXW5qPc%bWH5ทvw09"; - let token = generate_tenant_token(json!(SEARCH_RULES), key, None).unwrap(); + 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"); + &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_formated_uid() { + let api_key_uid = "xxx".to_string(); + let key = "Ëa1ทt9bVcL-vãUทtP3OpXW5qPc%bWH5ทvw09"; + let token = generate_tenant_token(api_key_uid.clone(), json!(SEARCH_RULES), key, None); - assert_eq!(decoded.claims.api_key_prefix, "Ëa1ทt9bV"); + assert!(token.is_err()); + } + + #[test] + fn test_generate_token_with_wrong_uid_version() { + let api_key_uid = "6a11eb96-2485-11ed-861d-0242ac120002".to_string(); + let key = "Ëa1ทt9bVcL-vãUทtP3OpXW5qPc%bWH5ทvw09"; + let token = generate_tenant_token(api_key_uid.clone(), json!(SEARCH_RULES), key, None); + + assert!(token.is_err()); } } diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 00000000..fd7b3d9d --- /dev/null +++ b/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(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 = std::time::Duration::from_millis(10); + let now = time::Instant::now(); + + async_sleep(sleep_duration).await; + + assert!(now.elapsed() >= sleep_duration); + } +}