use async_trait::async_trait;
use serde::{de::DeserializeOwned, Deserialize, Serialize};
/// Derive the [`Document`](crate::documents::Document) trait.
///
/// ## Field attribute
/// Use the `#[document(..)]` field attribute to generate the correct settings
/// for each field. The available parameters are:
/// - `primary_key` (can only be used once)
/// - `distinct` (can only be used once)
/// - `searchable`
/// - `displayed`
/// - `filterable`
/// - `sortable`
///
/// ## Index name
/// The name of the index will be the name of the struct converted to snake case.
///
/// ## Sample usage:
/// ```
/// use serde::{Serialize, Deserialize};
/// use meilisearch_sdk::documents::Document;
/// use meilisearch_sdk::settings::Settings;
/// use meilisearch_sdk::indexes::Index;
/// use meilisearch_sdk::client::Client;
///
/// #[derive(Serialize, Deserialize, Document)]
/// struct Movie {
/// #[document(primary_key)]
/// movie_id: u64,
/// #[document(displayed, searchable)]
/// title: String,
/// #[document(displayed)]
/// description: String,
/// #[document(filterable, sortable, displayed)]
/// release_date: String,
/// #[document(filterable, displayed)]
/// genres: Vec<String>,
/// }
///
/// async fn usage(client: Client) {
/// // Default settings with the distinct, searchable, displayed, filterable, and sortable fields set correctly.
/// let settings: Settings = Movie::generate_settings();
/// // Index created with the name `movie` and the primary key set to `movie_id`
/// let index: Index = Movie::generate_index(&client).await.unwrap();
/// }
/// ```
pub use meilisearch_index_setting_macro::Document;
use crate::settings::Settings;
use crate::tasks::Task;
use crate::{errors::Error, indexes::Index};
#[async_trait]
pub trait Document {
fn generate_settings() -> Settings;
async fn generate_index(client: &crate::client::Client) -> Result<Index, Task>;
}
#[derive(Debug, Clone, Deserialize)]
pub struct DocumentsResults<T> {
pub results: Vec<T>,
pub limit: u32,
pub offset: u32,
pub total: u32,
}
#[derive(Debug, Clone, Serialize)]
pub struct DocumentQuery<'a> {
#[serde(skip_serializing)]
pub index: &'a Index,
/// The fields that should appear in the documents. By default all of the fields are present.
#[serde(skip_serializing_if = "Option::is_none")]
pub fields: Option<Vec<&'a str>>,
}
impl<'a> DocumentQuery<'a> {
pub fn new(index: &Index) -> DocumentQuery {
DocumentQuery {
index,
fields: None,
}
}
/// Specify the fields to return in the document.
///
/// # Example
///
/// ```
/// # use meilisearch_sdk::{client::*, indexes::*, documents::*};
/// #
/// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700");
/// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey");
/// #
/// # let client = Client::new(MEILISEARCH_URL, MEILISEARCH_API_KEY);
/// let index = client.index("document_query_with_fields");
/// let mut document_query = DocumentQuery::new(&index);
///
/// document_query.with_fields(["title"]);
/// ```
pub fn with_fields(
&mut self,
fields: impl IntoIterator<Item = &'a str>,
) -> &mut DocumentQuery<'a> {
self.fields = Some(fields.into_iter().collect());
self
}
/// Execute the get document query.
///
/// # Example
///
/// ```
/// # use meilisearch_sdk::{client::*, indexes::*, documents::*};
/// # use serde::{Deserialize, Serialize};
/// #
/// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700");
/// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey");
/// #
/// # let client = Client::new(MEILISEARCH_URL, MEILISEARCH_API_KEY);
///
/// # futures::executor::block_on(async move {
/// #[derive(Debug, Serialize, Deserialize, PartialEq)]
/// struct MyObject {
/// id: String,
/// kind: String,
/// }
/// #[derive(Debug, Serialize, Deserialize, PartialEq)]
/// struct MyObjectReduced {
/// id: String,
/// }
///
/// # let index = client.index("document_query_execute");
/// # index.add_or_replace(&[MyObject{id:"1".to_string(), kind:String::from("a kind")},MyObject{id:"2".to_string(), kind:String::from("some kind")}], None).await.unwrap().wait_for_completion(&client, None, None).await.unwrap();
///
/// let document = DocumentQuery::new(&index).with_fields(["id"]).execute::<MyObjectReduced>("1").await.unwrap();
///
/// assert_eq!(
/// document,
/// MyObjectReduced { id: "1".to_string() }
/// );
/// # index.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap();
/// # });
pub async fn execute<T: DeserializeOwned + 'static>(
&self,
document_id: &str,
) -> Result<T, Error> {
self.index.get_document_with::<T>(document_id, self).await
}
}
#[derive(Debug, Clone, Serialize)]
pub struct DocumentsQuery<'a> {
#[serde(skip_serializing)]
pub index: &'a Index,
/// The number of documents to skip.
/// If the value of the parameter `offset` is `n`, the `n` first documents will not be returned.
/// This is helpful for pagination.
///
/// Example: If you want to skip the first document, set offset to `1`.
#[serde(skip_serializing_if = "Option::is_none")]
pub offset: Option<usize>,
/// The maximum number of documents returned.
/// If the value of the parameter `limit` is `n`, there will never be more than `n` documents in the response.
/// This is helpful for pagination.
///
/// Example: If you don't want to get more than two documents, set limit to `2`.
/// Default: `20`
#[serde(skip_serializing_if = "Option::is_none")]
pub limit: Option<usize>,
/// The fields that should appear in the documents. By default all of the fields are present.
#[serde(skip_serializing_if = "Option::is_none")]
pub fields: Option<Vec<&'a str>>,
}
impl<'a> DocumentsQuery<'a> {
pub fn new(index: &Index) -> DocumentsQuery {
DocumentsQuery {
index,
offset: None,
limit: None,
fields: None,
}
}
/// Specify the offset.
///
/// # Example
///
/// ```
/// # use meilisearch_sdk::{client::*, indexes::*, documents::*};
/// #
/// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700");
/// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey");
/// #
/// # let client = Client::new(MEILISEARCH_URL, MEILISEARCH_API_KEY);
/// let index = client.index("my_index");
///
/// let mut documents_query = DocumentsQuery::new(&index).with_offset(1);
/// ```
pub fn with_offset(&mut self, offset: usize) -> &mut DocumentsQuery<'a> {
self.offset = Some(offset);
self
}
/// Specify the limit.
///
/// # Example
///
/// ```
/// # use meilisearch_sdk::{client::*, indexes::*, documents::*};
/// #
/// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700");
/// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey");
/// #
/// # let client = Client::new(MEILISEARCH_URL, MEILISEARCH_API_KEY);
/// let index = client.index("my_index");
///
/// let mut documents_query = DocumentsQuery::new(&index);
///
/// documents_query.with_limit(1);
/// ```
pub fn with_limit(&mut self, limit: usize) -> &mut DocumentsQuery<'a> {
self.limit = Some(limit);
self
}
/// Specify the fields to return in the documents.
///
/// # Example
///
/// ```
/// # use meilisearch_sdk::{client::*, indexes::*, documents::*};
/// #
/// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700");
/// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey");
/// #
/// # let client = Client::new(MEILISEARCH_URL, MEILISEARCH_API_KEY);
/// let index = client.index("my_index");
///
/// let mut documents_query = DocumentsQuery::new(&index);
///
/// documents_query.with_fields(["title"]);
/// ```
pub fn with_fields(
&mut self,
fields: impl IntoIterator<Item = &'a str>,
) -> &mut DocumentsQuery<'a> {
self.fields = Some(fields.into_iter().collect());
self
}
/// Execute the get documents query.
///
/// # Example
///
/// ```
/// # use meilisearch_sdk::{client::*, indexes::*, documents::*};
/// # use serde::{Deserialize, Serialize};
/// #
/// # let MEILISEARCH_URL = option_env!("MEILISEARCH_URL").unwrap_or("http://localhost:7700");
/// # let MEILISEARCH_API_KEY = option_env!("MEILISEARCH_API_KEY").unwrap_or("masterKey");
/// #
/// # let client = Client::new(MEILISEARCH_URL, MEILISEARCH_API_KEY);
///
/// # futures::executor::block_on(async move {
/// # let index = client.create_index("documents_query_execute", None).await.unwrap().wait_for_completion(&client, None, None).await.unwrap().try_make_index(&client).unwrap();
/// #[derive(Debug, Serialize, Deserialize, PartialEq)]
/// struct MyObject {
/// id: Option<usize>,
/// kind: String,
/// }
/// let index = client.index("documents_query_execute");
///
/// let document = DocumentsQuery::new(&index)
/// .with_offset(1)
/// .execute::<MyObject>()
/// .await
/// .unwrap();
///
/// # index.delete().await.unwrap().wait_for_completion(&client, None, None).await.unwrap();
/// # });
/// ```
pub async fn execute<T: DeserializeOwned + 'static>(
&self,
) -> Result<DocumentsResults<T>, Error> {
self.index.get_documents_with::<T>(self).await
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{client::*, indexes::*};
use ::meilisearch_sdk::documents::Document;
use meilisearch_test_macro::meilisearch_test;
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize, PartialEq)]
struct MyObject {
id: Option<usize>,
kind: String,
}
#[allow(unused)]
#[derive(Document)]
struct MovieClips {
#[document(primary_key)]
movie_id: u64,
#[document(distinct)]
owner: String,
#[document(displayed, searchable)]
title: String,
#[document(displayed)]
description: String,
#[document(filterable, sortable, displayed)]
release_date: String,
#[document(filterable, displayed)]
genres: Vec<String>,
}
#[allow(unused)]
#[derive(Document)]
struct VideoClips {
video_id: u64,
}
async fn setup_test_index(client: &Client, index: &Index) -> Result<(), Error> {
let t0 = index
.add_documents(
&[
MyObject {
id: Some(0),
kind: "text".into(),
},
MyObject {
id: Some(1),
kind: "text".into(),
},
MyObject {
id: Some(2),
kind: "title".into(),
},
MyObject {
id: Some(3),
kind: "title".into(),
},
],
None,
)
.await?;
t0.wait_for_completion(client, None, None).await?;
Ok(())
}
#[meilisearch_test]
async fn test_get_documents_with_execute(client: Client, index: Index) -> Result<(), Error> {
setup_test_index(&client, &index).await?;
// let documents = index.get_documents(None, None, None).await.unwrap();
let documents = DocumentsQuery::new(&index)
.with_limit(1)
.with_offset(1)
.with_fields(["kind"])
.execute::<MyObject>()
.await
.unwrap();
assert_eq!(documents.limit, 1);
assert_eq!(documents.offset, 1);
assert_eq!(documents.results.len(), 1);
Ok(())
}
#[meilisearch_test]
async fn test_get_documents_with_only_one_param(
client: Client,
index: Index,
) -> Result<(), Error> {
setup_test_index(&client, &index).await?;
// let documents = index.get_documents(None, None, None).await.unwrap();
let documents = DocumentsQuery::new(&index)
.with_limit(1)
.execute::<MyObject>()
.await
.unwrap();
assert_eq!(documents.limit, 1);
assert_eq!(documents.offset, 0);
assert_eq!(documents.results.len(), 1);
Ok(())
}
#[meilisearch_test]
async fn test_settings_generated_by_macro(client: Client, index: Index) -> Result<(), Error> {
setup_test_index(&client, &index).await?;
let movie_settings: Settings = MovieClips::generate_settings();
let video_settings: Settings = VideoClips::generate_settings();
assert_eq!(movie_settings.searchable_attributes.unwrap(), ["title"]);
assert!(video_settings.searchable_attributes.unwrap().is_empty());
assert_eq!(
movie_settings.displayed_attributes.unwrap(),
["title", "description", "release_date", "genres"]
);
assert!(video_settings.displayed_attributes.unwrap().is_empty());
assert_eq!(
movie_settings.filterable_attributes.unwrap(),
["release_date", "genres"]
);
assert!(video_settings.filterable_attributes.unwrap().is_empty());
assert_eq!(
movie_settings.sortable_attributes.unwrap(),
["release_date"]
);
assert!(video_settings.sortable_attributes.unwrap().is_empty());
Ok(())
}
#[meilisearch_test]
async fn test_generate_index(client: Client) -> Result<(), Error> {
let index: Index = MovieClips::generate_index(&client).await.unwrap();
assert_eq!(index.uid, "movie_clips");
index
.delete()
.await?
.wait_for_completion(&client, None, None)
.await?;
Ok(())
}
#[derive(Serialize, Deserialize, Document)]
struct Movie {
#[document(primary_key)]
movie_id: u64,
#[document(displayed, searchable)]
title: String,
#[document(displayed)]
description: String,
#[document(filterable, sortable, displayed)]
release_date: String,
#[document(filterable, displayed)]
genres: Vec<String>,
}
}