diff --git a/heromodels/examples/basic_user_example.rs b/heromodels/examples/basic_user_example.rs index f12747d..7a64391 100644 --- a/heromodels/examples/basic_user_example.rs +++ b/heromodels/examples/basic_user_example.rs @@ -1,5 +1,5 @@ use heromodels::db::{Collection, Db}; -use heromodels::models::userexample::user::{IsActive, UserName}; +use heromodels::models::userexample::user::index::{is_active, username}; use heromodels::models::{Comment, Model, User}; fn main() { @@ -64,7 +64,7 @@ fn main() { let stored_users = db .collection::() .expect("can open user collection") - .get::("johndoe") + .get::("johndoe") .expect("can load stored user"); assert_eq!(stored_users.len(), 1); @@ -80,7 +80,7 @@ fn main() { let active_users = db .collection::() .expect("can open user collection") - .get::(&true) + .get::(&true) .expect("can load stored users"); // We should have 2 active users assert_eq!(active_users.len(), 2); @@ -95,14 +95,14 @@ fn main() { let active_users = db .collection::() .expect("can open user collection") - .get::(&true) + .get::(&true) .expect("can load stored users"); assert_eq!(active_users.len(), 1); // And verify we still have 2 inactive users let inactive_users = db .collection::() .expect("can open user collection") - .get::(&false) + .get::(&false) .expect("can load stored users"); assert_eq!(inactive_users.len(), 2); diff --git a/heromodels/examples/custom_model_example.rs b/heromodels/examples/custom_model_example.rs index 4ff42d5..408b871 100644 --- a/heromodels/examples/custom_model_example.rs +++ b/heromodels/examples/custom_model_example.rs @@ -1,37 +1,37 @@ use heromodels::model; -use heromodels::models::core::model::{BaseModelData, Model, Index, IndexKey}; -use serde::{Serialize, Deserialize}; +use heromodels::models::core::model::{BaseModelData, Model}; +use serde::{Deserialize, Serialize}; // 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] + + // Mark fields for indexing with attributes + #[index] pub login: String, - - // #[index] + + #[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()); -} \ No newline at end of file +} diff --git a/heromodels/examples/model_macro_example.rs b/heromodels/examples/model_macro_example.rs index 85bbf0f..c1b6f85 100644 --- a/heromodels/examples/model_macro_example.rs +++ b/heromodels/examples/model_macro_example.rs @@ -8,7 +8,7 @@ use serde::{Serialize, Deserialize}; pub struct SimpleUser { pub base_data: BaseModelData, - /// @index + #[index] pub login: String, pub full_name: String, @@ -20,10 +20,10 @@ pub struct SimpleUser { pub struct CustomUser { pub base_data: BaseModelData, - /// @index(name = "user_name", key_type = "str") + #[index(name = "user_name")] pub login_name: String, - /// @index(key_type = "bool") + #[index] pub is_active: bool, pub full_name: String, diff --git a/heromodels/heromodels-derive/Cargo.lock b/heromodels/heromodels-derive/Cargo.lock new file mode 100644 index 0000000..fd87b6e --- /dev/null +++ b/heromodels/heromodels-derive/Cargo.lock @@ -0,0 +1,68 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "heromodels-derive" +version = "0.1.0" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "serde" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "syn" +version = "2.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" diff --git a/heromodels/heromodels-derive/Cargo.toml b/heromodels/heromodels-derive/Cargo.toml index 1bb53f4..8e1c125 100644 --- a/heromodels/heromodels-derive/Cargo.toml +++ b/heromodels/heromodels-derive/Cargo.toml @@ -11,4 +11,7 @@ proc-macro = true [dependencies] syn = { version = "2.0", features = ["full", "extra-traits", "parsing"] } quote = "1.0" -proc-macro2 = "1.0" \ No newline at end of file +proc-macro2 = "1.0" + +[dev-dependencies] +serde = { version = "1.0", features = ["derive"] } \ No newline at end of file diff --git a/heromodels/heromodels-derive/src/lib.rs b/heromodels/heromodels-derive/src/lib.rs index c78d6bd..72f8b94 100644 --- a/heromodels/heromodels-derive/src/lib.rs +++ b/heromodels/heromodels-derive/src/lib.rs @@ -1,6 +1,6 @@ use proc_macro::TokenStream; -use quote::{quote, format_ident}; -use syn::{parse_macro_input, DeriveInput, Data, Fields}; +use quote::{format_ident, quote}; +use syn::{Data, DeriveInput, Fields, parse_macro_input}; /// Convert a string to snake_case fn to_snake_case(s: &str) -> String { @@ -15,144 +15,183 @@ fn to_snake_case(s: &str) -> String { } /// 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 -} - +// 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); - + let mut 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 + + // Extract fields with #[index] attribute 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); + let mut custom_index_names = std::collections::HashMap::new(); + + if let Data::Struct(ref mut data_struct) = input.data { + if let Fields::Named(ref mut fields_named) = data_struct.fields { + for field in &mut fields_named.named { + let mut attr_idx = None; + for (i, attr) in field.attrs.iter().enumerate() { + if attr.path().is_ident("index") { + attr_idx = Some(i); + if let Some(ref field_name) = field.ident { + // Check if the attribute has parameters + let mut custom_name = None; + + // Parse attribute arguments if any + let meta = attr.meta.clone(); + if let syn::Meta::List(list) = meta { + if let Ok(nested) = list.parse_args_with(syn::punctuated::Punctuated::::parse_terminated) { + for meta in nested { + if let syn::Meta::NameValue(name_value) = meta { + if name_value.path.is_ident("name") { + if let syn::Expr::Lit(syn::ExprLit { lit: syn::Lit::Str(lit_str), .. }) = name_value.value { + custom_name = Some(lit_str.value()); + } + } + } + } } - } + } + + indexed_fields.push((field_name.clone(), field.ty.clone())); + + if let Some(name) = custom_name { + custom_index_names.insert(field_name.to_string(), name); } } } } + + if let Some(idx) = attr_idx { + field.attrs.remove(idx); + } } } } - + // Generate Model trait implementation let db_keys_impl = if indexed_fields.is_empty() { quote! { - fn db_keys(&self) -> Vec { + fn db_keys(&self) -> Vec { Vec::new() } } } else { - let field_keys = indexed_fields.iter().map(|field_name| { - let name_str = field_name.to_string(); + let field_keys = indexed_fields.iter().map(|(field_name, _)| { + let name_str = custom_index_names + .get(&field_name.to_string()) + .cloned() + .unwrap_or(field_name.to_string()); quote! { - IndexKey { + crate::models::IndexKey { name: #name_str, value: self.#field_name.to_string(), } } }); - + quote! { - fn db_keys(&self) -> Vec { + fn db_keys(&self) -> Vec { vec![ #(#field_keys),* ] } } }; - + let model_impl = quote! { - impl Model for #struct_name { + impl crate::models::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 { + + fn base_data_mut(&mut self) -> &mut crate::models::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 { + + for (field_name, field_type) in &indexed_fields { let name_str = field_name.to_string(); - + + // Get custom index name if specified, otherwise use field name + let index_key = match custom_index_names.get(&name_str) { + Some(custom_name) => custom_name.clone(), + None => name_str.clone(), + }; + // 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); - + // let struct_name_str = to_pascal_case(&name_str); + // let index_struct_name = format_ident!("{}", struct_name_str); + let index_struct_name = format_ident!("{}", &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; - + + impl crate::models::Index for #index_struct_name { + type Model = super::#struct_name; + type Key = #field_type; + fn key() -> &'static str { - #name_str + #index_key } } }; - + index_impls.extend(index_impl); } - + + if !index_impls.is_empty() { + index_impls = quote! { + pub mod index { + #index_impls + } + } + } + // Combine the original struct with the generated implementations let expanded = quote! { #input - + #model_impl - + #index_impls }; - + // Return the generated code expanded.into() } diff --git a/heromodels/heromodels-derive/tests/test_model_macro.rs b/heromodels/heromodels-derive/tests/test_model_macro.rs index bc75ae4..fab6d70 100644 --- a/heromodels/heromodels-derive/tests/test_model_macro.rs +++ b/heromodels/heromodels-derive/tests/test_model_macro.rs @@ -1,6 +1,48 @@ -use heromodels::model; -use heromodels::models::core::model::{BaseModelData, Model, Index}; +use heromodels_derive::model; +use serde::{Serialize, Deserialize}; +// Define the necessary structs and traits for testing +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct BaseModelData { + pub id: u32, + pub created_at: i64, + pub modified_at: i64, + pub comments: Vec, +} + +impl BaseModelData { + pub fn new(id: u32) -> Self { + let now = 1000; // Mock timestamp + Self { + id, + created_at: now, + modified_at: now, + comments: Vec::new(), + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct IndexKey { + pub name: &'static str, + pub value: String, +} + +pub trait Model: std::fmt::Debug + Clone { + fn db_prefix() -> &'static str; + fn get_id(&self) -> u32; + fn base_data_mut(&mut self) -> &mut BaseModelData; + fn db_keys(&self) -> Vec; +} + +pub trait Index { + type Model: Model; + type Key: ?Sized; + fn key() -> &'static str; +} + +// Test struct using the model macro +#[derive(Debug, Clone, Serialize, Deserialize)] #[model] struct TestUser { base_data: BaseModelData, @@ -12,6 +54,19 @@ struct TestUser { is_active: bool, } +// Test struct with custom index name +#[derive(Debug, Clone, Serialize, Deserialize)] +#[model] +struct TestUserWithCustomIndex { + base_data: BaseModelData, + + #[index(name = "custom_username")] + username: String, + + #[index] + is_active: bool, +} + #[test] fn test_basic_model() { assert_eq!(TestUser::db_prefix(), "test_user"); @@ -28,4 +83,24 @@ fn test_basic_model() { assert_eq!(keys[0].value, "test"); assert_eq!(keys[1].name, "is_active"); assert_eq!(keys[1].value, "true"); +} + +#[test] +fn test_custom_index_name() { + let user = TestUserWithCustomIndex { + base_data: BaseModelData::new(1), + username: "test".to_string(), + is_active: true, + }; + + // Check that the Username struct uses the custom index name + assert_eq!(Username::key(), "custom_username"); + + // Check that the db_keys method returns the correct keys + let keys = user.db_keys(); + assert_eq!(keys.len(), 2); + assert_eq!(keys[0].name, "custom_username"); + assert_eq!(keys[0].value, "test"); + assert_eq!(keys[1].name, "is_active"); + assert_eq!(keys[1].value, "true"); } \ No newline at end of file diff --git a/heromodels/src/db.rs b/heromodels/src/db.rs index 7c1f0e1..130a280 100644 --- a/heromodels/src/db.rs +++ b/heromodels/src/db.rs @@ -1,3 +1,5 @@ +use std::borrow::Borrow; + use crate::models::{Index, Model}; use serde::{Deserialize, Serialize}; @@ -22,9 +24,11 @@ where type Error: std::fmt::Debug; /// Get all items where the given index field is equal to key. - fn get(&self, key: &I::Key) -> Result, Error> + fn get(&self, key: &Q) -> Result, Error> where - I: Index; + I: Index, + I::Key: Borrow, + Q: ToString + ?Sized; /// Get an object from its ID. This does not use an index lookup fn get_by_id(&self, id: u32) -> Result, Error>; @@ -33,9 +37,11 @@ where fn set(&self, value: &V) -> Result<(), Error>; /// Delete all items from the db with a given index. - fn delete(&self, key: &I::Key) -> Result<(), Error> + fn delete(&self, key: &Q) -> Result<(), Error> where - I: Index; + I: Index, + I::Key: Borrow, + Q: ToString + ?Sized; /// Delete an object with a given ID fn delete_by_id(&self, id: u32) -> Result<(), Error>; diff --git a/heromodels/src/db/hero.rs b/heromodels/src/db/hero.rs index f66b92e..e064bf0 100644 --- a/heromodels/src/db/hero.rs +++ b/heromodels/src/db/hero.rs @@ -4,6 +4,7 @@ use serde::Deserialize; use crate::models::{Index, Model}; use std::{ + borrow::Borrow, collections::HashSet, sync::{Arc, Mutex}, }; @@ -42,9 +43,11 @@ where { type Error = tst::Error; - fn get(&self, key: &I::Key) -> Result, super::Error> + fn get(&self, key: &Q) -> Result, super::Error> where I: Index, + I::Key: Borrow, + Q: ToString + ?Sized, { let mut index_db = self.index.lock().expect("can lock index DB"); let index_key = Self::index_key(M::db_prefix(), I::key(), &key.to_string()); @@ -140,9 +143,11 @@ where Ok(()) } - fn delete(&self, key: &I::Key) -> Result<(), super::Error> + fn delete(&self, key: &Q) -> Result<(), super::Error> where I: Index, + I::Key: Borrow, + Q: ToString + ?Sized, { let mut index_db = self.index.lock().expect("can lock index db"); let key = Self::index_key(M::db_prefix(), I::key(), &key.to_string()); diff --git a/heromodels/src/models/userexample/user.rs b/heromodels/src/models/userexample/user.rs index 0b8e7e7..d906567 100644 --- a/heromodels/src/models/userexample/user.rs +++ b/heromodels/src/models/userexample/user.rs @@ -1,22 +1,27 @@ -use crate::models::core::model::{BaseModelData, Index, IndexKey, Model}; +use crate::models::core::model::BaseModelData; +use heromodels_derive::model; use serde::{Deserialize, Serialize}; /// Represents a user in the system #[derive(Debug, Clone, Serialize, Deserialize)] +#[model] pub struct User { /// Base model data pub base_data: BaseModelData, /// User's username + #[index] pub username: String, /// User's email address + #[index] pub email: String, /// User's full name pub full_name: String, /// Whether the user is active + #[index(name = "ac")] pub is_active: bool, } @@ -78,70 +83,70 @@ impl User { } // Implement the Model trait for User -impl Model for User { - fn db_prefix() -> &'static str { - "user" - } - - fn get_id(&self) -> u32 { - self.base_data.id - } - - //WHY? - fn base_data_mut(&mut self) -> &mut BaseModelData { - &mut self.base_data - } - - fn db_keys(&self) -> Vec { - vec![ - IndexKey { - name: "username", - value: self.username.clone(), - }, - IndexKey { - name: "email", - value: self.email.clone(), - }, - IndexKey { - name: "is_active", - value: self.is_active.to_string(), - }, - ] - } -} - -// Marker structs for indexed fields - -pub struct UserName; -pub struct Email; -pub struct IsActive; - -impl Index for UserName { - type Model = User; - - type Key = str; - - fn key() -> &'static str { - "username" - } -} - -impl Index for Email { - type Model = User; - - type Key = str; - - fn key() -> &'static str { - "email" - } -} - -impl Index for IsActive { - type Model = User; - - type Key = bool; - - fn key() -> &'static str { - "is_active" - } -} +// impl Model for User { +// fn db_prefix() -> &'static str { +// "user" +// } +// +// fn get_id(&self) -> u32 { +// self.base_data.id +// } +// +// //WHY? +// fn base_data_mut(&mut self) -> &mut BaseModelData { +// &mut self.base_data +// } +// +// fn db_keys(&self) -> Vec { +// vec![ +// IndexKey { +// name: "username", +// value: self.username.clone(), +// }, +// IndexKey { +// name: "email", +// value: self.email.clone(), +// }, +// IndexKey { +// name: "is_active", +// value: self.is_active.to_string(), +// }, +// ] +// } +// } +// +// // Marker structs for indexed fields +// +// pub struct UserName; +// pub struct Email; +// pub struct IsActive; +// +// impl Index for UserName { +// type Model = User; +// +// type Key = str; +// +// fn key() -> &'static str { +// "username" +// } +// } +// +// impl Index for Email { +// type Model = User; +// +// type Key = str; +// +// fn key() -> &'static str { +// "email" +// } +// } +// +// impl Index for IsActive { +// type Model = User; +// +// type Key = bool; +// +// fn key() -> &'static str { +// "is_active" +// } +// }