meilisearch_index_setting_macro/
lib.rs

1use convert_case::{Case, Casing};
2use proc_macro2::Ident;
3use quote::quote;
4use structmeta::{Flag, StructMeta};
5use syn::{parse_macro_input, spanned::Spanned};
6
7#[derive(Clone, StructMeta, Default)]
8struct FieldAttrs {
9    primary_key: Flag,
10    displayed: Flag,
11    searchable: Flag,
12    distinct: Flag,
13    filterable: Flag,
14    sortable: Flag,
15}
16
17#[proc_macro_derive(IndexConfig, attributes(index_config))]
18pub fn generate_index_settings(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
19    let ast = parse_macro_input!(input as syn::DeriveInput);
20
21    let fields: &syn::Fields = match ast.data {
22        syn::Data::Struct(ref data) => &data.fields,
23        _ => {
24            return proc_macro::TokenStream::from(
25                syn::Error::new(ast.ident.span(), "Applicable only to struct").to_compile_error(),
26            );
27        }
28    };
29
30    let struct_ident = &ast.ident;
31
32    let index_config_implementation = get_index_config_implementation(struct_ident, fields);
33    proc_macro::TokenStream::from(quote! {
34        #index_config_implementation
35    })
36}
37
38fn get_index_config_implementation(
39    struct_ident: &Ident,
40    fields: &syn::Fields,
41) -> proc_macro2::TokenStream {
42    let mut primary_key_attribute = String::new();
43    let mut distinct_key_attribute = String::new();
44    let mut displayed_attributes = vec![];
45    let mut searchable_attributes = vec![];
46    let mut filterable_attributes = vec![];
47    let mut sortable_attributes = vec![];
48
49    let index_name = struct_ident
50        .to_string()
51        .from_case(Case::UpperCamel)
52        .to_case(Case::Snake);
53
54    let mut primary_key_found = false;
55    let mut distinct_found = false;
56
57    for field in fields {
58        let attrs = field
59            .attrs
60            .iter()
61            .filter(|attr| attr.path().is_ident("index_config"))
62            .map(|attr| attr.parse_args::<FieldAttrs>().unwrap())
63            .collect::<Vec<_>>()
64            .first()
65            .cloned()
66            .unwrap_or_default();
67
68        // Check if the primary key field is unique
69        if attrs.primary_key.value() {
70            if primary_key_found {
71                return syn::Error::new(
72                    field.span(),
73                    "Only one field can be marked as primary key",
74                )
75                .to_compile_error();
76            }
77            primary_key_attribute = field.ident.clone().unwrap().to_string();
78            primary_key_found = true;
79        }
80
81        // Check if the distinct field is unique
82        if attrs.distinct.value() {
83            if distinct_found {
84                return syn::Error::new(field.span(), "Only one field can be marked as distinct")
85                    .to_compile_error();
86            }
87            distinct_key_attribute = field.ident.clone().unwrap().to_string();
88            distinct_found = true;
89        }
90
91        if attrs.displayed.value() {
92            displayed_attributes.push(field.ident.clone().unwrap().to_string());
93        }
94
95        if attrs.searchable.value() {
96            searchable_attributes.push(field.ident.clone().unwrap().to_string());
97        }
98
99        if attrs.filterable.value() {
100            filterable_attributes.push(field.ident.clone().unwrap().to_string());
101        }
102
103        if attrs.sortable.value() {
104            sortable_attributes.push(field.ident.clone().unwrap().to_string());
105        }
106    }
107
108    let primary_key_token: proc_macro2::TokenStream = if primary_key_attribute.is_empty() {
109        quote! {
110            ::std::option::Option::None
111        }
112    } else {
113        quote! {
114            ::std::option::Option::Some(#primary_key_attribute)
115        }
116    };
117
118    let display_attr_tokens =
119        get_settings_token_for_list(&displayed_attributes, "with_displayed_attributes");
120    let sortable_attr_tokens =
121        get_settings_token_for_list(&sortable_attributes, "with_sortable_attributes");
122    let filterable_attr_tokens =
123        get_settings_token_for_list(&filterable_attributes, "with_filterable_attributes");
124    let searchable_attr_tokens =
125        get_settings_token_for_list(&searchable_attributes, "with_searchable_attributes");
126    let distinct_attr_token = get_settings_token_for_string_for_some_string(
127        &distinct_key_attribute,
128        "with_distinct_attribute",
129    );
130
131    quote! {
132        #[::meilisearch_sdk::macro_helper::async_trait(?Send)]
133        impl ::meilisearch_sdk::documents::IndexConfig for #struct_ident {
134            const INDEX_STR: &'static str = #index_name;
135
136            fn generate_settings() -> ::meilisearch_sdk::settings::Settings {
137            ::meilisearch_sdk::settings::Settings::new()
138            #display_attr_tokens
139            #sortable_attr_tokens
140            #filterable_attr_tokens
141            #searchable_attr_tokens
142            #distinct_attr_token
143        }
144
145         async fn generate_index<Http: ::meilisearch_sdk::request::HttpClient>(client: &::meilisearch_sdk::client::Client<Http>) -> std::result::Result<::meilisearch_sdk::indexes::Index<Http>, ::meilisearch_sdk::tasks::Task> {
146            return client.create_index(#index_name, #primary_key_token)
147                .await.unwrap()
148                .wait_for_completion(&client, ::std::option::Option::None, ::std::option::Option::None)
149                .await.unwrap()
150                .try_make_index(&client);
151            }
152        }
153    }
154}
155
156fn get_settings_token_for_list(
157    field_name_list: &[String],
158    method_name: &str,
159) -> proc_macro2::TokenStream {
160    let string_attributes = field_name_list.iter().map(|attr| {
161        quote! {
162            #attr
163        }
164    });
165    let method_ident = Ident::new(method_name, proc_macro2::Span::call_site());
166
167    if field_name_list.is_empty() {
168        quote! {
169            .#method_ident(::std::iter::empty::<&str>())
170        }
171    } else {
172        quote! {
173            .#method_ident([#(#string_attributes),*])
174        }
175    }
176}
177
178fn get_settings_token_for_string_for_some_string(
179    field_name: &String,
180    method_name: &str,
181) -> proc_macro2::TokenStream {
182    let method_ident = Ident::new(method_name, proc_macro2::Span::call_site());
183
184    if field_name.is_empty() {
185        proc_macro2::TokenStream::new()
186    } else {
187        quote! {
188            .#method_ident(::std::option::Option::Some(#field_name))
189        }
190    }
191}