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}