Add proc macro to implement models
Signed-off-by: Lee Smet <lee.smet@hotmail.com>
This commit is contained in:
		
							
								
								
									
										10
									
								
								heromodels/Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										10
									
								
								heromodels/Cargo.lock
									
									
									
										generated
									
									
									
								
							@@ -262,11 +262,21 @@ dependencies = [
 | 
			
		||||
 "bincode",
 | 
			
		||||
 "chrono",
 | 
			
		||||
 "fjall",
 | 
			
		||||
 "heromodels-derive",
 | 
			
		||||
 "ourdb",
 | 
			
		||||
 "serde",
 | 
			
		||||
 "tst",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "heromodels-derive"
 | 
			
		||||
version = "0.1.0"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "proc-macro2",
 | 
			
		||||
 "quote",
 | 
			
		||||
 "syn",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "iana-time-zone"
 | 
			
		||||
version = "0.1.63"
 | 
			
		||||
 
 | 
			
		||||
@@ -12,3 +12,4 @@ chrono = { version = "0.4", features = ["serde"] }
 | 
			
		||||
fjall = "2.9.0"
 | 
			
		||||
ourdb = { path = "../ourdb" }
 | 
			
		||||
tst = { path = "../tst" }
 | 
			
		||||
heromodels-derive = { path = "./heromodels-derive" }
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										37
									
								
								heromodels/examples/custom_model_example.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								heromodels/examples/custom_model_example.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,37 @@
 | 
			
		||||
use heromodels::model;
 | 
			
		||||
use heromodels::models::core::model::{BaseModelData, Model, Index, IndexKey};
 | 
			
		||||
use serde::{Serialize, Deserialize};
 | 
			
		||||
 | 
			
		||||
// Define a custom attribute for indexing
 | 
			
		||||
#[derive(Debug, Clone, Serialize, Deserialize)]
 | 
			
		||||
#[model]
 | 
			
		||||
pub struct CustomUser {
 | 
			
		||||
    pub base_data: BaseModelData,
 | 
			
		||||
    
 | 
			
		||||
    // Mark fields for indexing with a comment
 | 
			
		||||
    // #[index]
 | 
			
		||||
    pub login: String,
 | 
			
		||||
    
 | 
			
		||||
    // #[index]
 | 
			
		||||
    pub is_active: bool,
 | 
			
		||||
    
 | 
			
		||||
    pub full_name: String,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn main() {
 | 
			
		||||
    println!("Hero Models - Custom Model Example");
 | 
			
		||||
    println!("==================================");
 | 
			
		||||
    
 | 
			
		||||
    // Example usage of the generated implementation
 | 
			
		||||
    println!("CustomUser DB Prefix: {}", CustomUser::db_prefix());
 | 
			
		||||
    
 | 
			
		||||
    let user = CustomUser {
 | 
			
		||||
        base_data: BaseModelData::new(1),
 | 
			
		||||
        login: "johndoe".to_string(),
 | 
			
		||||
        is_active: true,
 | 
			
		||||
        full_name: "John Doe".to_string(),
 | 
			
		||||
    };
 | 
			
		||||
    
 | 
			
		||||
    println!("\nCustomUser ID: {}", user.get_id());
 | 
			
		||||
    println!("CustomUser DB Keys: {:?}", user.db_keys());
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										58
									
								
								heromodels/examples/model_macro_example.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								heromodels/examples/model_macro_example.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,58 @@
 | 
			
		||||
use heromodels::model;
 | 
			
		||||
use heromodels::models::core::model::{BaseModelData, Model, Index, IndexKey};
 | 
			
		||||
use serde::{Serialize, Deserialize};
 | 
			
		||||
 | 
			
		||||
// Basic usage
 | 
			
		||||
#[derive(Debug, Clone, Serialize, Deserialize)]
 | 
			
		||||
#[model]
 | 
			
		||||
pub struct SimpleUser {
 | 
			
		||||
    pub base_data: BaseModelData,
 | 
			
		||||
    
 | 
			
		||||
    /// @index
 | 
			
		||||
    pub login: String,
 | 
			
		||||
    
 | 
			
		||||
    pub full_name: String,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// With customization options
 | 
			
		||||
#[derive(Debug, Clone, Serialize, Deserialize)]
 | 
			
		||||
#[model(prefix = "custom_user")]
 | 
			
		||||
pub struct CustomUser {
 | 
			
		||||
    pub base_data: BaseModelData,
 | 
			
		||||
    
 | 
			
		||||
    /// @index(name = "user_name", key_type = "str")
 | 
			
		||||
    pub login_name: String,
 | 
			
		||||
    
 | 
			
		||||
    /// @index(key_type = "bool")
 | 
			
		||||
    pub is_active: bool,
 | 
			
		||||
    
 | 
			
		||||
    pub full_name: String,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn main() {
 | 
			
		||||
    println!("Hero Models - Model Macro Example");
 | 
			
		||||
    println!("=================================");
 | 
			
		||||
    
 | 
			
		||||
    // Example usage of the generated implementations
 | 
			
		||||
    println!("SimpleUser DB Prefix: {}", SimpleUser::db_prefix());
 | 
			
		||||
    println!("CustomUser DB Prefix: {}", CustomUser::db_prefix());
 | 
			
		||||
    
 | 
			
		||||
    let user = SimpleUser {
 | 
			
		||||
        base_data: BaseModelData::new(1),
 | 
			
		||||
        login: "johndoe".to_string(),
 | 
			
		||||
        full_name: "John Doe".to_string(),
 | 
			
		||||
    };
 | 
			
		||||
    
 | 
			
		||||
    let custom_user = CustomUser {
 | 
			
		||||
        base_data: BaseModelData::new(2),
 | 
			
		||||
        login_name: "janesmith".to_string(),
 | 
			
		||||
        is_active: true,
 | 
			
		||||
        full_name: "Jane Smith".to_string(),
 | 
			
		||||
    };
 | 
			
		||||
    
 | 
			
		||||
    println!("\nSimpleUser ID: {}", user.get_id());
 | 
			
		||||
    println!("SimpleUser DB Keys: {:?}", user.db_keys());
 | 
			
		||||
    
 | 
			
		||||
    println!("\nCustomUser ID: {}", custom_user.get_id());
 | 
			
		||||
    println!("CustomUser DB Keys: {:?}", custom_user.db_keys());
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										29
									
								
								heromodels/examples/simple_model_example.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								heromodels/examples/simple_model_example.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,29 @@
 | 
			
		||||
use heromodels::model;
 | 
			
		||||
use heromodels::models::core::model::{BaseModelData, Model, IndexKey};
 | 
			
		||||
use serde::{Serialize, Deserialize};
 | 
			
		||||
 | 
			
		||||
// Basic usage
 | 
			
		||||
#[derive(Debug, Clone, Serialize, Deserialize)]
 | 
			
		||||
#[model]
 | 
			
		||||
pub struct SimpleUser {
 | 
			
		||||
    pub base_data: BaseModelData,
 | 
			
		||||
    pub login: String,
 | 
			
		||||
    pub full_name: String,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn main() {
 | 
			
		||||
    println!("Hero Models - Simple Model Example");
 | 
			
		||||
    println!("==================================");
 | 
			
		||||
    
 | 
			
		||||
    // Example usage of the generated implementation
 | 
			
		||||
    println!("SimpleUser DB Prefix: {}", SimpleUser::db_prefix());
 | 
			
		||||
    
 | 
			
		||||
    let user = SimpleUser {
 | 
			
		||||
        base_data: BaseModelData::new(1),
 | 
			
		||||
        login: "johndoe".to_string(),
 | 
			
		||||
        full_name: "John Doe".to_string(),
 | 
			
		||||
    };
 | 
			
		||||
    
 | 
			
		||||
    println!("\nSimpleUser ID: {}", user.get_id());
 | 
			
		||||
    println!("SimpleUser DB Keys: {:?}", user.db_keys());
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										14
									
								
								heromodels/heromodels-derive/Cargo.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								heromodels/heromodels-derive/Cargo.toml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,14 @@
 | 
			
		||||
[package]
 | 
			
		||||
name = "heromodels-derive"
 | 
			
		||||
version = "0.1.0"
 | 
			
		||||
edition = "2024"
 | 
			
		||||
description = "Derive macros for heromodels"
 | 
			
		||||
authors = ["Your Name <your.email@example.com>"]
 | 
			
		||||
 | 
			
		||||
[lib]
 | 
			
		||||
proc-macro = true
 | 
			
		||||
 | 
			
		||||
[dependencies]
 | 
			
		||||
syn = { version = "2.0", features = ["full", "extra-traits", "parsing"] }
 | 
			
		||||
quote = "1.0"
 | 
			
		||||
proc-macro2 = "1.0"
 | 
			
		||||
							
								
								
									
										158
									
								
								heromodels/heromodels-derive/src/lib.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										158
									
								
								heromodels/heromodels-derive/src/lib.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,158 @@
 | 
			
		||||
use proc_macro::TokenStream;
 | 
			
		||||
use quote::{quote, format_ident};
 | 
			
		||||
use syn::{parse_macro_input, DeriveInput, Data, Fields};
 | 
			
		||||
 | 
			
		||||
/// Convert a string to snake_case
 | 
			
		||||
fn to_snake_case(s: &str) -> String {
 | 
			
		||||
    let mut result = String::new();
 | 
			
		||||
    for (i, c) in s.char_indices() {
 | 
			
		||||
        if i > 0 && c.is_uppercase() {
 | 
			
		||||
            result.push('_');
 | 
			
		||||
        }
 | 
			
		||||
        result.push(c.to_lowercase().next().unwrap());
 | 
			
		||||
    }
 | 
			
		||||
    result
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Convert a string to PascalCase
 | 
			
		||||
fn to_pascal_case(s: &str) -> String {
 | 
			
		||||
    let mut result = String::new();
 | 
			
		||||
    let mut capitalize_next = true;
 | 
			
		||||
    
 | 
			
		||||
    for c in s.chars() {
 | 
			
		||||
        if c == '_' {
 | 
			
		||||
            capitalize_next = true;
 | 
			
		||||
        } else if capitalize_next {
 | 
			
		||||
            result.push(c.to_uppercase().next().unwrap());
 | 
			
		||||
            capitalize_next = false;
 | 
			
		||||
        } else {
 | 
			
		||||
            result.push(c);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    result
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
/// Implements the Model trait and generates Index trait implementations for fields marked with #[index].
 | 
			
		||||
#[proc_macro_attribute]
 | 
			
		||||
pub fn model(_attr: TokenStream, item: TokenStream) -> TokenStream {
 | 
			
		||||
    // Parse the input tokens into a syntax tree
 | 
			
		||||
    let input = parse_macro_input!(item as DeriveInput);
 | 
			
		||||
    
 | 
			
		||||
    // Extract struct name
 | 
			
		||||
    let struct_name = &input.ident;
 | 
			
		||||
    
 | 
			
		||||
    // Convert struct name to snake_case for db_prefix
 | 
			
		||||
    let name_str = struct_name.to_string();
 | 
			
		||||
    let db_prefix = to_snake_case(&name_str);
 | 
			
		||||
    
 | 
			
		||||
    // Extract fields with /// @index doc comment
 | 
			
		||||
    let mut indexed_fields = Vec::new();
 | 
			
		||||
    
 | 
			
		||||
    if let Data::Struct(data_struct) = &input.data {
 | 
			
		||||
        if let Fields::Named(fields_named) = &data_struct.fields {
 | 
			
		||||
            for field in &fields_named.named {
 | 
			
		||||
                for attr in &field.attrs {
 | 
			
		||||
                    if attr.path().is_ident("doc") {
 | 
			
		||||
                        let meta = attr.meta.clone().try_into().unwrap();
 | 
			
		||||
                        if let syn::Meta::NameValue(name_value) = meta {
 | 
			
		||||
                            if let syn::Expr::Lit(syn::ExprLit { lit: syn::Lit::Str(lit_str), .. }) = name_value.value {
 | 
			
		||||
                                let doc_str = lit_str.value();
 | 
			
		||||
                                if doc_str.trim().starts_with("@index") {
 | 
			
		||||
                                    if let Some(field_name) = &field.ident {
 | 
			
		||||
                                        indexed_fields.push(field_name);
 | 
			
		||||
                                    }
 | 
			
		||||
                                }
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    // Generate Model trait implementation
 | 
			
		||||
    let db_keys_impl = if indexed_fields.is_empty() {
 | 
			
		||||
        quote! {
 | 
			
		||||
            fn db_keys(&self) -> Vec<IndexKey> {
 | 
			
		||||
                Vec::new()
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    } else {
 | 
			
		||||
        let field_keys = indexed_fields.iter().map(|field_name| {
 | 
			
		||||
            let name_str = field_name.to_string();
 | 
			
		||||
            quote! {
 | 
			
		||||
                IndexKey {
 | 
			
		||||
                    name: #name_str,
 | 
			
		||||
                    value: self.#field_name.to_string(),
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
        
 | 
			
		||||
        quote! {
 | 
			
		||||
            fn db_keys(&self) -> Vec<IndexKey> {
 | 
			
		||||
                vec![
 | 
			
		||||
                    #(#field_keys),*
 | 
			
		||||
                ]
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
    
 | 
			
		||||
    let model_impl = quote! {
 | 
			
		||||
        impl Model for #struct_name {
 | 
			
		||||
            fn db_prefix() -> &'static str {
 | 
			
		||||
                #db_prefix
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            fn get_id(&self) -> u32 {
 | 
			
		||||
                self.base_data.id
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            fn base_data_mut(&mut self) -> &mut BaseModelData {
 | 
			
		||||
                &mut self.base_data
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            #db_keys_impl
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
    
 | 
			
		||||
    // Generate Index trait implementations
 | 
			
		||||
    let mut index_impls = proc_macro2::TokenStream::new();
 | 
			
		||||
    
 | 
			
		||||
    for field_name in &indexed_fields {
 | 
			
		||||
        let name_str = field_name.to_string();
 | 
			
		||||
        
 | 
			
		||||
        // Convert field name to PascalCase for struct name
 | 
			
		||||
        let struct_name_str = to_pascal_case(&name_str);
 | 
			
		||||
        let index_struct_name = format_ident!("{}", struct_name_str);
 | 
			
		||||
        
 | 
			
		||||
        // Default to str for key type
 | 
			
		||||
        let index_impl = quote! {
 | 
			
		||||
            pub struct #index_struct_name;
 | 
			
		||||
            
 | 
			
		||||
            impl Index for #index_struct_name {
 | 
			
		||||
                type Model = #struct_name;
 | 
			
		||||
                type Key = str;
 | 
			
		||||
                
 | 
			
		||||
                fn key() -> &'static str {
 | 
			
		||||
                    #name_str
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
        
 | 
			
		||||
        index_impls.extend(index_impl);
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    // Combine the original struct with the generated implementations
 | 
			
		||||
    let expanded = quote! {
 | 
			
		||||
        #input
 | 
			
		||||
        
 | 
			
		||||
        #model_impl
 | 
			
		||||
        
 | 
			
		||||
        #index_impls
 | 
			
		||||
    };
 | 
			
		||||
    
 | 
			
		||||
    // Return the generated code
 | 
			
		||||
    expanded.into()
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										31
									
								
								heromodels/heromodels-derive/tests/test_model_macro.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								heromodels/heromodels-derive/tests/test_model_macro.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,31 @@
 | 
			
		||||
use heromodels::model;
 | 
			
		||||
use heromodels::models::core::model::{BaseModelData, Model, Index};
 | 
			
		||||
 | 
			
		||||
#[model]
 | 
			
		||||
struct TestUser {
 | 
			
		||||
    base_data: BaseModelData,
 | 
			
		||||
    
 | 
			
		||||
    #[index]
 | 
			
		||||
    username: String,
 | 
			
		||||
    
 | 
			
		||||
    #[index]
 | 
			
		||||
    is_active: bool,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[test]
 | 
			
		||||
fn test_basic_model() {
 | 
			
		||||
    assert_eq!(TestUser::db_prefix(), "test_user");
 | 
			
		||||
    
 | 
			
		||||
    let user = TestUser {
 | 
			
		||||
        base_data: BaseModelData::new(1),
 | 
			
		||||
        username: "test".to_string(),
 | 
			
		||||
        is_active: true,
 | 
			
		||||
    };
 | 
			
		||||
    
 | 
			
		||||
    let keys = user.db_keys();
 | 
			
		||||
    assert_eq!(keys.len(), 2);
 | 
			
		||||
    assert_eq!(keys[0].name, "username");
 | 
			
		||||
    assert_eq!(keys[0].value, "test");
 | 
			
		||||
    assert_eq!(keys[1].name, "is_active");
 | 
			
		||||
    assert_eq!(keys[1].value, "true");
 | 
			
		||||
}
 | 
			
		||||
@@ -3,3 +3,6 @@ pub mod models;
 | 
			
		||||
 | 
			
		||||
/// Database implementations
 | 
			
		||||
pub mod db;
 | 
			
		||||
 | 
			
		||||
// Re-export the procedural macro
 | 
			
		||||
pub use heromodels_derive::model;
 | 
			
		||||
 
 | 
			
		||||
@@ -145,4 +145,3 @@ impl Index for IsActive {
 | 
			
		||||
        "is_active"
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user