use proc_macro::TokenStream; use quote::quote; use syn::{parse_macro_input, Data, DeriveInput, Fields, Type}; /// Derive macro for the Object trait /// /// Automatically implements `index_keys()` and `indexed_fields()` based on fields marked with #[index] /// /// # Example /// /// ```rust /// #[derive(Object)] /// pub struct Note { /// pub base_data: BaseData, /// /// #[index] /// pub title: Option, /// /// pub content: Option, /// /// #[index] /// pub tags: BTreeMap, /// } /// ``` #[proc_macro_derive(Object, attributes(index))] pub fn derive_object(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DeriveInput); let name = &input.ident; let generics = &input.generics; let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); // Extract fields with #[index] attribute let indexed_fields = match &input.data { Data::Struct(data) => match &data.fields { Fields::Named(fields) => { fields.named.iter().filter_map(|field| { let has_index = field.attrs.iter().any(|attr| { attr.path().is_ident("index") }); if has_index { let field_name = field.ident.as_ref()?; let field_type = &field.ty; Some((field_name.clone(), field_type.clone())) } else { None } }).collect::>() } _ => vec![], }, _ => vec![], }; // Generate index_keys() implementation let index_keys_impl = generate_index_keys(&indexed_fields); // Generate indexed_fields() implementation let field_names: Vec<_> = indexed_fields.iter() .map(|(name, _)| name.to_string()) .collect(); // Always use ::osiris for external usage // When used inside the osiris crate's src/, the compiler will resolve it correctly let crate_path = quote! { ::osiris }; let expanded = quote! { impl #impl_generics #crate_path::Object for #name #ty_generics #where_clause { fn object_type() -> &'static str { stringify!(#name) } fn base_data(&self) -> &#crate_path::BaseData { &self.base_data } fn base_data_mut(&mut self) -> &mut #crate_path::BaseData { &mut self.base_data } fn index_keys(&self) -> Vec<#crate_path::IndexKey> { let mut keys = Vec::new(); // Index from base_data if let Some(mime) = &self.base_data.mime { keys.push(#crate_path::IndexKey::new("mime", mime)); } #index_keys_impl keys } fn indexed_fields() -> Vec<&'static str> { vec![#(#field_names),*] } } }; TokenStream::from(expanded) } fn generate_index_keys(fields: &[(syn::Ident, Type)]) -> proc_macro2::TokenStream { let mut implementations = Vec::new(); // Always use ::osiris let crate_path = quote! { ::osiris }; for (field_name, field_type) in fields { let field_name_str = field_name.to_string(); // Check if it's an Option type if is_option_type(field_type) { implementations.push(quote! { if let Some(value) = &self.#field_name { keys.push(#crate_path::IndexKey::new(#field_name_str, value)); } }); } // Check if it's a BTreeMap (for tags) else if is_btreemap_type(field_type) { implementations.push(quote! { for (key, value) in &self.#field_name { keys.push(#crate_path::IndexKey { name: concat!(#field_name_str, ":tag"), value: format!("{}={}", key, value), }); } }); } // Check if it's a Vec else if is_vec_type(field_type) { implementations.push(quote! { for (idx, value) in self.#field_name.iter().enumerate() { keys.push(#crate_path::IndexKey { name: concat!(#field_name_str, ":item"), value: format!("{}:{}", idx, value), }); } }); } // For OffsetDateTime, index as date string else if is_offsetdatetime_type(field_type) { implementations.push(quote! { { let date_str = self.#field_name.date().to_string(); keys.push(#crate_path::IndexKey::new(#field_name_str, date_str)); } }); } // For enums or other types, convert to string else { implementations.push(quote! { { let value_str = format!("{:?}", &self.#field_name); keys.push(#crate_path::IndexKey::new(#field_name_str, value_str)); } }); } } quote! { #(#implementations)* } } fn is_option_type(ty: &Type) -> bool { if let Type::Path(type_path) = ty { if let Some(segment) = type_path.path.segments.last() { return segment.ident == "Option"; } } false } fn is_btreemap_type(ty: &Type) -> bool { if let Type::Path(type_path) = ty { if let Some(segment) = type_path.path.segments.last() { return segment.ident == "BTreeMap"; } } false } fn is_vec_type(ty: &Type) -> bool { if let Type::Path(type_path) = ty { if let Some(segment) = type_path.path.segments.last() { return segment.ident == "Vec"; } } false } fn is_offsetdatetime_type(ty: &Type) -> bool { if let Type::Path(type_path) = ty { if let Some(segment) = type_path.path.segments.last() { return segment.ident == "OffsetDateTime"; } } false }