Update macro to use #[index] attributes

- Also use proper types for index.
 - Update DB interface to be more flexible for index params

Signed-off-by: Lee Smet <lee.smet@hotmail.com>
This commit is contained in:
Lee Smet 2025-04-25 13:26:15 +02:00
parent dc93518a35
commit 96a1ecd974
Signed by untrusted user who does not match committer: lee
GPG Key ID: 72CBFB5FDA7FE025
10 changed files with 368 additions and 167 deletions

View File

@ -1,5 +1,5 @@
use heromodels::db::{Collection, Db}; 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}; use heromodels::models::{Comment, Model, User};
fn main() { fn main() {
@ -64,7 +64,7 @@ fn main() {
let stored_users = db let stored_users = db
.collection::<User>() .collection::<User>()
.expect("can open user collection") .expect("can open user collection")
.get::<UserName>("johndoe") .get::<username, _>("johndoe")
.expect("can load stored user"); .expect("can load stored user");
assert_eq!(stored_users.len(), 1); assert_eq!(stored_users.len(), 1);
@ -80,7 +80,7 @@ fn main() {
let active_users = db let active_users = db
.collection::<User>() .collection::<User>()
.expect("can open user collection") .expect("can open user collection")
.get::<IsActive>(&true) .get::<is_active, _>(&true)
.expect("can load stored users"); .expect("can load stored users");
// We should have 2 active users // We should have 2 active users
assert_eq!(active_users.len(), 2); assert_eq!(active_users.len(), 2);
@ -95,14 +95,14 @@ fn main() {
let active_users = db let active_users = db
.collection::<User>() .collection::<User>()
.expect("can open user collection") .expect("can open user collection")
.get::<IsActive>(&true) .get::<is_active, _>(&true)
.expect("can load stored users"); .expect("can load stored users");
assert_eq!(active_users.len(), 1); assert_eq!(active_users.len(), 1);
// And verify we still have 2 inactive users // And verify we still have 2 inactive users
let inactive_users = db let inactive_users = db
.collection::<User>() .collection::<User>()
.expect("can open user collection") .expect("can open user collection")
.get::<IsActive>(&false) .get::<is_active, _>(&false)
.expect("can load stored users"); .expect("can load stored users");
assert_eq!(inactive_users.len(), 2); assert_eq!(inactive_users.len(), 2);

View File

@ -1,37 +1,37 @@
use heromodels::model; use heromodels::model;
use heromodels::models::core::model::{BaseModelData, Model, Index, IndexKey}; use heromodels::models::core::model::{BaseModelData, Model};
use serde::{Serialize, Deserialize}; use serde::{Deserialize, Serialize};
// Define a custom attribute for indexing // Define a custom attribute for indexing
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
#[model] #[model]
pub struct CustomUser { pub struct CustomUser {
pub base_data: BaseModelData, pub base_data: BaseModelData,
// Mark fields for indexing with a comment // Mark fields for indexing with attributes
// #[index] #[index]
pub login: String, pub login: String,
// #[index] #[index]
pub is_active: bool, pub is_active: bool,
pub full_name: String, pub full_name: String,
} }
fn main() { fn main() {
println!("Hero Models - Custom Model Example"); println!("Hero Models - Custom Model Example");
println!("=================================="); println!("==================================");
// Example usage of the generated implementation // Example usage of the generated implementation
println!("CustomUser DB Prefix: {}", CustomUser::db_prefix()); println!("CustomUser DB Prefix: {}", CustomUser::db_prefix());
let user = CustomUser { let user = CustomUser {
base_data: BaseModelData::new(1), base_data: BaseModelData::new(1),
login: "johndoe".to_string(), login: "johndoe".to_string(),
is_active: true, is_active: true,
full_name: "John Doe".to_string(), full_name: "John Doe".to_string(),
}; };
println!("\nCustomUser ID: {}", user.get_id()); println!("\nCustomUser ID: {}", user.get_id());
println!("CustomUser DB Keys: {:?}", user.db_keys()); println!("CustomUser DB Keys: {:?}", user.db_keys());
} }

View File

@ -8,7 +8,7 @@ use serde::{Serialize, Deserialize};
pub struct SimpleUser { pub struct SimpleUser {
pub base_data: BaseModelData, pub base_data: BaseModelData,
/// @index #[index]
pub login: String, pub login: String,
pub full_name: String, pub full_name: String,
@ -20,10 +20,10 @@ pub struct SimpleUser {
pub struct CustomUser { pub struct CustomUser {
pub base_data: BaseModelData, pub base_data: BaseModelData,
/// @index(name = "user_name", key_type = "str") #[index(name = "user_name")]
pub login_name: String, pub login_name: String,
/// @index(key_type = "bool") #[index]
pub is_active: bool, pub is_active: bool,
pub full_name: String, pub full_name: String,

68
heromodels/heromodels-derive/Cargo.lock generated Normal file
View File

@ -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"

View File

@ -11,4 +11,7 @@ proc-macro = true
[dependencies] [dependencies]
syn = { version = "2.0", features = ["full", "extra-traits", "parsing"] } syn = { version = "2.0", features = ["full", "extra-traits", "parsing"] }
quote = "1.0" quote = "1.0"
proc-macro2 = "1.0" proc-macro2 = "1.0"
[dev-dependencies]
serde = { version = "1.0", features = ["derive"] }

View File

@ -1,6 +1,6 @@
use proc_macro::TokenStream; use proc_macro::TokenStream;
use quote::{quote, format_ident}; use quote::{format_ident, quote};
use syn::{parse_macro_input, DeriveInput, Data, Fields}; use syn::{Data, DeriveInput, Fields, parse_macro_input};
/// Convert a string to snake_case /// Convert a string to snake_case
fn to_snake_case(s: &str) -> String { fn to_snake_case(s: &str) -> String {
@ -15,144 +15,183 @@ fn to_snake_case(s: &str) -> String {
} }
/// Convert a string to PascalCase /// Convert a string to PascalCase
fn to_pascal_case(s: &str) -> String { // fn to_pascal_case(s: &str) -> String {
let mut result = String::new(); // let mut result = String::new();
let mut capitalize_next = true; // let mut capitalize_next = true;
//
for c in s.chars() { // for c in s.chars() {
if c == '_' { // if c == '_' {
capitalize_next = true; // capitalize_next = true;
} else if capitalize_next { // } else if capitalize_next {
result.push(c.to_uppercase().next().unwrap()); // result.push(c.to_uppercase().next().unwrap());
capitalize_next = false; // capitalize_next = false;
} else { // } else {
result.push(c); // result.push(c);
} // }
} // }
//
result // result
} // }
/// Implements the Model trait and generates Index trait implementations for fields marked with #[index]. /// Implements the Model trait and generates Index trait implementations for fields marked with #[index].
#[proc_macro_attribute] #[proc_macro_attribute]
pub fn model(_attr: TokenStream, item: TokenStream) -> TokenStream { pub fn model(_attr: TokenStream, item: TokenStream) -> TokenStream {
// Parse the input tokens into a syntax tree // 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 // Extract struct name
let struct_name = &input.ident; let struct_name = &input.ident;
// Convert struct name to snake_case for db_prefix // Convert struct name to snake_case for db_prefix
let name_str = struct_name.to_string(); let name_str = struct_name.to_string();
let db_prefix = to_snake_case(&name_str); 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(); let mut indexed_fields = Vec::new();
let mut custom_index_names = std::collections::HashMap::new();
if let Data::Struct(data_struct) = &input.data {
if let Fields::Named(fields_named) = &data_struct.fields { if let Data::Struct(ref mut data_struct) = input.data {
for field in &fields_named.named { if let Fields::Named(ref mut fields_named) = data_struct.fields {
for attr in &field.attrs { for field in &mut fields_named.named {
if attr.path().is_ident("doc") { let mut attr_idx = None;
let meta = attr.meta.clone().try_into().unwrap(); for (i, attr) in field.attrs.iter().enumerate() {
if let syn::Meta::NameValue(name_value) = meta { if attr.path().is_ident("index") {
if let syn::Expr::Lit(syn::ExprLit { lit: syn::Lit::Str(lit_str), .. }) = name_value.value { attr_idx = Some(i);
let doc_str = lit_str.value(); if let Some(ref field_name) = field.ident {
if doc_str.trim().starts_with("@index") { // Check if the attribute has parameters
if let Some(field_name) = &field.ident { let mut custom_name = None;
indexed_fields.push(field_name);
// 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::<syn::Meta, syn::Token![,]>::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 // Generate Model trait implementation
let db_keys_impl = if indexed_fields.is_empty() { let db_keys_impl = if indexed_fields.is_empty() {
quote! { quote! {
fn db_keys(&self) -> Vec<IndexKey> { fn db_keys(&self) -> Vec<crate::models::IndexKey> {
Vec::new() Vec::new()
} }
} }
} else { } else {
let field_keys = indexed_fields.iter().map(|field_name| { let field_keys = indexed_fields.iter().map(|(field_name, _)| {
let name_str = field_name.to_string(); let name_str = custom_index_names
.get(&field_name.to_string())
.cloned()
.unwrap_or(field_name.to_string());
quote! { quote! {
IndexKey { crate::models::IndexKey {
name: #name_str, name: #name_str,
value: self.#field_name.to_string(), value: self.#field_name.to_string(),
} }
} }
}); });
quote! { quote! {
fn db_keys(&self) -> Vec<IndexKey> { fn db_keys(&self) -> Vec<crate::models::IndexKey> {
vec![ vec![
#(#field_keys),* #(#field_keys),*
] ]
} }
} }
}; };
let model_impl = quote! { let model_impl = quote! {
impl Model for #struct_name { impl crate::models::Model for #struct_name {
fn db_prefix() -> &'static str { fn db_prefix() -> &'static str {
#db_prefix #db_prefix
} }
fn get_id(&self) -> u32 { fn get_id(&self) -> u32 {
self.base_data.id 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 &mut self.base_data
} }
#db_keys_impl #db_keys_impl
} }
}; };
// Generate Index trait implementations // Generate Index trait implementations
let mut index_impls = proc_macro2::TokenStream::new(); 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(); 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 // Convert field name to PascalCase for struct name
let struct_name_str = to_pascal_case(&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!("{}", struct_name_str);
let index_struct_name = format_ident!("{}", &name_str);
// Default to str for key type // Default to str for key type
let index_impl = quote! { let index_impl = quote! {
pub struct #index_struct_name; pub struct #index_struct_name;
impl Index for #index_struct_name { impl crate::models::Index for #index_struct_name {
type Model = #struct_name; type Model = super::#struct_name;
type Key = str; type Key = #field_type;
fn key() -> &'static str { fn key() -> &'static str {
#name_str #index_key
} }
} }
}; };
index_impls.extend(index_impl); 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 // Combine the original struct with the generated implementations
let expanded = quote! { let expanded = quote! {
#input #input
#model_impl #model_impl
#index_impls #index_impls
}; };
// Return the generated code // Return the generated code
expanded.into() expanded.into()
} }

View File

@ -1,6 +1,48 @@
use heromodels::model; use heromodels_derive::model;
use heromodels::models::core::model::{BaseModelData, Model, Index}; 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<u32>,
}
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<IndexKey>;
}
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] #[model]
struct TestUser { struct TestUser {
base_data: BaseModelData, base_data: BaseModelData,
@ -12,6 +54,19 @@ struct TestUser {
is_active: bool, 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] #[test]
fn test_basic_model() { fn test_basic_model() {
assert_eq!(TestUser::db_prefix(), "test_user"); assert_eq!(TestUser::db_prefix(), "test_user");
@ -28,4 +83,24 @@ fn test_basic_model() {
assert_eq!(keys[0].value, "test"); assert_eq!(keys[0].value, "test");
assert_eq!(keys[1].name, "is_active"); assert_eq!(keys[1].name, "is_active");
assert_eq!(keys[1].value, "true"); 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");
} }

View File

@ -1,3 +1,5 @@
use std::borrow::Borrow;
use crate::models::{Index, Model}; use crate::models::{Index, Model};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -22,9 +24,11 @@ where
type Error: std::fmt::Debug; type Error: std::fmt::Debug;
/// Get all items where the given index field is equal to key. /// Get all items where the given index field is equal to key.
fn get<I>(&self, key: &I::Key) -> Result<Vec<V>, Error<Self::Error>> fn get<I, Q>(&self, key: &Q) -> Result<Vec<V>, Error<Self::Error>>
where where
I: Index<Model = V>; I: Index<Model = V>,
I::Key: Borrow<Q>,
Q: ToString + ?Sized;
/// Get an object from its ID. This does not use an index lookup /// Get an object from its ID. This does not use an index lookup
fn get_by_id(&self, id: u32) -> Result<Option<V>, Error<Self::Error>>; fn get_by_id(&self, id: u32) -> Result<Option<V>, Error<Self::Error>>;
@ -33,9 +37,11 @@ where
fn set(&self, value: &V) -> Result<(), Error<Self::Error>>; fn set(&self, value: &V) -> Result<(), Error<Self::Error>>;
/// Delete all items from the db with a given index. /// Delete all items from the db with a given index.
fn delete<I>(&self, key: &I::Key) -> Result<(), Error<Self::Error>> fn delete<I, Q>(&self, key: &Q) -> Result<(), Error<Self::Error>>
where where
I: Index<Model = V>; I: Index<Model = V>,
I::Key: Borrow<Q>,
Q: ToString + ?Sized;
/// Delete an object with a given ID /// Delete an object with a given ID
fn delete_by_id(&self, id: u32) -> Result<(), Error<Self::Error>>; fn delete_by_id(&self, id: u32) -> Result<(), Error<Self::Error>>;

View File

@ -4,6 +4,7 @@ use serde::Deserialize;
use crate::models::{Index, Model}; use crate::models::{Index, Model};
use std::{ use std::{
borrow::Borrow,
collections::HashSet, collections::HashSet,
sync::{Arc, Mutex}, sync::{Arc, Mutex},
}; };
@ -42,9 +43,11 @@ where
{ {
type Error = tst::Error; type Error = tst::Error;
fn get<I>(&self, key: &I::Key) -> Result<Vec<M>, super::Error<Self::Error>> fn get<I, Q>(&self, key: &Q) -> Result<Vec<M>, super::Error<Self::Error>>
where where
I: Index<Model = M>, I: Index<Model = M>,
I::Key: Borrow<Q>,
Q: ToString + ?Sized,
{ {
let mut index_db = self.index.lock().expect("can lock index DB"); 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()); let index_key = Self::index_key(M::db_prefix(), I::key(), &key.to_string());
@ -140,9 +143,11 @@ where
Ok(()) Ok(())
} }
fn delete<I>(&self, key: &I::Key) -> Result<(), super::Error<Self::Error>> fn delete<I, Q>(&self, key: &Q) -> Result<(), super::Error<Self::Error>>
where where
I: Index<Model = M>, I: Index<Model = M>,
I::Key: Borrow<Q>,
Q: ToString + ?Sized,
{ {
let mut index_db = self.index.lock().expect("can lock index db"); 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()); let key = Self::index_key(M::db_prefix(), I::key(), &key.to_string());

View File

@ -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}; use serde::{Deserialize, Serialize};
/// Represents a user in the system /// Represents a user in the system
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
#[model]
pub struct User { pub struct User {
/// Base model data /// Base model data
pub base_data: BaseModelData, pub base_data: BaseModelData,
/// User's username /// User's username
#[index]
pub username: String, pub username: String,
/// User's email address /// User's email address
#[index]
pub email: String, pub email: String,
/// User's full name /// User's full name
pub full_name: String, pub full_name: String,
/// Whether the user is active /// Whether the user is active
#[index(name = "ac")]
pub is_active: bool, pub is_active: bool,
} }
@ -78,70 +83,70 @@ impl User {
} }
// Implement the Model trait for User // Implement the Model trait for User
impl Model for User { // impl Model for User {
fn db_prefix() -> &'static str { // fn db_prefix() -> &'static str {
"user" // "user"
} // }
//
fn get_id(&self) -> u32 { // fn get_id(&self) -> u32 {
self.base_data.id // self.base_data.id
} // }
//
//WHY? // //WHY?
fn base_data_mut(&mut self) -> &mut BaseModelData { // fn base_data_mut(&mut self) -> &mut BaseModelData {
&mut self.base_data // &mut self.base_data
} // }
//
fn db_keys(&self) -> Vec<IndexKey> { // fn db_keys(&self) -> Vec<IndexKey> {
vec![ // vec![
IndexKey { // IndexKey {
name: "username", // name: "username",
value: self.username.clone(), // value: self.username.clone(),
}, // },
IndexKey { // IndexKey {
name: "email", // name: "email",
value: self.email.clone(), // value: self.email.clone(),
}, // },
IndexKey { // IndexKey {
name: "is_active", // name: "is_active",
value: self.is_active.to_string(), // value: self.is_active.to_string(),
}, // },
] // ]
} // }
} // }
//
// Marker structs for indexed fields // // Marker structs for indexed fields
//
pub struct UserName; // pub struct UserName;
pub struct Email; // pub struct Email;
pub struct IsActive; // pub struct IsActive;
//
impl Index for UserName { // impl Index for UserName {
type Model = User; // type Model = User;
//
type Key = str; // type Key = str;
//
fn key() -> &'static str { // fn key() -> &'static str {
"username" // "username"
} // }
} // }
//
impl Index for Email { // impl Index for Email {
type Model = User; // type Model = User;
//
type Key = str; // type Key = str;
//
fn key() -> &'static str { // fn key() -> &'static str {
"email" // "email"
} // }
} // }
//
impl Index for IsActive { // impl Index for IsActive {
type Model = User; // type Model = User;
//
type Key = bool; // type Key = bool;
//
fn key() -> &'static str { // fn key() -> &'static str {
"is_active" // "is_active"
} // }
} // }