From f909af065ecfb71faefdb86186fb0e71cecbe204 Mon Sep 17 00:00:00 2001 From: Vladimir Donich Date: Sun, 1 Sep 2024 11:18:07 +0000 Subject: [PATCH 1/8] Add support for `#[index_config(index_name = "")]`; Cleanup code --- meilisearch-index-setting-macro/src/lib.rs | 76 +++++++++++++--------- 1 file changed, 44 insertions(+), 32 deletions(-) diff --git a/meilisearch-index-setting-macro/src/lib.rs b/meilisearch-index-setting-macro/src/lib.rs index 23d89f06..21e3fc61 100644 --- a/meilisearch-index-setting-macro/src/lib.rs +++ b/meilisearch-index-setting-macro/src/lib.rs @@ -1,8 +1,8 @@ use convert_case::{Case, Casing}; use proc_macro2::Ident; use quote::quote; -use structmeta::{Flag, StructMeta}; -use syn::{parse_macro_input, spanned::Spanned}; +use structmeta::{Flag, NameValue, StructMeta}; +use syn::{parse_macro_input, spanned::Spanned, Attribute, LitStr}; #[derive(Clone, StructMeta, Default)] struct FieldAttrs { @@ -14,30 +14,51 @@ struct FieldAttrs { sortable: Flag, } +#[derive(StructMeta)] +struct StructAttrs { + index_name: NameValue, +} + #[proc_macro_derive(IndexConfig, attributes(index_config))] pub fn generate_index_settings(input: proc_macro::TokenStream) -> proc_macro::TokenStream { - let ast = parse_macro_input!(input as syn::DeriveInput); + let syn::DeriveInput { + attrs, ident, data, .. + } = parse_macro_input!(input as syn::DeriveInput); - let fields: &syn::Fields = match ast.data { + let fields: &syn::Fields = match data { syn::Data::Struct(ref data) => &data.fields, _ => { return proc_macro::TokenStream::from( - syn::Error::new(ast.ident.span(), "Applicable only to struct").to_compile_error(), + syn::Error::new(ident.span(), "Applicable only to struct").to_compile_error(), ); } }; - let struct_ident = &ast.ident; + let struct_ident = &ident; - let index_config_implementation = get_index_config_implementation(struct_ident, fields); + let index_config_implementation = get_index_config_implementation(struct_ident, fields, attrs); proc_macro::TokenStream::from(quote! { #index_config_implementation }) } +fn filter_attrs(attrs: &[Attribute]) -> impl Iterator { + attrs + .iter() + .filter(|attr| attr.path().is_ident("index_config")) +} + +fn get_index_name(struct_ident: &Ident, struct_attrs: &[Attribute]) -> String { + filter_attrs(struct_attrs) + .find_map(|attr| attr.parse_args::().ok()) + .map(|attr| attr.index_name.value.value()) + .unwrap_or_else(|| struct_ident.to_string().to_case(Case::Snake)) +} + fn get_index_config_implementation( struct_ident: &Ident, fields: &syn::Fields, + attrs: Vec, ) -> proc_macro2::TokenStream { let mut primary_key_attribute = String::new(); let mut distinct_key_attribute = String::new(); @@ -46,23 +67,14 @@ fn get_index_config_implementation( let mut filterable_attributes = vec![]; let mut sortable_attributes = vec![]; - let index_name = struct_ident - .to_string() - .from_case(Case::UpperCamel) - .to_case(Case::Snake); + let index_name = get_index_name(struct_ident, &attrs); let mut primary_key_found = false; let mut distinct_found = false; for field in fields { - let attrs = field - .attrs - .iter() - .filter(|attr| attr.path().is_ident("index_config")) - .map(|attr| attr.parse_args::().unwrap()) - .collect::>() - .first() - .cloned() + let attrs = filter_attrs(&field.attrs) + .find_map(|attr| attr.parse_args::().ok()) .unwrap_or_default(); // Check if the primary key field is unique @@ -134,20 +146,20 @@ fn get_index_config_implementation( const INDEX_STR: &'static str = #index_name; fn generate_settings() -> ::meilisearch_sdk::settings::Settings { - ::meilisearch_sdk::settings::Settings::new() - #display_attr_tokens - #sortable_attr_tokens - #filterable_attr_tokens - #searchable_attr_tokens - #distinct_attr_token - } + ::meilisearch_sdk::settings::Settings::new() + #display_attr_tokens + #sortable_attr_tokens + #filterable_attr_tokens + #searchable_attr_tokens + #distinct_attr_token + } - async fn generate_index(client: &::meilisearch_sdk::client::Client) -> std::result::Result<::meilisearch_sdk::indexes::Index, ::meilisearch_sdk::tasks::Task> { - return client.create_index(#index_name, #primary_key_token) - .await.unwrap() - .wait_for_completion(&client, ::std::option::Option::None, ::std::option::Option::None) - .await.unwrap() - .try_make_index(&client); + async fn generate_index(client: &::meilisearch_sdk::client::Client) -> std::result::Result<::meilisearch_sdk::indexes::Index, ::meilisearch_sdk::tasks::Task> { + client.create_index(#index_name, #primary_key_token) + .await.unwrap() + .wait_for_completion(client, ::std::option::Option::None, ::std::option::Option::None) + .await.unwrap() + .try_make_index(client) } } } From 1240bba4bac9fd1aaa6c3c781edcdcbd46923f7d Mon Sep 17 00:00:00 2001 From: Vladimir Donich Date: Sun, 1 Sep 2024 11:50:20 +0000 Subject: [PATCH 2/8] Add docs for `meilisearch_index_setting_macro` --- meilisearch-index-setting-macro/src/lib.rs | 39 ++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/meilisearch-index-setting-macro/src/lib.rs b/meilisearch-index-setting-macro/src/lib.rs index 21e3fc61..64de1a67 100644 --- a/meilisearch-index-setting-macro/src/lib.rs +++ b/meilisearch-index-setting-macro/src/lib.rs @@ -1,3 +1,42 @@ +/*! +# Usage + +### Basic usage +```no_run +use meilisearch_index_setting_macro::IndexConfig; +use meilisearch_sdk::documents::IndexConfig as IndexConfigTrait; + +#[derive(IndexConfig)] +pub struct Products { + #[index_config(primary_key)] + pub id: i64, + #[index_config(searchable, filterable)] + pub name: String +} + +#[tokio::main] +async fn main() { + let client = ...; + let index = Products::generate_index(&client).await.unwrap(); + let _ = index.set_settings(&Products::generate_settings()).await.unwrap(); +} +``` + +### Overriding index name +```no_run +use meilisearch_index_setting_macro::IndexConfig; + +#[derive(IndexConfig)] +#[index_config(index_name = "unique_products")] +pub struct Products { + #[index_config(primary_key)] + pub id: i64, + #[index_config(searchable, filterable)] + pub name: String +} +``` +*/ + use convert_case::{Case, Casing}; use proc_macro2::Ident; use quote::quote; From 637d41f5e1c13bacc4bcce2ad82733e64ded8cd3 Mon Sep 17 00:00:00 2001 From: Vladimir Donich Date: Sun, 1 Sep 2024 12:49:00 +0000 Subject: [PATCH 3/8] Only accept valid index names --- meilisearch-index-setting-macro/src/lib.rs | 29 +++++++++++++++++++--- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/meilisearch-index-setting-macro/src/lib.rs b/meilisearch-index-setting-macro/src/lib.rs index 64de1a67..ca3dc086 100644 --- a/meilisearch-index-setting-macro/src/lib.rs +++ b/meilisearch-index-setting-macro/src/lib.rs @@ -39,7 +39,7 @@ pub struct Products { use convert_case::{Case, Casing}; use proc_macro2::Ident; -use quote::quote; +use quote::{quote, ToTokens}; use structmeta::{Flag, NameValue, StructMeta}; use syn::{parse_macro_input, spanned::Spanned, Attribute, LitStr}; @@ -58,6 +58,11 @@ struct StructAttrs { index_name: NameValue, } +fn is_valid_name(name: &str) -> bool { + name.chars() + .all(|c| matches!(c, 'a'..='z' | 'A'..='Z' | '0'..='9' | '-' | '_')) +} + #[proc_macro_derive(IndexConfig, attributes(index_config))] pub fn generate_index_settings(input: proc_macro::TokenStream) -> proc_macro::TokenStream { let syn::DeriveInput { @@ -87,11 +92,27 @@ fn filter_attrs(attrs: &[Attribute]) -> impl Iterator { .filter(|attr| attr.path().is_ident("index_config")) } -fn get_index_name(struct_ident: &Ident, struct_attrs: &[Attribute]) -> String { +fn get_index_name(struct_ident: &Ident, struct_attrs: &[Attribute]) -> proc_macro2::TokenStream { filter_attrs(struct_attrs) .find_map(|attr| attr.parse_args::().ok()) - .map(|attr| attr.index_name.value.value()) - .unwrap_or_else(|| struct_ident.to_string().to_case(Case::Snake)) + .map(|attr| { + let index_name = attr.index_name; + let span = index_name.name_span; + let name = index_name.value.value(); + + if is_valid_name(&name) { + name.to_token_stream() + } else { + // Throw an error instead of silently using struct name + syn::Error::new(span, "Invalid index name").to_compile_error() + } + }) + .unwrap_or_else(|| { + struct_ident + .to_string() + .to_case(Case::Snake) + .to_token_stream() + }) } fn get_index_config_implementation( From b43ea387e89a7eb7aad99ef4dab6d11051de538b Mon Sep 17 00:00:00 2001 From: Vladimir Donich Date: Sun, 1 Sep 2024 13:00:05 +0000 Subject: [PATCH 4/8] Add name validation for structs themselves --- meilisearch-index-setting-macro/src/lib.rs | 24 +++++++++++----------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/meilisearch-index-setting-macro/src/lib.rs b/meilisearch-index-setting-macro/src/lib.rs index ca3dc086..b738ede3 100644 --- a/meilisearch-index-setting-macro/src/lib.rs +++ b/meilisearch-index-setting-macro/src/lib.rs @@ -93,26 +93,26 @@ fn filter_attrs(attrs: &[Attribute]) -> impl Iterator { } fn get_index_name(struct_ident: &Ident, struct_attrs: &[Attribute]) -> proc_macro2::TokenStream { - filter_attrs(struct_attrs) + let (span, name) = filter_attrs(struct_attrs) .find_map(|attr| attr.parse_args::().ok()) .map(|attr| { let index_name = attr.index_name; let span = index_name.name_span; let name = index_name.value.value(); - if is_valid_name(&name) { - name.to_token_stream() - } else { - // Throw an error instead of silently using struct name - syn::Error::new(span, "Invalid index name").to_compile_error() - } + (span, name) }) .unwrap_or_else(|| { - struct_ident - .to_string() - .to_case(Case::Snake) - .to_token_stream() - }) + ( + struct_ident.span(), + struct_ident.to_string().to_case(Case::Snake), + ) + }); + + match is_valid_name(&name) { + true => name.to_token_stream(), + false => syn::Error::new(span, "Invalid index name").to_compile_error(), + } } fn get_index_config_implementation( From 5536cf3efc82fd4a5f5f79f8669968b1f75d7673 Mon Sep 17 00:00:00 2001 From: Vladimir Donich Date: Sun, 1 Sep 2024 13:34:27 +0000 Subject: [PATCH 5/8] Edit existing `IndexConfig` derive docs instead --- meilisearch-index-setting-macro/src/lib.rs | 39 ---------------------- src/documents.rs | 4 +++ 2 files changed, 4 insertions(+), 39 deletions(-) diff --git a/meilisearch-index-setting-macro/src/lib.rs b/meilisearch-index-setting-macro/src/lib.rs index b738ede3..c32da560 100644 --- a/meilisearch-index-setting-macro/src/lib.rs +++ b/meilisearch-index-setting-macro/src/lib.rs @@ -1,42 +1,3 @@ -/*! -# Usage - -### Basic usage -```no_run -use meilisearch_index_setting_macro::IndexConfig; -use meilisearch_sdk::documents::IndexConfig as IndexConfigTrait; - -#[derive(IndexConfig)] -pub struct Products { - #[index_config(primary_key)] - pub id: i64, - #[index_config(searchable, filterable)] - pub name: String -} - -#[tokio::main] -async fn main() { - let client = ...; - let index = Products::generate_index(&client).await.unwrap(); - let _ = index.set_settings(&Products::generate_settings()).await.unwrap(); -} -``` - -### Overriding index name -```no_run -use meilisearch_index_setting_macro::IndexConfig; - -#[derive(IndexConfig)] -#[index_config(index_name = "unique_products")] -pub struct Products { - #[index_config(primary_key)] - pub id: i64, - #[index_config(searchable, filterable)] - pub name: String -} -``` -*/ - use convert_case::{Case, Casing}; use proc_macro2::Ident; use quote::{quote, ToTokens}; diff --git a/src/documents.rs b/src/documents.rs index 17e2e610..5c579d8e 100644 --- a/src/documents.rs +++ b/src/documents.rs @@ -16,6 +16,10 @@ use serde::{de::DeserializeOwned, Deserialize, Serialize}; /// ## Index name /// The name of the index will be the name of the struct converted to snake case. /// +/// Or it can be overridden with `#[index_config(index_name = "new_name")]` at the struct attribute level. +/// +/// ⚠️ Struct and index names should follow the naming [guidelines](https://www.meilisearch.com/docs/learn/getting_started/indexes#index-uid) +/// /// ## Sample usage: /// ``` /// use serde::{Serialize, Deserialize}; From 561a0fd9ad5f5c40b1d8a8a495ef24197eeaa968 Mon Sep 17 00:00:00 2001 From: Vladimir Donich Date: Sun, 1 Sep 2024 17:15:53 +0000 Subject: [PATCH 6/8] [index-setting-macro] Prevent empty names inside index name override --- meilisearch-index-setting-macro/src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/meilisearch-index-setting-macro/src/lib.rs b/meilisearch-index-setting-macro/src/lib.rs index c32da560..95723775 100644 --- a/meilisearch-index-setting-macro/src/lib.rs +++ b/meilisearch-index-setting-macro/src/lib.rs @@ -22,6 +22,7 @@ struct StructAttrs { fn is_valid_name(name: &str) -> bool { name.chars() .all(|c| matches!(c, 'a'..='z' | 'A'..='Z' | '0'..='9' | '-' | '_')) + && !name.is_empty() } #[proc_macro_derive(IndexConfig, attributes(index_config))] From 462b5547ec27021560d3c246a0f72fddb59c5d77 Mon Sep 17 00:00:00 2001 From: Vladimir Donich Date: Sun, 1 Sep 2024 18:28:38 +0000 Subject: [PATCH 7/8] [index-setting-macro] Support for changing `pagination` settings --- meilisearch-index-setting-macro/src/lib.rs | 73 ++++++++++++++-------- 1 file changed, 47 insertions(+), 26 deletions(-) diff --git a/meilisearch-index-setting-macro/src/lib.rs b/meilisearch-index-setting-macro/src/lib.rs index 95723775..fc605e78 100644 --- a/meilisearch-index-setting-macro/src/lib.rs +++ b/meilisearch-index-setting-macro/src/lib.rs @@ -1,6 +1,6 @@ use convert_case::{Case, Casing}; use proc_macro2::Ident; -use quote::{quote, ToTokens}; +use quote::quote; use structmeta::{Flag, NameValue, StructMeta}; use syn::{parse_macro_input, spanned::Spanned, Attribute, LitStr}; @@ -16,7 +16,8 @@ struct FieldAttrs { #[derive(StructMeta)] struct StructAttrs { - index_name: NameValue, + index_name: Option>, + max_total_hits: Option>, } fn is_valid_name(name: &str) -> bool { @@ -54,29 +55,6 @@ fn filter_attrs(attrs: &[Attribute]) -> impl Iterator { .filter(|attr| attr.path().is_ident("index_config")) } -fn get_index_name(struct_ident: &Ident, struct_attrs: &[Attribute]) -> proc_macro2::TokenStream { - let (span, name) = filter_attrs(struct_attrs) - .find_map(|attr| attr.parse_args::().ok()) - .map(|attr| { - let index_name = attr.index_name; - let span = index_name.name_span; - let name = index_name.value.value(); - - (span, name) - }) - .unwrap_or_else(|| { - ( - struct_ident.span(), - struct_ident.to_string().to_case(Case::Snake), - ) - }); - - match is_valid_name(&name) { - true => name.to_token_stream(), - false => syn::Error::new(span, "Invalid index name").to_compile_error(), - } -} - fn get_index_config_implementation( struct_ident: &Ident, fields: &syn::Fields, @@ -89,7 +67,33 @@ fn get_index_config_implementation( let mut filterable_attributes = vec![]; let mut sortable_attributes = vec![]; - let index_name = get_index_name(struct_ident, &attrs); + let mut index_name_override = None; + + let mut max_total_hits = None; + + let struct_attrs = + filter_attrs(&attrs).filter_map(|attr| attr.parse_args::().ok()); + for struct_attr in struct_attrs { + if let Some(index_name_value) = struct_attr.index_name { + index_name_override = Some((index_name_value.value.value(), index_name_value.name_span)) + } + + if let Some(max_total_hits_value) = struct_attr.max_total_hits { + max_total_hits = Some(max_total_hits_value.value) + } + } + + let (index_name, span) = index_name_override.unwrap_or_else(|| { + ( + struct_ident.to_string().to_case(Case::Snake), + struct_ident.span(), + ) + }); + + if !is_valid_name(&index_name) { + return syn::Error::new(span, "Index must follow the naming guidelines.") + .to_compile_error(); + } let mut primary_key_found = false; let mut distinct_found = false; @@ -162,6 +166,8 @@ fn get_index_config_implementation( "with_distinct_attribute", ); + let pagination_token = get_pagination_token(&max_total_hits, "with_pagination"); + quote! { #[::meilisearch_sdk::macro_helper::async_trait(?Send)] impl ::meilisearch_sdk::documents::IndexConfig for #struct_ident { @@ -174,6 +180,7 @@ fn get_index_config_implementation( #filterable_attr_tokens #searchable_attr_tokens #distinct_attr_token + #pagination_token } async fn generate_index(client: &::meilisearch_sdk::client::Client) -> std::result::Result<::meilisearch_sdk::indexes::Index, ::meilisearch_sdk::tasks::Task> { @@ -187,6 +194,20 @@ fn get_index_config_implementation( } } +fn get_pagination_token( + max_hits: &Option, + method_name: &str, +) -> proc_macro2::TokenStream { + let method_ident = Ident::new(method_name, proc_macro2::Span::call_site()); + + match max_hits { + Some(value) => { + quote! { .#method_ident(::meilisearch_sdk::settings::PaginationSetting { max_total_hits: #value }) } + } + None => quote! {}, + } +} + fn get_settings_token_for_list( field_name_list: &[String], method_name: &str, From 400d6c593e533ad256e2bf4d99a7ea348cd41780 Mon Sep 17 00:00:00 2001 From: Vladimir Donich Date: Sun, 1 Sep 2024 18:29:04 +0000 Subject: [PATCH 8/8] Update documentation with new derive macro features --- src/documents.rs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/documents.rs b/src/documents.rs index 5c579d8e..ad757c7e 100644 --- a/src/documents.rs +++ b/src/documents.rs @@ -3,9 +3,19 @@ use serde::{de::DeserializeOwned, Deserialize, Serialize}; /// Derive the [`IndexConfig`](crate::documents::IndexConfig) trait. /// +/// ## Struct attribute +/// Use the `#[index_config(..)]` struct attribute to set general index settings. +/// +/// The available parameters are: +/// - `index_name = "new_name"` - Override index name +/// - `max_total_hits = 5_000` - [Set pagination settings](https://www.meilisearch.com/docs/reference/api/settings#update-pagination-settings) +/// - Value can be anything that returns usize. +/// /// ## Field attribute /// Use the `#[index_config(..)]` field attribute to generate the correct settings -/// for each field. The available parameters are: +/// for each field. +/// +/// The available parameters are: /// - `primary_key` (can only be used once) /// - `distinct` (can only be used once) /// - `searchable` @@ -16,7 +26,7 @@ use serde::{de::DeserializeOwned, Deserialize, Serialize}; /// ## Index name /// The name of the index will be the name of the struct converted to snake case. /// -/// Or it can be overridden with `#[index_config(index_name = "new_name")]` at the struct attribute level. +/// Or it can be overridden with `index_name` at the struct attribute level. /// /// ⚠️ Struct and index names should follow the naming [guidelines](https://www.meilisearch.com/docs/learn/getting_started/indexes#index-uid) ///