meilisearch/meilisearch-rust

Consider using macros

Closed this issue ยท 4 comments

Macros are a wonderful feature of the Rust language that could help us for 2 things :

Add an abstraction over the Document trait

We currently use a Document trait to make structures of users compatible with MeiliSearch.
It works great but has some unpleasant boilerplate code.

Examples

Currently

#[derive(Debug, Serialize, Deserialize)]
pub struct Crate {
    name: String,
    downloads: Option<usize>,
    description: String,
    keywords: Vec<String>,
    categories: Vec<String>,
    readme: String,
    version: String,
}

// Implement the Document trait so that we can use our struct with MeiliSearch
impl Document for Crate {
    type UIDType = String;

    fn get_uid(&self) -> &Self::UIDType {
        &self.name
    }
}

With macros

#[derive(Debug, Serialize, Deserialize, MeiliSearchDocument)]
pub struct Crate {
    #[primary_key]
    name: String,
    downloads: Option<usize>,
    description: String,
    keywords: Vec<String>,
    categories: Vec<String>,
    readme: String,
    version: String,
}

Simplify our tests

There are currently issues about tests in this library. They are many but they are not standardized enough. This is a pain point when it comes to maintain existing tests and adding new ones.
We could use macros to generate tests.

Examples

Currently (unit test)

    #[derive(Debug, Serialize, Deserialize, PartialEq)]
    struct Document {
        id: usize,
        value: String,
        kind: String,
    }

    impl document::Document for Document {
        type UIDType = usize;

        fn get_uid(&self) -> &Self::UIDType {
            &self.id
        }
    }

    #[allow(unused_must_use)]
    async fn setup_test_index<'a>(client: &'a Client<'a>, name: &'a str) -> Index<'a> {
        // try to delete
        client.delete_index(name).await;

        let index = client.create_index(name, None).await.unwrap();
        index.add_documents(&[
            Document { id: 0, kind: "text".into(), value: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.".to_string() },
            Document { id: 1, kind: "text".into(), value: "dolor sit amet, consectetur adipiscing elit".to_string() },
            Document { id: 2, kind: "title".into(), value: "The Social Network".to_string() },
            Document { id: 3, kind: "title".into(), value: "Harry Potter and the Sorcerer's Stone".to_string() },
            Document { id: 4, kind: "title".into(), value: "Harry Potter and the Chamber of Secrets".to_string() },
            Document { id: 5, kind: "title".into(), value: "Harry Potter and the Prisoner of Azkaban".to_string() },
            Document { id: 6, kind: "title".into(), value: "Harry Potter and the Goblet of Fire".to_string() },
            Document { id: 7, kind: "title".into(), value: "Harry Potter and the Order of the Phoenix".to_string() },
            Document { id: 8, kind: "title".into(), value: "Harry Potter and the Half-Blood Prince".to_string() },
            Document { id: 9, kind: "title".into(), value: "Harry Potter and the Deathly Hallows".to_string() },
        ], None).await.unwrap();
        index.set_attributes_for_faceting(["kind"]).await.unwrap();
        sleep(Duration::from_secs(1));
        index
    }

    #[async_test]
    async fn test_query_string() {
        let client = Client::new("http://localhost:7700", "masterKey");
        let index = setup_test_index(&client, "test_query_string").await;

        let results: SearchResults<Document> =
            index.search().with_query("dolor").execute().await.unwrap();
        assert_eq!(results.hits.len(), 2);

        client.delete_index("test_query_string").await.unwrap();
    }

With macros (unit test)

    #[meilisearch_test]
    async fn test_query_string() {
        let results: SearchResults<Document> =
            index.search().with_query("dolor").execute().await.unwrap();
        assert_eq!(results.hits.len(), 2);
    }

Currently (doc test)
    /// ```
    /// # use meilisearch_sdk::{client::*, indexes::*};
    /// # futures::executor::block_on(async move {
    /// // create the client
    /// let client = Client::new("http://localhost:7700", "masterKey");
    ///
    /// let indexes: Vec<Index> = client.list_all_indexes().await.unwrap();
    /// println!("{:?}", indexes);
    /// # });
    /// ```

With macros (doc test)
    /// ```
    /// # #[meilisearch_doctest]
    /// # fn test() {
    /// let indexes: Vec<Index> = client.list_all_indexes().await.unwrap();
    /// println!("{:?}", indexes);
    /// # }
    /// ```

The problem with macros

Rust does not allow us to define macros in a regular crate. We would need to create a new crate that would contain macros. That could cause extra maintenance work in the future, but it should not be necessary to update macros often.

Thanks for this issue @Mubelotix!

Anyone using this crate: feel free to give your opinion and what you prefer here ๐Ÿ˜

For the structs I don't have much of an opinion either way as both options are OK for me. For the tests however I agree with @Mubelotix, if macros could be used to simplify adding and maintaining them that it would be really nice.

Given the new version of the rust sdk, the Document trait is no longer needed. How would that change affect this?

Hey, I'm closing this issue as it is stale and missing information. Feel free to re-open it if it is still relevant!