203 lines
6.2 KiB
Rust
203 lines
6.2 KiB
Rust
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<String>,
|
|
///
|
|
/// pub content: Option<String>,
|
|
///
|
|
/// #[index]
|
|
/// pub tags: BTreeMap<String, String>,
|
|
/// }
|
|
/// ```
|
|
#[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![],
|
|
},
|
|
_ => 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
|
|
}
|