first commit
This commit is contained in:
12
osiris_derive/Cargo.toml
Normal file
12
osiris_derive/Cargo.toml
Normal file
@@ -0,0 +1,12 @@
|
||||
[package]
|
||||
name = "osiris_derive"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
proc-macro = true
|
||||
|
||||
[dependencies]
|
||||
syn = { version = "2.0", features = ["full", "extra-traits"] }
|
||||
quote = "1.0"
|
||||
proc-macro2 = "1.0"
|
||||
202
osiris_derive/src/lib.rs
Normal file
202
osiris_derive/src/lib.rs
Normal file
@@ -0,0 +1,202 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user