meilisearch_sdk/
client.rs

1use serde::de::Error as SerdeError;
2use serde::{de::DeserializeOwned, Deserialize, Serialize};
3use serde_json::{json, Value};
4use std::{collections::HashMap, time::Duration};
5use time::OffsetDateTime;
6
7use crate::{
8    errors::*,
9    indexes::*,
10    key::{Key, KeyBuilder, KeyUpdater, KeysQuery, KeysResults},
11    request::*,
12    search::*,
13    task_info::TaskInfo,
14    tasks::{Task, TasksCancelQuery, TasksDeleteQuery, TasksResults, TasksSearchQuery},
15    utils::SleepBackend,
16    DefaultHttpClient,
17};
18
19/// The top-level struct of the SDK, representing a client containing [indexes](../indexes/struct.Index.html).
20#[derive(Debug, Clone)]
21pub struct Client<Http: HttpClient = DefaultHttpClient> {
22    pub(crate) host: String,
23    pub(crate) api_key: Option<String>,
24    pub(crate) http_client: Http,
25}
26
27#[derive(Debug, Clone, Serialize, Deserialize)]
28pub struct SwapIndexes {
29    pub indexes: (String, String),
30    #[serde(skip_serializing_if = "Option::is_none")]
31    pub rename: Option<bool>,
32}
33
34#[cfg(feature = "reqwest")]
35impl Client {
36    /// Create a client using the specified server.
37    ///
38    /// Don't put a '/' at the end of the host.
39    ///
40    /// In production mode, see [the documentation about authentication](https://www.meilisearch.com/docs/learn/security/master_api_keys#authentication).
41    ///
42    /// # Example
43    ///
44    /// ```
45    /// # use meilisearch_sdk::{client::*, indexes::*};
46    /// #
47    /// let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700");
48    /// let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey");
49    ///
50    /// let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap();
51    /// ```
52    pub fn new(
53        host: impl Into<String>,
54        api_key: Option<impl Into<String>>,
55    ) -> Result<Client, Error> {
56        let api_key = api_key.map(|key| key.into());
57        let http_client = crate::reqwest::ReqwestClient::new(api_key.as_deref())?;
58
59        Ok(Client {
60            host: host.into(),
61            api_key,
62            http_client,
63        })
64    }
65}
66
67impl<Http: HttpClient> Client<Http> {
68    // Create a client with a custom http client
69    pub fn new_with_client(
70        host: impl Into<String>,
71        api_key: Option<impl Into<String>>,
72        http_client: Http,
73    ) -> Client<Http> {
74        Client {
75            host: host.into(),
76            api_key: api_key.map(|key| key.into()),
77            http_client,
78        }
79    }
80
81    fn parse_indexes_results_from_value(
82        &self,
83        value: &Value,
84    ) -> Result<IndexesResults<Http>, Error> {
85        let raw_indexes = value["results"]
86            .as_array()
87            .ok_or_else(|| serde_json::Error::custom("Missing or invalid 'results' field"))
88            .map_err(Error::ParseError)?;
89
90        let limit = value["limit"]
91            .as_u64()
92            .ok_or_else(|| serde_json::Error::custom("Missing or invalid 'limit' field"))
93            .map_err(Error::ParseError)? as u32;
94
95        let offset = value["offset"]
96            .as_u64()
97            .ok_or_else(|| serde_json::Error::custom("Missing or invalid 'offset' field"))
98            .map_err(Error::ParseError)? as u32;
99
100        let total = value["total"]
101            .as_u64()
102            .ok_or_else(|| serde_json::Error::custom("Missing or invalid 'total' field"))
103            .map_err(Error::ParseError)? as u32;
104
105        let results = raw_indexes
106            .iter()
107            .map(|raw_index| Index::from_value(raw_index.clone(), self.clone()))
108            .collect::<Result<_, _>>()?;
109
110        let indexes_results = IndexesResults {
111            limit,
112            offset,
113            total,
114            results,
115        };
116
117        Ok(indexes_results)
118    }
119
120    pub async fn execute_multi_search_query<T: 'static + DeserializeOwned + Send + Sync>(
121        &self,
122        body: &MultiSearchQuery<'_, '_, Http>,
123    ) -> Result<MultiSearchResponse<T>, Error> {
124        self.http_client
125            .request::<(), &MultiSearchQuery<Http>, MultiSearchResponse<T>>(
126                &format!("{}/multi-search", &self.host),
127                Method::Post { body, query: () },
128                200,
129            )
130            .await
131    }
132
133    pub async fn execute_federated_multi_search_query<
134        T: 'static + DeserializeOwned + Send + Sync,
135    >(
136        &self,
137        body: &FederatedMultiSearchQuery<'_, '_, Http>,
138    ) -> Result<FederatedMultiSearchResponse<T>, Error> {
139        self.http_client
140            .request::<(), &FederatedMultiSearchQuery<Http>, FederatedMultiSearchResponse<T>>(
141                &format!("{}/multi-search", &self.host),
142                Method::Post { body, query: () },
143                200,
144            )
145            .await
146    }
147
148    /// Make multiple search requests.
149    ///
150    /// # Example
151    ///
152    /// ```
153    /// # use serde::{Serialize, Deserialize};
154    /// # use meilisearch_sdk::{client::*, indexes::*, search::*};
155    /// #
156    /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700");
157    /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey");
158    /// #
159    /// #[derive(Serialize, Deserialize, Debug)]
160    /// struct Movie {
161    ///     name: String,
162    ///     description: String,
163    /// }
164    ///
165    /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async {
166    /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap();
167    /// let mut movies = client.index("search");
168    /// # // add some documents
169    /// # movies.add_or_replace(&[Movie{name:String::from("Interstellar"), description:String::from("Interstellar chronicles the adventures of a group of explorers who make use of a newly discovered wormhole to surpass the limitations on human space travel and conquer the vast distances involved in an interstellar voyage.")},Movie{name:String::from("Unknown"), description:String::from("Unknown")}], Some("name")).await.unwrap().wait_for_completion(&client, None, None).await.unwrap();
170    ///
171    /// let search_query_1 = SearchQuery::new(&movies)
172    ///     .with_query("Interstellar")
173    ///     .build();
174    /// let search_query_2 = SearchQuery::new(&movies)
175    ///     .with_query("")
176    ///     .build();
177    ///
178    /// let response = client
179    ///     .multi_search()
180    ///     .with_search_query(search_query_1)
181    ///     .with_search_query(search_query_2)
182    ///     .execute::<Movie>()
183    ///     .await
184    ///     .unwrap();
185    ///
186    /// assert_eq!(response.results.len(), 2);
187    /// # movies.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap();
188    /// # });
189    /// ```
190    ///
191    /// # Federated Search
192    ///
193    /// You can use [`MultiSearchQuery::with_federation`] to perform a [federated
194    /// search][1] where results from different indexes are merged and returned as
195    /// one list.
196    ///
197    /// When executing a federated query, the type parameter `T` is less clear,
198    /// as the documents in the different indexes potentially have different
199    /// fields and you might have one Rust type per index. In most cases, you
200    /// either want to create an enum with one variant per index and `#[serde
201    /// (untagged)]` attribute, or if you need more control, just pass
202    /// `serde_json::Map<String, serde_json::Value>` and then deserialize that
203    /// into the appropriate target types later.
204    ///
205    /// [1]: https://www.meilisearch.com/docs/learn/multi_search/multi_search_vs_federated_search#what-is-federated-search
206    #[must_use]
207    pub fn multi_search(&self) -> MultiSearchQuery<'_, '_, Http> {
208        MultiSearchQuery::new(self)
209    }
210
211    /// Return the host associated with this index.
212    ///
213    /// # Example
214    ///
215    /// ```
216    /// # use meilisearch_sdk::{client::*};
217    /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey");
218    /// let client = Client::new("http://doggo.dog", Some(MEILISEARCH_API_KEY)).unwrap();
219    ///
220    /// assert_eq!(client.get_host(), "http://doggo.dog");
221    /// ```
222    #[must_use]
223    pub fn get_host(&self) -> &str {
224        &self.host
225    }
226
227    /// Return the api key associated with this index.
228    ///
229    /// # Example
230    ///
231    /// ```
232    /// # use meilisearch_sdk::{client::*};
233    /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700");
234    /// let client = Client::new(MEILISEARCH_URL, Some("doggo")).unwrap();
235    ///
236    /// assert_eq!(client.get_api_key(), Some("doggo"));
237    /// ```
238    #[must_use]
239    pub fn get_api_key(&self) -> Option<&str> {
240        self.api_key.as_deref()
241    }
242
243    /// List all [Indexes](Index) with query parameters and return values as instances of [Index].
244    ///
245    /// # Example
246    ///
247    /// ```
248    /// # use meilisearch_sdk::{client::*, indexes::*};
249    /// #
250    /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700");
251    /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey");
252    /// #
253    /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async {
254    /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap();
255    /// let indexes: IndexesResults = client.list_all_indexes().await.unwrap();
256    ///
257    /// let indexes: IndexesResults = client.list_all_indexes().await.unwrap();
258    /// println!("{:?}", indexes);
259    /// # });
260    /// ```
261    pub async fn list_all_indexes(&self) -> Result<IndexesResults<Http>, Error> {
262        let value = self.list_all_indexes_raw().await?;
263        let indexes_results = self.parse_indexes_results_from_value(&value)?;
264        Ok(indexes_results)
265    }
266
267    /// List all [Indexes](Index) and returns values as instances of [Index].
268    ///
269    /// # Example
270    ///
271    /// ```
272    /// # use meilisearch_sdk::{client::*, indexes::*};
273    /// #
274    /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700");
275    /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey");
276    /// #
277    /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async {
278    /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap();
279    /// let mut query = IndexesQuery::new(&client);
280    /// query.with_limit(1);
281    ///
282    /// let indexes: IndexesResults = client.list_all_indexes_with(&query).await.unwrap();
283    ///
284    /// assert_eq!(indexes.limit, 1);
285    /// # });
286    /// ```
287    pub async fn list_all_indexes_with(
288        &self,
289        indexes_query: &IndexesQuery<'_, Http>,
290    ) -> Result<IndexesResults<Http>, Error> {
291        let value = self.list_all_indexes_raw_with(indexes_query).await?;
292        let indexes_results = self.parse_indexes_results_from_value(&value)?;
293
294        Ok(indexes_results)
295    }
296
297    /// List all [Indexes](Index) and returns as Json.
298    ///
299    /// # Example
300    ///
301    /// ```
302    /// # use meilisearch_sdk::{client::*, indexes::*};
303    /// #
304    /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700");
305    /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey");
306    /// #
307    /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async {
308    /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap();
309    /// let json_indexes = client.list_all_indexes_raw().await.unwrap();
310    ///
311    /// println!("{:?}", json_indexes);
312    /// # });
313    /// ```
314    pub async fn list_all_indexes_raw(&self) -> Result<Value, Error> {
315        let json_indexes = self
316            .http_client
317            .request::<(), (), Value>(
318                &format!("{}/indexes", self.host),
319                Method::Get { query: () },
320                200,
321            )
322            .await?;
323
324        Ok(json_indexes)
325    }
326
327    /// List all [Indexes](Index) with query parameters and returns as Json.
328    ///
329    /// # Example
330    ///
331    /// ```
332    /// # use meilisearch_sdk::{client::*, indexes::*};
333    /// #
334    /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700");
335    /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey");
336    /// #
337    /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async {
338    /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap();
339    /// let mut query = IndexesQuery::new(&client);
340    /// query.with_limit(1);
341    ///
342    /// let json_indexes = client.list_all_indexes_raw_with(&query).await.unwrap();
343    ///
344    /// println!("{:?}", json_indexes);
345    /// # });
346    /// ```
347    pub async fn list_all_indexes_raw_with(
348        &self,
349        indexes_query: &IndexesQuery<'_, Http>,
350    ) -> Result<Value, Error> {
351        let json_indexes = self
352            .http_client
353            .request::<&IndexesQuery<Http>, (), Value>(
354                &format!("{}/indexes", self.host),
355                Method::Get {
356                    query: indexes_query,
357                },
358                200,
359            )
360            .await?;
361
362        Ok(json_indexes)
363    }
364
365    /// Get an [Index], this index should already exist.
366    ///
367    /// # Example
368    ///
369    /// ```
370    /// # use meilisearch_sdk::{client::*, indexes::*};
371    /// #
372    /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700");
373    /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey");
374    /// #
375    /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async {
376    /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap();
377    /// # let index = client.create_index("get_index", None).await.unwrap().wait_for_completion(&client, None, None).await.unwrap().try_make_index(&client).unwrap();
378    /// let index = client.get_index("get_index").await.unwrap();
379    ///
380    /// assert_eq!(index.as_ref(), "get_index");
381    /// # index.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap();
382    /// # });
383    /// ```
384    pub async fn get_index(&self, uid: impl AsRef<str>) -> Result<Index<Http>, Error> {
385        let mut idx = self.index(uid.as_ref());
386        idx.fetch_info().await?;
387        Ok(idx)
388    }
389
390    /// Get a raw JSON [Index], this index should already exist.
391    ///
392    /// If you use it directly from an [Index], you can use the method [`Index::fetch_info`], which is the equivalent method from an index.
393    ///
394    /// # Example
395    ///
396    /// ```
397    /// # use meilisearch_sdk::{client::*, indexes::*};
398    /// #
399    /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700");
400    /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey");
401    /// #
402    /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async {
403    /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap();
404    /// # let index = client.create_index("get_raw_index", None).await.unwrap().wait_for_completion(&client, None, None).await.unwrap().try_make_index(&client).unwrap();
405    /// let raw_index = client.get_raw_index("get_raw_index").await.unwrap();
406    ///
407    /// assert_eq!(raw_index.get("uid").unwrap().as_str().unwrap(), "get_raw_index");
408    /// # index.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap();
409    /// # });
410    /// ```
411    pub async fn get_raw_index(&self, uid: impl AsRef<str>) -> Result<Value, Error> {
412        self.http_client
413            .request::<(), (), Value>(
414                &format!("{}/indexes/{}", self.host, uid.as_ref()),
415                Method::Get { query: () },
416                200,
417            )
418            .await
419    }
420
421    /// Create a corresponding object of an [Index] without any check or doing an HTTP call.
422    pub fn index(&self, uid: impl Into<String>) -> Index<Http> {
423        Index::new(uid, self.clone())
424    }
425
426    /// Create an [Index].
427    ///
428    /// The second parameter will be used as the primary key of the new index.
429    /// If it is not specified, Meilisearch will **try** to infer the primary key.
430    ///
431    /// # Example
432    ///
433    /// ```
434    /// # use meilisearch_sdk::{client::*, indexes::*};
435    /// #
436    /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700");
437    /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey");
438    /// #
439    /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async {
440    /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap();
441    /// // Create a new index called movies and access it
442    /// let task = client.create_index("create_index", None).await.unwrap();
443    ///
444    /// // Wait for the task to complete
445    /// let task = task.wait_for_completion(&client, None, None).await.unwrap();
446    ///
447    /// // Try to get the inner index if the task succeeded
448    /// let index = task.try_make_index(&client).unwrap();
449    ///
450    /// assert_eq!(index.as_ref(), "create_index");
451    /// # index.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap();
452    /// # });
453    /// ```
454    pub async fn create_index(
455        &self,
456        uid: impl AsRef<str>,
457        primary_key: Option<&str>,
458    ) -> Result<TaskInfo, Error> {
459        self.http_client
460            .request::<(), Value, TaskInfo>(
461                &format!("{}/indexes", self.host),
462                Method::Post {
463                    query: (),
464                    body: json!({
465                        "uid": uid.as_ref(),
466                        "primaryKey": primary_key,
467                    }),
468                },
469                202,
470            )
471            .await
472    }
473
474    /// Delete an index from its UID.
475    ///
476    /// To delete an [Index], use the [`Index::delete`] method.
477    pub async fn delete_index(&self, uid: impl AsRef<str>) -> Result<TaskInfo, Error> {
478        self.http_client
479            .request::<(), (), TaskInfo>(
480                &format!("{}/indexes/{}", self.host, uid.as_ref()),
481                Method::Delete { query: () },
482                202,
483            )
484            .await
485    }
486
487    /// Alias for [`Client::list_all_indexes`].
488    pub async fn get_indexes(&self) -> Result<IndexesResults<Http>, Error> {
489        self.list_all_indexes().await
490    }
491
492    /// Alias for [`Client::list_all_indexes_with`].
493    pub async fn get_indexes_with(
494        &self,
495        indexes_query: &IndexesQuery<'_, Http>,
496    ) -> Result<IndexesResults<Http>, Error> {
497        self.list_all_indexes_with(indexes_query).await
498    }
499
500    /// Alias for [`Client::list_all_indexes_raw`].
501    pub async fn get_indexes_raw(&self) -> Result<Value, Error> {
502        self.list_all_indexes_raw().await
503    }
504
505    /// Alias for [`Client::list_all_indexes_raw_with`].
506    pub async fn get_indexes_raw_with(
507        &self,
508        indexes_query: &IndexesQuery<'_, Http>,
509    ) -> Result<Value, Error> {
510        self.list_all_indexes_raw_with(indexes_query).await
511    }
512
513    /// Swaps a list of two [Indexes](Index).
514    ///
515    /// # Example
516    ///
517    /// ```
518    /// # use meilisearch_sdk::{client::*, indexes::*};
519    /// #
520    /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700");
521    /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey");
522    /// #
523    /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async {
524    /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap();
525    /// let task_index_1 = client.create_index("swap_index_1", None).await.unwrap();
526    /// let task_index_2 = client.create_index("swap_index_2", None).await.unwrap();
527    ///
528    /// // Wait for the task to complete
529    /// task_index_2.wait_for_completion(&client, None, None).await.unwrap();
530    ///
531    /// let task = client
532    ///     .swap_indexes([&SwapIndexes {
533    ///         indexes: (
534    ///             "swap_index_1".to_string(),
535    ///             "swap_index_2".to_string(),
536    ///         ),
537    ///         rename: None,
538    ///     }])
539    ///     .await
540    ///     .unwrap();
541    ///
542    /// client.index("swap_index_1").delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap();
543    /// client.index("swap_index_2").delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap();
544    /// # });
545    /// ```
546    pub async fn swap_indexes(
547        &self,
548        indexes: impl IntoIterator<Item = &SwapIndexes>,
549    ) -> Result<TaskInfo, Error> {
550        self.http_client
551            .request::<(), Vec<&SwapIndexes>, TaskInfo>(
552                &format!("{}/swap-indexes", self.host),
553                Method::Post {
554                    query: (),
555                    body: indexes.into_iter().collect(),
556                },
557                202,
558            )
559            .await
560    }
561
562    /// Get stats of all [Indexes](Index).
563    ///
564    /// # Example
565    ///
566    /// ```
567    /// # use meilisearch_sdk::{client::*, indexes::*};
568    /// #
569    /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700");
570    /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey");
571    /// #
572    /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async {
573    /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap();
574    /// let stats = client.get_stats().await.unwrap();
575    /// # });
576    /// ```
577    pub async fn get_stats(&self) -> Result<ClientStats, Error> {
578        self.http_client
579            .request::<(), (), ClientStats>(
580                &format!("{}/stats", self.host),
581                Method::Get { query: () },
582                200,
583            )
584            .await
585    }
586
587    /// Get health of Meilisearch server.
588    ///
589    /// # Example
590    ///
591    /// ```
592    /// # use meilisearch_sdk::{client::*, errors::*};
593    /// #
594    /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700");
595    /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey");
596    /// #
597    /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async {
598    /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap();
599    /// let health = client.health().await.unwrap();
600    ///
601    /// assert_eq!(health.status, "available");
602    /// # });
603    /// ```
604    pub async fn health(&self) -> Result<Health, Error> {
605        self.http_client
606            .request::<(), (), Health>(
607                &format!("{}/health", self.host),
608                Method::Get { query: () },
609                200,
610            )
611            .await
612    }
613
614    /// Get health of Meilisearch server.
615    ///
616    /// # Example
617    ///
618    /// ```
619    /// # use meilisearch_sdk::client::*;
620    /// #
621    /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700");
622    /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey");
623    /// #
624    /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async {
625    /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap();
626    /// let health = client.is_healthy().await;
627    ///
628    /// assert_eq!(health, true);
629    /// # });
630    /// ```
631    pub async fn is_healthy(&self) -> bool {
632        if let Ok(health) = self.health().await {
633            health.status.as_str() == "available"
634        } else {
635            false
636        }
637    }
638
639    /// Get the API [Keys](Key) from Meilisearch with parameters.
640    ///
641    /// See [`Client::create_key`], [`Client::get_key`], and the [meilisearch documentation](https://www.meilisearch.com/docs/reference/api/keys#get-all-keys).
642    ///
643    /// # Example
644    ///
645    /// ```
646    /// # use meilisearch_sdk::{client::*, errors::Error, key::KeysQuery};
647    /// #
648    /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700");
649    /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey");
650    /// #
651    /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async {
652    /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap();
653    /// let mut query = KeysQuery::new();
654    /// query.with_limit(1);
655    ///
656    /// let keys = client.get_keys_with(&query).await.unwrap();
657    ///
658    /// assert_eq!(keys.results.len(), 1);
659    /// # });
660    /// ```
661    pub async fn get_keys_with(&self, keys_query: &KeysQuery) -> Result<KeysResults, Error> {
662        let keys = self
663            .http_client
664            .request::<&KeysQuery, (), KeysResults>(
665                &format!("{}/keys", self.host),
666                Method::Get { query: keys_query },
667                200,
668            )
669            .await?;
670
671        Ok(keys)
672    }
673
674    /// Get the API [Keys](Key) from Meilisearch.
675    ///
676    /// See [`Client::create_key`], [`Client::get_key`], and the [meilisearch documentation](https://www.meilisearch.com/docs/reference/api/keys#get-all-keys).
677    ///
678    /// # Example
679    ///
680    /// ```
681    /// # use meilisearch_sdk::{client::*, errors::Error, key::KeyBuilder};
682    /// #
683    /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700");
684    /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey");
685    /// #
686    /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async {
687    /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap();
688    /// let keys = client.get_keys().await.unwrap();
689    ///
690    /// assert_eq!(keys.limit, 20);
691    /// # });
692    /// ```
693    pub async fn get_keys(&self) -> Result<KeysResults, Error> {
694        let keys = self
695            .http_client
696            .request::<(), (), KeysResults>(
697                &format!("{}/keys", self.host),
698                Method::Get { query: () },
699                200,
700            )
701            .await?;
702
703        Ok(keys)
704    }
705
706    /// Get one API [Key] from Meilisearch.
707    ///
708    /// See also [`Client::create_key`], [`Client::get_keys`], and the [meilisearch documentation](https://www.meilisearch.com/docs/reference/api/keys#get-one-key).
709    ///
710    /// # Example
711    ///
712    /// ```
713    /// # use meilisearch_sdk::{client::*, errors::Error, key::KeyBuilder};
714    /// #
715    /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700");
716    /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey");
717    /// #
718    /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async {
719    /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap();
720    /// # let key = client.get_keys().await.unwrap().results.into_iter()
721    /// #    .find(|k| k.name.as_ref().map_or(false, |name| name.starts_with("Default Search API Key")))
722    /// #    .expect("No default search key");
723    /// let key = client.get_key(key).await.expect("Invalid key");
724    ///
725    /// assert_eq!(key.name, Some("Default Search API Key".to_string()));
726    /// # });
727    /// ```
728    pub async fn get_key(&self, key: impl AsRef<str>) -> Result<Key, Error> {
729        self.http_client
730            .request::<(), (), Key>(
731                &format!("{}/keys/{}", self.host, key.as_ref()),
732                Method::Get { query: () },
733                200,
734            )
735            .await
736    }
737
738    /// Delete an API [Key] from Meilisearch.
739    ///
740    /// See also [`Client::create_key`], [`Client::update_key`], [`Client::get_key`], and the [meilisearch documentation](https://www.meilisearch.com/docs/reference/api/keys#delete-a-key).
741    ///
742    /// # Example
743    ///
744    /// ```
745    /// # use meilisearch_sdk::{client::*, errors::Error, key::KeyBuilder};
746    /// #
747    /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700");
748    /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey");
749    /// #
750    /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async {
751    /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap();
752    /// let key = KeyBuilder::new();
753    /// let key = client.create_key(key).await.unwrap();
754    /// let inner_key = key.key.clone();
755    ///
756    /// client.delete_key(key).await.unwrap();
757    ///
758    /// let keys = client.get_keys().await.unwrap();
759    ///
760    /// assert!(keys.results.iter().all(|key| key.key != inner_key));
761    /// # });
762    /// ```
763    pub async fn delete_key(&self, key: impl AsRef<str>) -> Result<(), Error> {
764        self.http_client
765            .request::<(), (), ()>(
766                &format!("{}/keys/{}", self.host, key.as_ref()),
767                Method::Delete { query: () },
768                204,
769            )
770            .await
771    }
772
773    /// Create an API [Key] in Meilisearch.
774    ///
775    /// See also [`Client::update_key`], [`Client::delete_key`], [`Client::get_key`], and the [meilisearch documentation](https://www.meilisearch.com/docs/reference/api/keys#create-a-key).
776    ///
777    /// # Example
778    ///
779    /// ```
780    /// # use meilisearch_sdk::{client::*, errors::Error, key::*};
781    /// #
782    /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700");
783    /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey");
784    /// #
785    /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async {
786    /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap();
787    /// let name = "create_key".to_string();
788    /// let mut key = KeyBuilder::new();
789    /// key.with_name(&name);
790    ///
791    /// let key = client.create_key(key).await.unwrap();
792    ///
793    /// assert_eq!(key.name, Some(name));
794    /// # client.delete_key(key).await.unwrap();
795    /// # });
796    /// ```
797    pub async fn create_key(&self, key: impl AsRef<KeyBuilder>) -> Result<Key, Error> {
798        self.http_client
799            .request::<(), &KeyBuilder, Key>(
800                &format!("{}/keys", self.host),
801                Method::Post {
802                    query: (),
803                    body: key.as_ref(),
804                },
805                201,
806            )
807            .await
808    }
809
810    /// Update an API [Key] in Meilisearch.
811    ///
812    /// See also [`Client::create_key`], [`Client::delete_key`], [`Client::get_key`], and the [meilisearch documentation](https://www.meilisearch.com/docs/reference/api/keys#update-a-key).
813    ///
814    /// # Example
815    ///
816    /// ```
817    /// # use meilisearch_sdk::{client::*, errors::Error, key::*};
818    /// #
819    /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700");
820    /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey");
821    /// #
822    /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async {
823    /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap();
824    /// let new_key = KeyBuilder::new();
825    /// let mut new_key = client.create_key(new_key).await.unwrap();
826    /// let mut key_update = KeyUpdater::new(new_key);
827    ///
828    /// let name = "my name".to_string();
829    /// key_update.with_name(&name);
830    ///
831    /// let key = client.update_key(key_update).await.unwrap();
832    ///
833    /// assert_eq!(key.name, Some(name));
834    /// # client.delete_key(key).await.unwrap();
835    /// # });
836    /// ```
837    pub async fn update_key(&self, key: impl AsRef<KeyUpdater>) -> Result<Key, Error> {
838        self.http_client
839            .request::<(), &KeyUpdater, Key>(
840                &format!("{}/keys/{}", self.host, key.as_ref().key),
841                Method::Patch {
842                    body: key.as_ref(),
843                    query: (),
844                },
845                200,
846            )
847            .await
848    }
849
850    /// Get version of the Meilisearch server.
851    ///
852    /// # Example
853    ///
854    /// ```
855    /// # use meilisearch_sdk::client::*;
856    /// #
857    /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700");
858    /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey");
859    /// #
860    /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async {
861    /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap();
862    /// let version = client.get_version().await.unwrap();
863    /// # });
864    /// ```
865    pub async fn get_version(&self) -> Result<Version, Error> {
866        self.http_client
867            .request::<(), (), Version>(
868                &format!("{}/version", self.host),
869                Method::Get { query: () },
870                200,
871            )
872            .await
873    }
874
875    /// Wait until Meilisearch processes a [Task], and get its status.
876    ///
877    /// `interval` = The frequency at which the server should be polled. **Default = 50ms**
878    ///
879    /// `timeout` = The maximum time to wait for processing to complete. **Default = 5000ms**
880    ///
881    /// If the waited time exceeds `timeout` then an [`Error::Timeout`] will be returned.
882    ///
883    /// See also [`Index::wait_for_task`, `Task::wait_for_completion`, `TaskInfo::wait_for_completion`].
884    ///
885    /// # Example
886    ///
887    /// ```
888    /// # use meilisearch_sdk::{client::*, indexes::*, tasks::*};
889    /// # use serde::{Serialize, Deserialize};
890    /// #
891    /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700");
892    /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey");
893    /// #
894    /// #
895    /// # #[derive(Debug, Serialize, Deserialize, PartialEq)]
896    /// # struct Document {
897    /// #    id: usize,
898    /// #    value: String,
899    /// #    kind: String,
900    /// # }
901    /// #
902    /// #
903    /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async {
904    /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap();
905    /// let movies = client.index("movies_client_wait_for_task");
906    ///
907    /// let task = movies.add_documents(&[
908    ///     Document { id: 0, kind: "title".into(), value: "The Social Network".to_string() },
909    ///     Document { id: 1, kind: "title".into(), value: "Harry Potter and the Sorcerer's Stone".to_string() },
910    /// ], None).await.unwrap();
911    ///
912    /// let status = client.wait_for_task(task, None, None).await.unwrap();
913    ///
914    /// assert!(matches!(status, Task::Succeeded { .. }));
915    /// # movies.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap();
916    /// # });
917    /// ```
918    pub async fn wait_for_task(
919        &self,
920        task_id: impl AsRef<u32>,
921        interval: Option<Duration>,
922        timeout: Option<Duration>,
923    ) -> Result<Task, Error> {
924        let interval = interval.unwrap_or_else(|| Duration::from_millis(50));
925        let timeout = timeout.unwrap_or_else(|| Duration::from_millis(5000));
926
927        let mut elapsed_time = Duration::new(0, 0);
928        let mut task_result: Result<Task, Error>;
929
930        while timeout > elapsed_time {
931            task_result = self.get_task(&task_id).await;
932            match task_result {
933                Ok(status) => match status {
934                    Task::Failed { .. } | Task::Succeeded { .. } => {
935                        return self.get_task(task_id).await;
936                    }
937                    Task::Enqueued { .. } | Task::Processing { .. } => {
938                        elapsed_time += interval;
939                        self.sleep_backend().sleep(interval).await;
940                    }
941                },
942                Err(error) => return Err(error),
943            };
944        }
945
946        Err(Error::Timeout)
947    }
948
949    /// Get a task from the server given a task id.
950    ///
951    /// # Example
952    ///
953    /// ```
954    /// # use meilisearch_sdk::{client::*, tasks::*};
955    /// #
956    /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700");
957    /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey");
958    /// #
959    /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async {
960    /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap();
961    /// # let index = client.create_index("movies_get_task", None).await.unwrap().wait_for_completion(&client, None, None).await.unwrap().try_make_index(&client).unwrap();
962    /// let task = index.delete_all_documents().await.unwrap();
963    ///
964    /// let task = client.get_task(task).await.unwrap();
965    /// # index.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap();
966    /// # });
967    /// ```
968    pub async fn get_task(&self, task_id: impl AsRef<u32>) -> Result<Task, Error> {
969        self.http_client
970            .request::<(), (), Task>(
971                &format!("{}/tasks/{}", self.host, task_id.as_ref()),
972                Method::Get { query: () },
973                200,
974            )
975            .await
976    }
977
978    /// Get all tasks with query parameters from the server.
979    ///
980    /// # Example
981    ///
982    /// ```
983    /// # use meilisearch_sdk::{client::*, tasks::*};
984    /// #
985    /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700");
986    /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey");
987    /// #
988    /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async {
989    /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap();
990    /// let mut query = TasksSearchQuery::new(&client);
991    /// query.with_index_uids(["get_tasks_with"]);
992    ///
993    /// let tasks = client.get_tasks_with(&query).await.unwrap();
994    /// # });
995    /// ```
996    pub async fn get_tasks_with(
997        &self,
998        tasks_query: &TasksSearchQuery<'_, Http>,
999    ) -> Result<TasksResults, Error> {
1000        let tasks = self
1001            .http_client
1002            .request::<&TasksSearchQuery<Http>, (), TasksResults>(
1003                &format!("{}/tasks", self.host),
1004                Method::Get { query: tasks_query },
1005                200,
1006            )
1007            .await?;
1008
1009        Ok(tasks)
1010    }
1011
1012    /// Cancel tasks with filters [`TasksCancelQuery`].
1013    ///
1014    /// # Example
1015    ///
1016    /// ```
1017    /// # use meilisearch_sdk::{client::*, tasks::*};
1018    /// #
1019    /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700");
1020    /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey");
1021    /// #
1022    /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async {
1023    /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap();
1024    /// let mut query = TasksCancelQuery::new(&client);
1025    /// query.with_index_uids(["movies"]);
1026    ///
1027    /// let res = client.cancel_tasks_with(&query).await.unwrap();
1028    /// # });
1029    /// ```
1030    pub async fn cancel_tasks_with(
1031        &self,
1032        filters: &TasksCancelQuery<'_, Http>,
1033    ) -> Result<TaskInfo, Error> {
1034        let tasks = self
1035            .http_client
1036            .request::<&TasksCancelQuery<Http>, (), TaskInfo>(
1037                &format!("{}/tasks/cancel", self.host),
1038                Method::Post {
1039                    query: filters,
1040                    body: (),
1041                },
1042                200,
1043            )
1044            .await?;
1045
1046        Ok(tasks)
1047    }
1048
1049    /// Delete tasks with filters [`TasksDeleteQuery`].
1050    ///
1051    /// # Example
1052    ///
1053    /// ```
1054    /// # use meilisearch_sdk::{client::*, tasks::*};
1055    /// #
1056    /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700");
1057    /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey");
1058    /// #
1059    /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async {
1060    /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap();
1061    /// let mut query = TasksDeleteQuery::new(&client);
1062    /// query.with_index_uids(["movies"]);
1063    ///
1064    /// let res = client.delete_tasks_with(&query).await.unwrap();
1065    /// # });
1066    /// ```
1067    pub async fn delete_tasks_with(
1068        &self,
1069        filters: &TasksDeleteQuery<'_, Http>,
1070    ) -> Result<TaskInfo, Error> {
1071        let tasks = self
1072            .http_client
1073            .request::<&TasksDeleteQuery<Http>, (), TaskInfo>(
1074                &format!("{}/tasks", self.host),
1075                Method::Delete { query: filters },
1076                200,
1077            )
1078            .await?;
1079
1080        Ok(tasks)
1081    }
1082
1083    /// Get all tasks from the server.
1084    ///
1085    /// # Example
1086    ///
1087    /// ```
1088    /// # use meilisearch_sdk::{client::*, tasks::*};
1089    /// #
1090    /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700");
1091    /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey");
1092    /// #
1093    /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async {
1094    /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap();
1095    /// let tasks = client.get_tasks().await.unwrap();
1096    ///
1097    /// assert!(tasks.results.len() > 0);
1098    /// # });
1099    /// ```
1100    pub async fn get_tasks(&self) -> Result<TasksResults, Error> {
1101        let tasks = self
1102            .http_client
1103            .request::<(), (), TasksResults>(
1104                &format!("{}/tasks", self.host),
1105                Method::Get { query: () },
1106                200,
1107            )
1108            .await?;
1109
1110        Ok(tasks)
1111    }
1112
1113    /// Generates a new tenant token.
1114    ///
1115    /// # Example
1116    ///
1117    /// ```
1118    /// # use meilisearch_sdk::client::Client;
1119    /// #
1120    /// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700");
1121    /// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey");
1122    /// #
1123    /// # tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async {
1124    /// # let client = Client::new(MEILISEARCH_URL, Some(MEILISEARCH_API_KEY)).unwrap();
1125    /// let api_key_uid = "76cf8b87-fd12-4688-ad34-260d930ca4f4".to_string();
1126    /// let token = client.generate_tenant_token(api_key_uid, serde_json::json!(["*"]), None, None).unwrap();
1127    ///
1128    /// let client = Client::new(MEILISEARCH_URL, Some(token)).unwrap();
1129    /// # });
1130    /// ```
1131    #[cfg(not(target_arch = "wasm32"))]
1132    pub fn generate_tenant_token(
1133        &self,
1134        api_key_uid: String,
1135        search_rules: Value,
1136        api_key: Option<&str>,
1137        expires_at: Option<OffsetDateTime>,
1138    ) -> Result<String, Error> {
1139        let api_key = match self.get_api_key() {
1140            Some(key) => api_key.unwrap_or(key),
1141            None => {
1142                return Err(Error::CantUseWithoutApiKey(
1143                    "generate_tenant_token".to_string(),
1144                ))
1145            }
1146        };
1147
1148        crate::tenant_tokens::generate_tenant_token(api_key_uid, search_rules, api_key, expires_at)
1149    }
1150
1151    fn sleep_backend(&self) -> SleepBackend {
1152        SleepBackend::infer(self.http_client.is_tokio())
1153    }
1154}
1155
1156#[derive(Debug, Clone, Deserialize)]
1157#[serde(rename_all = "camelCase")]
1158pub struct ClientStats {
1159    /// Storage space claimed by Meilisearch and LMDB in bytes
1160    pub database_size: usize,
1161
1162    /// Storage space used by the database in bytes, excluding unused space claimed by LMDB
1163    pub used_database_size: usize,
1164
1165    /// When the last update was made to the database in the `RFC 3339` format
1166    #[serde(with = "time::serde::rfc3339::option")]
1167    pub last_update: Option<OffsetDateTime>,
1168
1169    /// The statistics for each index found in the database
1170    pub indexes: HashMap<String, IndexStats>,
1171}
1172
1173/// Health of the Meilisearch server.
1174///
1175/// # Example
1176///
1177/// ```
1178/// # use meilisearch_sdk::{client::*, indexes::*, errors::Error};
1179/// Health {
1180///     status: "available".to_string(),
1181/// };
1182/// ```
1183#[derive(Debug, Clone, Deserialize)]
1184pub struct Health {
1185    pub status: String,
1186}
1187
1188/// Version of a Meilisearch server.
1189///
1190/// # Example
1191///
1192/// ```
1193/// # use meilisearch_sdk::{client::*, indexes::*, errors::Error};
1194/// Version {
1195///     commit_sha: "b46889b5f0f2f8b91438a08a358ba8f05fc09fc1".to_string(),
1196///     commit_date: "2019-11-15T09:51:54.278247+00:00".to_string(),
1197///     pkg_version: "0.1.1".to_string(),
1198/// };
1199/// ```
1200#[derive(Debug, Clone, Deserialize)]
1201#[serde(rename_all = "camelCase")]
1202pub struct Version {
1203    pub commit_sha: String,
1204    pub commit_date: String,
1205    pub pkg_version: String,
1206}
1207
1208#[cfg(test)]
1209mod tests {
1210    use big_s::S;
1211    use time::OffsetDateTime;
1212
1213    use meilisearch_test_macro::meilisearch_test;
1214
1215    use crate::{client::*, key::Action, reqwest::qualified_version};
1216
1217    #[derive(Debug, Serialize, Deserialize, PartialEq)]
1218    struct Document {
1219        id: String,
1220    }
1221
1222    #[meilisearch_test]
1223    async fn test_swapping_two_indexes(client: Client) {
1224        let index_1 = client.index("test_swapping_two_indexes_1");
1225        let index_2 = client.index("test_swapping_two_indexes_2");
1226
1227        let t0 = index_1
1228            .add_documents(
1229                &[Document {
1230                    id: "1".to_string(),
1231                }],
1232                None,
1233            )
1234            .await
1235            .unwrap();
1236
1237        index_2
1238            .add_documents(
1239                &[Document {
1240                    id: "2".to_string(),
1241                }],
1242                None,
1243            )
1244            .await
1245            .unwrap();
1246
1247        t0.wait_for_completion(&client, None, None).await.unwrap();
1248
1249        let task = client
1250            .swap_indexes([&SwapIndexes {
1251                indexes: (
1252                    "test_swapping_two_indexes_1".to_string(),
1253                    "test_swapping_two_indexes_2".to_string(),
1254                ),
1255                rename: None,
1256            }])
1257            .await
1258            .unwrap();
1259        task.wait_for_completion(&client, None, None).await.unwrap();
1260
1261        let document = index_1.get_document("2").await.unwrap();
1262
1263        assert_eq!(
1264            Document {
1265                id: "2".to_string()
1266            },
1267            document
1268        );
1269    }
1270
1271    #[meilisearch_test]
1272    async fn test_methods_has_qualified_version_as_header() {
1273        let mut s = mockito::Server::new_async().await;
1274        let mock_server_url = s.url();
1275        let path = "/hello";
1276        let address = &format!("{mock_server_url}{path}");
1277        let user_agent = &*qualified_version();
1278        let client = Client::new(mock_server_url, None::<String>).unwrap();
1279
1280        let assertions = vec![
1281            (
1282                s.mock("GET", path)
1283                    .match_header("User-Agent", user_agent)
1284                    .create_async()
1285                    .await,
1286                client
1287                    .http_client
1288                    .request::<(), (), ()>(address, Method::Get { query: () }, 200),
1289            ),
1290            (
1291                s.mock("POST", path)
1292                    .match_header("User-Agent", user_agent)
1293                    .create_async()
1294                    .await,
1295                client.http_client.request::<(), (), ()>(
1296                    address,
1297                    Method::Post {
1298                        query: (),
1299                        body: {},
1300                    },
1301                    200,
1302                ),
1303            ),
1304            (
1305                s.mock("DELETE", path)
1306                    .match_header("User-Agent", user_agent)
1307                    .create_async()
1308                    .await,
1309                client.http_client.request::<(), (), ()>(
1310                    address,
1311                    Method::Delete { query: () },
1312                    200,
1313                ),
1314            ),
1315            (
1316                s.mock("PUT", path)
1317                    .match_header("User-Agent", user_agent)
1318                    .create_async()
1319                    .await,
1320                client.http_client.request::<(), (), ()>(
1321                    address,
1322                    Method::Put {
1323                        query: (),
1324                        body: (),
1325                    },
1326                    200,
1327                ),
1328            ),
1329            (
1330                s.mock("PATCH", path)
1331                    .match_header("User-Agent", user_agent)
1332                    .create_async()
1333                    .await,
1334                client.http_client.request::<(), (), ()>(
1335                    address,
1336                    Method::Patch {
1337                        query: (),
1338                        body: (),
1339                    },
1340                    200,
1341                ),
1342            ),
1343        ];
1344
1345        for (m, req) in assertions {
1346            let _ = req.await;
1347
1348            m.assert_async().await;
1349        }
1350    }
1351
1352    #[meilisearch_test]
1353    async fn test_get_tasks(client: Client) {
1354        let tasks = client.get_tasks().await.unwrap();
1355        assert_eq!(tasks.limit, 20);
1356    }
1357
1358    #[meilisearch_test]
1359    async fn test_rename_index_via_swap(client: Client, name: String) -> Result<(), Error> {
1360        let from = format!("{name}_from");
1361        let to = format!("{name}_to");
1362
1363        client
1364            .create_index(&from, None)
1365            .await?
1366            .wait_for_completion(&client, None, None)
1367            .await?;
1368
1369        let task = client
1370            .swap_indexes([&SwapIndexes {
1371                indexes: (from.clone(), to.clone()),
1372                rename: Some(true),
1373            }])
1374            .await?;
1375        task.wait_for_completion(&client, None, None).await?;
1376
1377        let new_index = client.get_index(&to).await?;
1378        assert_eq!(new_index.uid, to);
1379        // Optional: old uid should no longer resolve
1380        assert!(client.get_raw_index(&from).await.is_err());
1381
1382        new_index
1383            .delete()
1384            .await?
1385            .wait_for_completion(&client, None, None)
1386            .await?;
1387
1388        Ok(())
1389    }
1390
1391    #[meilisearch_test]
1392    async fn test_get_tasks_with_params(client: Client) {
1393        let query = TasksSearchQuery::new(&client);
1394        let tasks = client.get_tasks_with(&query).await.unwrap();
1395
1396        assert_eq!(tasks.limit, 20);
1397    }
1398
1399    #[meilisearch_test]
1400    async fn test_get_keys(client: Client) {
1401        let keys = client.get_keys().await.unwrap();
1402
1403        assert!(keys.results.len() >= 2);
1404    }
1405
1406    #[meilisearch_test]
1407    async fn test_delete_key(client: Client, name: String) {
1408        let mut key = KeyBuilder::new();
1409        key.with_name(&name);
1410        let key = client.create_key(key).await.unwrap();
1411
1412        client.delete_key(&key).await.unwrap();
1413        let keys = KeysQuery::new()
1414            .with_limit(10000)
1415            .execute(&client)
1416            .await
1417            .unwrap();
1418
1419        assert!(keys.results.iter().all(|k| k.key != key.key));
1420    }
1421
1422    #[meilisearch_test]
1423    async fn test_error_delete_key(client: Client, name: String) {
1424        // ==> accessing a key that does not exist
1425        let error = client.delete_key("invalid_key").await.unwrap_err();
1426        insta::assert_snapshot!(error, @"Meilisearch invalid_request: api_key_not_found: API key `invalid_key` not found.. https://docs.meilisearch.com/errors#api_key_not_found");
1427
1428        // ==> executing the action without enough right
1429        let mut key = KeyBuilder::new();
1430
1431        key.with_name(&name);
1432        let key = client.create_key(key).await.unwrap();
1433        let master_key = client.api_key.clone();
1434
1435        // create a new client with no right
1436        let client = Client::new(client.host, Some(key.key.clone())).unwrap();
1437        // with a wrong key
1438        let error = client.delete_key("invalid_key").await.unwrap_err();
1439        insta::assert_snapshot!(error, @"Meilisearch auth: invalid_api_key: The provided API key is invalid.. https://docs.meilisearch.com/errors#invalid_api_key");
1440        assert!(matches!(
1441            error,
1442            Error::Meilisearch(MeilisearchError {
1443                error_code: ErrorCode::InvalidApiKey,
1444                error_type: ErrorType::Auth,
1445                ..
1446            })
1447        ));
1448        // with a good key
1449        let error = client.delete_key(&key.key).await.unwrap_err();
1450        insta::assert_snapshot!(error, @"Meilisearch auth: invalid_api_key: The provided API key is invalid.. https://docs.meilisearch.com/errors#invalid_api_key");
1451        assert!(matches!(
1452            error,
1453            Error::Meilisearch(MeilisearchError {
1454                error_code: ErrorCode::InvalidApiKey,
1455                error_type: ErrorType::Auth,
1456                ..
1457            })
1458        ));
1459
1460        // cleanup
1461        let client = Client::new(client.host, master_key).unwrap();
1462        client.delete_key(key).await.unwrap();
1463    }
1464
1465    #[meilisearch_test]
1466    async fn test_create_key(client: Client, name: String) {
1467        let expires_at = OffsetDateTime::now_utc() + time::Duration::HOUR;
1468        let mut key = KeyBuilder::new();
1469        key.with_action(Action::DocumentsAdd)
1470            .with_name(&name)
1471            .with_expires_at(expires_at)
1472            .with_description("a description")
1473            .with_index("*");
1474        let key = client.create_key(key).await.unwrap();
1475
1476        assert_eq!(key.actions, vec![Action::DocumentsAdd]);
1477        assert_eq!(&key.name, &Some(name));
1478        // We can't compare the two timestamps directly because of some nanoseconds imprecision with the floats
1479        assert_eq!(
1480            key.expires_at.unwrap().unix_timestamp(),
1481            expires_at.unix_timestamp()
1482        );
1483        assert_eq!(key.indexes, vec![S("*")]);
1484
1485        client.delete_key(key).await.unwrap();
1486    }
1487
1488    #[meilisearch_test]
1489    async fn test_error_create_key(client: Client, name: String) {
1490        // ==> Invalid index name
1491        /* TODO: uncomment once meilisearch fix this bug: https://github.com/meilisearch/meilisearch/issues/2158
1492        let mut key = KeyBuilder::new();
1493        key.with_index("invalid index # / \\name with spaces");
1494        let error = client.create_key(key).await.unwrap_err();
1495
1496        assert!(matches!(
1497            error,
1498            Error::MeilisearchError {
1499                error_code: ErrorCode::InvalidApiKeyIndexes,
1500                error_type: ErrorType::InvalidRequest,
1501                ..
1502            }
1503        ));
1504        */
1505        // ==> executing the action without enough right
1506        let mut no_right_key = KeyBuilder::new();
1507        no_right_key.with_name(format!("{name}_1"));
1508        let no_right_key = client.create_key(no_right_key).await.unwrap();
1509
1510        // backup the master key for cleanup at the end of the test
1511        let master_client = client.clone();
1512        let client = Client::new(&master_client.host, Some(no_right_key.key.clone())).unwrap();
1513
1514        let mut key = KeyBuilder::new();
1515        key.with_name(format!("{name}_2"));
1516        let error = client.create_key(key).await.unwrap_err();
1517
1518        assert!(matches!(
1519            error,
1520            Error::Meilisearch(MeilisearchError {
1521                error_code: ErrorCode::InvalidApiKey,
1522                error_type: ErrorType::Auth,
1523                ..
1524            })
1525        ));
1526
1527        // cleanup
1528        master_client
1529            .delete_key(client.api_key.unwrap())
1530            .await
1531            .unwrap();
1532    }
1533
1534    #[meilisearch_test]
1535    async fn test_update_key(client: Client, description: String) {
1536        let mut key = KeyBuilder::new();
1537        key.with_name("test_update_key");
1538        let mut key = client.create_key(key).await.unwrap();
1539
1540        let name = S("new name");
1541        key.with_description(&description);
1542        key.with_name(&name);
1543
1544        let key = key.update(&client).await.unwrap();
1545
1546        assert_eq!(key.description, Some(description));
1547        assert_eq!(key.name, Some(name));
1548
1549        client.delete_key(key).await.unwrap();
1550    }
1551
1552    #[meilisearch_test]
1553    async fn test_get_index(client: Client, index_uid: String) -> Result<(), Error> {
1554        let task = client.create_index(&index_uid, None).await?;
1555        let index = client
1556            .wait_for_task(task, None, None)
1557            .await?
1558            .try_make_index(&client)
1559            .unwrap();
1560
1561        assert_eq!(index.uid, index_uid);
1562        index
1563            .delete()
1564            .await?
1565            .wait_for_completion(&client, None, None)
1566            .await?;
1567        Ok(())
1568    }
1569
1570    #[meilisearch_test]
1571    async fn test_error_create_index(client: Client, index: Index) -> Result<(), Error> {
1572        let error = client
1573            .create_index("Wrong index name", None)
1574            .await
1575            .unwrap_err();
1576
1577        assert!(matches!(
1578            error,
1579            Error::Meilisearch(MeilisearchError {
1580                error_code: ErrorCode::InvalidIndexUid,
1581                error_type: ErrorType::InvalidRequest,
1582                ..
1583            })
1584        ));
1585
1586        // we try to create an index with the same uid of an already existing index
1587        let error = client
1588            .create_index(&*index.uid, None)
1589            .await?
1590            .wait_for_completion(&client, None, None)
1591            .await?
1592            .unwrap_failure();
1593
1594        assert!(matches!(
1595            error,
1596            MeilisearchError {
1597                error_code: ErrorCode::IndexAlreadyExists,
1598                error_type: ErrorType::InvalidRequest,
1599                ..
1600            }
1601        ));
1602        Ok(())
1603    }
1604
1605    #[meilisearch_test]
1606    async fn test_list_all_indexes(client: Client) {
1607        let all_indexes = client.list_all_indexes().await.unwrap();
1608
1609        assert_eq!(all_indexes.limit, 20);
1610        assert_eq!(all_indexes.offset, 0);
1611    }
1612
1613    #[meilisearch_test]
1614    async fn test_list_all_indexes_with_params(client: Client) {
1615        let mut query = IndexesQuery::new(&client);
1616        query.with_limit(1);
1617        let all_indexes = client.list_all_indexes_with(&query).await.unwrap();
1618
1619        assert_eq!(all_indexes.limit, 1);
1620        assert_eq!(all_indexes.offset, 0);
1621    }
1622
1623    #[meilisearch_test]
1624    async fn test_list_all_indexes_raw(client: Client) {
1625        let all_indexes_raw = client.list_all_indexes_raw().await.unwrap();
1626
1627        assert_eq!(all_indexes_raw["limit"], json!(20));
1628        assert_eq!(all_indexes_raw["offset"], json!(0));
1629    }
1630
1631    #[meilisearch_test]
1632    async fn test_list_all_indexes_raw_with_params(client: Client) {
1633        let mut query = IndexesQuery::new(&client);
1634        query.with_limit(1);
1635        let all_indexes_raw = client.list_all_indexes_raw_with(&query).await.unwrap();
1636
1637        assert_eq!(all_indexes_raw["limit"], json!(1));
1638        assert_eq!(all_indexes_raw["offset"], json!(0));
1639    }
1640
1641    #[meilisearch_test]
1642    async fn test_get_primary_key_is_none(mut index: Index) {
1643        let primary_key = index.get_primary_key().await;
1644
1645        assert!(primary_key.is_ok());
1646        assert!(primary_key.unwrap().is_none());
1647    }
1648
1649    #[meilisearch_test]
1650    async fn test_get_primary_key(client: Client, index_uid: String) -> Result<(), Error> {
1651        let mut index = client
1652            .create_index(index_uid, Some("primary_key"))
1653            .await?
1654            .wait_for_completion(&client, None, None)
1655            .await?
1656            .try_make_index(&client)
1657            .unwrap();
1658
1659        let primary_key = index.get_primary_key().await;
1660        assert!(primary_key.is_ok());
1661        assert_eq!(primary_key?.unwrap(), "primary_key");
1662
1663        index
1664            .delete()
1665            .await?
1666            .wait_for_completion(&client, None, None)
1667            .await?;
1668
1669        Ok(())
1670    }
1671}