Skip to content

Commit 13da4de

Browse files
bors[bot]romilpunethaloiclec
authored
Merge #383
383: Fix path to `meilisearch_sdk` in Document derive proc macro r=bidoubiwa a=loiclec See #358 (review) for an explanation of the change. Additionally, a couple of references to `crate` were left in the proc macro, which were also changed to `::meilisearch_sdk`. Co-authored-by: Romil Punetha <[email protected]> Co-authored-by: Loïc Lecrenier <[email protected]>
2 parents 46d38e6 + d063095 commit 13da4de

File tree

5 files changed

+454
-4
lines changed

5 files changed

+454
-4
lines changed

Cargo.toml

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ authors = ["Mubelotix <[email protected]>"]
55
edition = "2018"
66
description = "Rust wrapper for the Meilisearch API. Meilisearch is a powerful, fast, open-source, easy to use and deploy search engine."
77
license = "MIT"
8-
readme = "README.md"
8+
readme = "README.md"
99
repository = "https://github.com/meilisearch/meilisearch-sdk"
1010

1111
[workspace]
@@ -20,13 +20,15 @@ serde_json = "1.0"
2020
time = { version = "0.3.7", features = ["serde-well-known", "formatting", "parsing"] }
2121
jsonwebtoken = { version = "8", default-features = false }
2222
yaup = "0.2.0"
23-
either = { version = "1.8.0" , features = ["serde"] }
23+
either = { version = "1.8.0", features = ["serde"] }
2424
thiserror = "1.0.37"
25+
meilisearch-index-setting-macro = { path = "meilisearch-index-setting-macro" }
26+
2527

2628
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
2729
futures = "0.3"
2830
isahc = { version = "1.0", features = ["http2", "text-decoding"], default_features = false }
29-
uuid = { version = "1.1.2", features = ["v4"] }
31+
uuid = { version = "1.1.2", features = ["v4"] }
3032

