Skip to main content

rok_search_macros/
lib.rs

1//! Proc-macro crate for `rok-orm` search abstraction.
2//!
3//! Provides `#[derive(Searchable)]` which generates an `impl rok_orm::search::Searchable`
4//! for any struct, reading `#[searchable(rank = N)]` field attributes.
5//!
6//! This crate is re-exported by `rok-orm` when `features = ["search", "search-macros"]`.
7//!
8//! # Example
9//! ```rust,ignore
10//! #[derive(serde::Serialize, serde::Deserialize, Searchable)]
11//! #[searchable(index = "posts")]
12//! pub struct Post {
13//!     pub id: i64,
14//!     #[searchable(rank = 10)]
15//!     pub title: String,
16//!     #[searchable(rank = 5)]
17//!     pub body: String,
18//!     pub published: bool,
19//! }
20//! ```
21
22use proc_macro::TokenStream;
23use proc_macro2::Span;
24use quote::quote;
25use syn::{parse_macro_input, Data, DeriveInput, Fields, LitInt, LitStr};
26
27/// Derive macro that implements `rok_orm::search::Searchable` for a struct.
28///
29/// Struct-level attribute `#[searchable(index = "table")]` sets the index name.
30/// Field-level attribute `#[searchable(rank = N)]` marks a field as searchable with
31/// the given rank weight (10 → A, 7–9 → B, 5–6 → C, < 5 → D).
32#[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}