1use proc_macro::TokenStream;
23use proc_macro2::Span;
24use quote::quote;
25use syn::{parse_macro_input, Data, DeriveInput, Fields, LitInt, LitStr};
26
27#[proc_macro_derive(Searchable, attributes(searchable))]
33pub fn derive_searchable(input: TokenStream) -> TokenStream {
34 let input = parse_macro_input!(input as DeriveInput);
35 let ident = &input.ident;
36
37 let mut index_name = ident.to_string().to_lowercase() + "s";
38
39 for attr in &input.attrs {
40 if attr.path().is_ident("searchable") {
41 let _ = attr.parse_nested_meta(|meta| {
42 if meta.path.is_ident("index") {
43 let value: LitStr = meta.value()?.parse()?;
44 index_name = value.value();
45 }
46 Ok(())
47 });
48 }
49 }
50
51 let fields = match &input.data {
52 Data::Struct(s) => match &s.fields {
53 Fields::Named(f) => &f.named,
54 _ => {
55 return syn::Error::new(
56 Span::call_site(),
57 "#[derive(Searchable)] requires named fields",
58 )
59 .to_compile_error()
60 .into();
61 }
62 },
63 _ => {
64 return syn::Error::new(
65 Span::call_site(),
66 "#[derive(Searchable)] only supports structs",
67 )
68 .to_compile_error()
69 .into();
70 }
71 };
72
73 let mut field_entries = Vec::new();
74
75 for field in fields {
76 let field_name = field.ident.as_ref().unwrap().to_string();
77 let mut rank: Option<u8> = None;
78
79 for attr in &field.attrs {
80 if attr.path().is_ident("searchable") {
81 let _ = attr.parse_nested_meta(|meta| {
82 if meta.path.is_ident("rank") {
83 let value: LitInt = meta.value()?.parse()?;
84 rank = Some(value.base10_parse::<u8>().map_err(|_| {
85 syn::Error::new(Span::call_site(), "rank must be a u8")
86 })?);
87 }
88 Ok(())
89 });
90 }
91 }
92
93 if let Some(r) = rank {
94 field_entries.push((field_name, r));
95 }
96 }
97
98 let searchable_fields_tokens: Vec<_> = field_entries
99 .iter()
100 .map(|(name, rank)| {
101 let rank_val = *rank;
102 quote! {
103 ::rok_orm::search::SearchField {
104 name: #name.into(),
105 weight: ::rok_orm::search::RankWeight::from_rank(#rank_val),
106 }
107 }
108 })
109 .collect();
110
111 let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
112
113 let expanded = quote! {
114 impl #impl_generics ::rok_orm::search::Searchable for #ident #ty_generics #where_clause {
115 fn index_name() -> &'static str {
116 #index_name
117 }
118 fn searchable_fields() -> ::std::vec::Vec<::rok_orm::search::SearchField> {
119 vec![ #(#searchable_fields_tokens),* ]
120 }
121 }
122 };
123
124 TokenStream::from(expanded)
125}