3133
[target.'cfg(target_arch = "wasm32")'.dependencies]
3234
js-sys = "0.3.47"
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
[package]
2+
name = "meilisearch-index-setting-macro"
3+
version = "0.1.0"
4+
edition = "2021"
5+
6+
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
7+
8+
[lib]
9+
proc-macro = true
10+
11+
[dependencies]
12+
syn = { version = "1.0.102", features = ["extra-traits"] }
13+
quote = "1.0.21"
14+
proc-macro2 = "1.0.46"
15+
convert_case = "0.6.0"
16+
17+
[dev-dependencies]
18+
meilisearch-sdk = "0.20.1"
19+
Lines changed: 280 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,280 @@
1+
use std::collections::HashSet;
2+
3+
use convert_case::{Case, Casing};
4+
use proc_macro2::TokenTree;
5+
use quote::quote;
6+
use syn::parse_macro_input;
7+
use syn::spanned::Spanned;
8+
9+
#[proc_macro_derive(Document, attributes(document))]
10+
pub fn generate_index_settings(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
11+
let ast = parse_macro_input!(input as syn::DeriveInput);
12+
13+
let fields: &syn::Fields = match ast.data {
14+
syn::Data::Struct(ref data) => &data.fields,
15+
_ => {
16+
return proc_macro::TokenStream::from(
17+
syn::Error::new(ast.ident.span(), "Applicable only to struct").to_compile_error(),
18+
);
19+
}
20+
};
21+
22+
let struct_ident = &ast.ident;
23+
24+
let document_implementation = get_document_implementation(struct_ident, fields);
25+
proc_macro::TokenStream::from(quote! {
26+
#document_implementation
27+
})
28+
}
29+
30+
fn get_document_implementation(
31+
struct_ident: &syn::Ident,
32+
fields: &syn::Fields,
33+
) -> proc_macro2::TokenStream {
34+
let mut attribute_set: std::collections::HashSet<String> = std::collections::HashSet::new();
35+
let mut primary_key_attribute: String = "".to_string();
36+
let mut distinct_key_attribute: String = "".to_string();
37+
let mut displayed_attributes: Vec<String> = vec![];
38+
let mut searchable_attributes: Vec<String> = vec![];
39+
let mut filterable_attributes: Vec<String> = vec![];
40+
let mut sortable_attributes: Vec<String> = vec![];
41+
let valid_attribute_names = std::collections::HashSet::from([
42+
"displayed",
43+
"searchable",
44+
"filterable",
45+
"sortable",
46+
"primary_key",
47+
"distinct",
48+
]);
49+
50+
let index_name = struct_ident
51+
.to_string()
52+
.from_case(Case::UpperCamel)
53+
.to_case(Case::Snake);
54+
55+
for field in fields {
56+
let attribute_list_result =
57+
extract_all_attr_values(&field.attrs, &mut attribute_set, &valid_attribute_names);
58+
59+
match attribute_list_result {
60+
Ok(attribute_list) => {
61+
for attribute in attribute_list {
62+
match attribute.as_str() {
63+
"displayed" => {
64+
displayed_attributes.push(field.ident.clone().unwrap().to_string())
65+
}
66+
"searchable" => {
67+
searchable_attributes.push(field.ident.clone().unwrap().to_string())
68+
}
69+
"filterable" => {
70+
filterable_attributes.push(field.ident.clone().unwrap().to_string())
71+
}
72+
"sortable" => {
73+
sortable_attributes.push(field.ident.clone().unwrap().to_string())
74+
}
75+
"primary_key" => {
76+
primary_key_attribute = field.ident.clone().unwrap().to_string()
77+
}
78+
"distinct" => {
79+
distinct_key_attribute = field.ident.clone().unwrap().to_string()
80+
}
81+
_ => {}
82+
}
83+
}
84+
}
85+
Err(e) => {
86+
return e;
87+
}
88+
}
89+
}
90+
91+
let primary_key_token: proc_macro2::TokenStream = if primary_key_attribute.is_empty() {
92+
quote! {
93+
::std::option::Option::None
94+
}
95+
} else {
96+
quote! {
97+
::std::option::Option::Some(#primary_key_attribute)
98+
}
99+
};
100+
101+
let display_attr_tokens =
102+
get_settings_token_for_list(&displayed_attributes, "with_displayed_attributes");
103+
let sortable_attr_tokens =
104+
get_settings_token_for_list(&sortable_attributes, "with_sortable_attributes");
105+
let filterable_attr_tokens =
106+
get_settings_token_for_list(&filterable_attributes, "with_filterable_attributes");
107+
let searchable_attr_tokens =
108+
get_settings_token_for_list(&searchable_attributes, "with_searchable_attributes");
109+
let distinct_attr_token =
110+
get_settings_token_for_string(&distinct_key_attribute, "with_distinct_attribute");
111+
112+
quote! {
113+
#[::meilisearch_sdk::macro_helper::async_trait]
114+
impl ::meilisearch_sdk::documents::Document for #struct_ident {
115+
fn generate_settings() -> ::meilisearch_sdk::settings::Settings {
116+
::meilisearch_sdk::settings::Settings::new()
117+
#display_attr_tokens
118+
#sortable_attr_tokens
119+
#filterable_attr_tokens
120+
#searchable_attr_tokens
121+
#distinct_attr_token
122+
}
123+
124+
async fn generate_index(client: &::meilisearch_sdk::client::Client) -> std::result::Result<::meilisearch_sdk::indexes::Index, ::meilisearch_sdk::tasks::Task> {
125+
return client.create_index(#index_name, #primary_key_token)
126+
.await.unwrap()
127+
.wait_for_completion(&client, ::std::option::Option::None, ::std::option::Option::None)
128+
.await.unwrap()
129+
.try_make_index(&client);
130+
}
131+
}
132+
}
133+
}
134+
135+
fn extract_all_attr_values(
136+
attrs: &[syn::Attribute],
137+
attribute_set: &mut std::collections::HashSet<String>,
138+
valid_attribute_names: &std::collections::HashSet<&str>,
139+
) -> std::result::Result<Vec<String>, proc_macro2::TokenStream> {
140+
let mut attribute_names: Vec<String> = vec![];
141+
let mut local_attribute_set: std::collections::HashSet<String> = HashSet::new();
142+
for attr in attrs {
143+
match attr.parse_meta() {
144+
std::result::Result::Ok(syn::Meta::List(list)) => {
145+
if !list.path.is_ident("document") {
146+
continue;
147+
}
148+
for token_stream in attr.tokens.clone().into_iter() {
149+
if let TokenTree::Group(group) = token_stream {
150+
for token in group.stream() {
151+
match token {
152+
TokenTree::Punct(punct) => validate_punct(&punct)?,
153+
TokenTree::Ident(ident) => {
154+
if ident == "primary_key"
155+
&& attribute_set.contains("primary_key")
156+
{
157+
return std::result::Result::Err(
158+
syn::Error::new(
159+
ident.span(),
160+
"`primary_key` already exists",
161+
)
162+
.to_compile_error(),
163+
);
164+
}
165+
if ident == "distinct" && attribute_set.contains("distinct") {
166+
return std::result::Result::Err(
167+
syn::Error::new(
168+
ident.span(),
169+
"`distinct` already exists",
170+
)
171+
.to_compile_error(),
172+
);
173+
}
174+
175+
if local_attribute_set.contains(ident.to_string().as_str()) {
176+
return std::result::Result::Err(
177+
syn::Error::new(
178+
ident.span(),
179+
format!(
180+
"`{}` already exists for this field",
181+
ident
182+
),
183+
)
184+
.to_compile_error(),
185+
);
186+
}
187+
188+
if !valid_attribute_names.contains(ident.to_string().as_str()) {
189+
return std::result::Result::Err(
190+
syn::Error::new(
191+
ident.span(),
192+
format!(
193+
"Property `{}` does not exist for type `document`",
194+
ident
195+
),
196+
)
197+
.to_compile_error(),
198+
);
199+
}
200+
attribute_names.push(ident.to_string());
201+
attribute_set.insert(ident.to_string());
202+
local_attribute_set.insert(ident.to_string());
203+
}
204+
_ => {
205+
return std::result::Result::Err(
206+
syn::Error::new(attr.span(), "Invalid parsing".to_string())
207+
.to_compile_error(),
208+
);
209+
}
210+
}
211+
}
212+
}
213+
}
214+
}
215+
std::result::Result::Err(e) => {
216+
println!("{:#?}", attr);
217+
for token_stream in attr.tokens.clone().into_iter() {
218+
if let TokenTree::Group(group) = token_stream {
219+
for token in group.stream() {
220+
if let TokenTree::Punct(punct) = token {
221+
validate_punct(&punct)?
222+
}
223+
}
224+
}
225+
}
226+
return std::result::Result::Err(
227+
syn::Error::new(attr.span(), e.to_string()).to_compile_error(),
228+
);
229+
}
230+
_ => {}
231+
}
232+
}
233+
std::result::Result::Ok(attribute_names)
234+
}
235+
236+
fn validate_punct(punct: &proc_macro2::Punct) -> std::result::Result<(), proc_macro2::TokenStream> {
237+
if punct.as_char() == ',' && punct.spacing() == proc_macro2::Spacing::Alone {
238+
return std::result::Result::Ok(());
239+
}
240+
std::result::Result::Err(
241+
syn::Error::new(punct.span(), "`,` expected".to_string()).to_compile_error(),
242+
)
243+
}
244+
245+
fn get_settings_token_for_list(
246+
field_name_list: &Vec<String>,
247+
method_name: &str,
248+
) -> proc_macro2::TokenStream {
249+
let string_attributes = field_name_list.iter().map(|attr| {
250+
quote! {
251+
#attr
252+
}
253+
});
254+
let method_ident = syn::Ident::new(method_name, proc_macro2::Span::call_site());
255+
256+
if !field_name_list.is_empty() {
257+
quote! {
258+
.#method_ident([#(#string_attributes),*])
259+
}
260+
} else {
261+
quote! {
262+
.#method_ident(::std::iter::empty::<&str>())
263+
}
264+
}
265+
}
266+
267+
fn get_settings_token_for_string(
268+
field_name: &String,
269+
method_name: &str,
270+
) -> proc_macro2::TokenStream {
271+
let method_ident = syn::Ident::new(method_name, proc_macro2::Span::call_site());
272+
273+
if !field_name.is_empty() {
274+
quote! {
275+
.#method_ident(#field_name)
276+
}
277+
} else {
278+
proc_macro2::TokenStream::new()
279+
}
280+
}

0 commit comments

Comments
 (0)