Restructure crates for correct proc macro usage

Signed-off-by: Lee Smet <lee.smet@hotmail.com>
This commit is contained in:
Lee Smet
2025-04-25 13:58:17 +02:00
parent 96a1ecd974
commit b8e1449ddb
20 changed files with 588 additions and 362 deletions

9
heromodels/Cargo.lock generated
View File

@@ -263,6 +263,7 @@ dependencies = [
"chrono",
"fjall",
"heromodels-derive",
"heromodels_core",
"ourdb",
"serde",
"tst",
@@ -277,6 +278,14 @@ dependencies = [
"syn",
]
[[package]]
name = "heromodels_core"
version = "0.1.0"
dependencies = [
"chrono",
"serde",
]
[[package]]
name = "iana-time-zone"
version = "0.1.63"

View File

@@ -12,4 +12,5 @@ chrono = { version = "0.4", features = ["serde"] }
fjall = "2.9.0"
ourdb = { path = "../ourdb" }
tst = { path = "../tst" }
heromodels-derive = { path = "./heromodels-derive" }
heromodels-derive = { path = "../heromodels-derive" }
heromodels_core = { path = "../heromodels_core" }

View File

@@ -1,6 +1,7 @@
use heromodels::db::{Collection, Db};
use heromodels::models::userexample::user::index::{is_active, username};
use heromodels::models::{Comment, Model, User};
use heromodels::models::userexample::user::user_index::{is_active, username};
use heromodels::models::{Comment, User};
use heromodels_core::Model;
fn main() {
let index_db = tst::TST::new("/tmp/ourdb/tst", true).expect("can create index DB");

View File

@@ -1,5 +1,5 @@
use heromodels::model;
use heromodels::models::core::model::{BaseModelData, Model};
use heromodels_core::{BaseModelData, Model};
use heromodels_derive::model;
use serde::{Deserialize, Serialize};
// Define a custom attribute for indexing

View File

@@ -1,58 +1,58 @@
use heromodels::model;
use heromodels::models::core::model::{BaseModelData, Model, Index, IndexKey};
use serde::{Serialize, Deserialize};
use heromodels_core::{BaseModelData, Model};
use heromodels_derive::model;
use serde::{Deserialize, Serialize};
// 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")]
#[model]
pub struct CustomUser {
pub base_data: BaseModelData,
#[index(name = "user_name")]
pub login_name: String,
#[index]
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());
}
}

View File

@@ -1,6 +1,6 @@
use heromodels::model;
use heromodels::models::core::model::{BaseModelData, Model, IndexKey};
use serde::{Serialize, Deserialize};
use heromodels_core::{BaseModelData, Model};
use heromodels_derive::model;
use serde::{Deserialize, Serialize};
// Basic usage
#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -14,16 +14,17 @@ pub struct SimpleUser {
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());
}
}

View File

@@ -1,68 +0,0 @@
# 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

@@ -1,17 +0,0 @@
[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"
[dev-dependencies]
serde = { version = "1.0", features = ["derive"] }

View File

@@ -1,197 +0,0 @@
use proc_macro::TokenStream;
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 {
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 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] attribute
let mut indexed_fields = Vec::new();
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::<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
let db_keys_impl = if indexed_fields.is_empty() {
quote! {
fn db_keys(&self) -> Vec<crate::models::IndexKey> {
Vec::new()
}
}
} else {
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! {
crate::models::IndexKey {
name: #name_str,
value: self.#field_name.to_string(),
}
}
});
quote! {
fn db_keys(&self) -> Vec<crate::models::IndexKey> {
vec![
#(#field_keys),*
]
}
}
};
let model_impl = quote! {
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 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, 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 index_struct_name = format_ident!("{}", &name_str);
// Default to str for key type
let index_impl = quote! {
pub struct #index_struct_name;
impl crate::models::Index for #index_struct_name {
type Model = super::#struct_name;
type Key = #field_type;
fn key() -> &'static 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()
}

View File

@@ -1,106 +0,0 @@
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<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]
struct TestUser {
base_data: BaseModelData,
#[index]
username: String,
#[index]
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");
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");
}
#[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,6 +1,6 @@
use std::borrow::Borrow;
use crate::models::{Index, Model};
use heromodels_core::{Index, Model};
use serde::{Deserialize, Serialize};
pub mod fjall;

View File

@@ -1,8 +1,7 @@
use heromodels_core::{Index, Model};
use ourdb::OurDBSetArgs;
use serde::Deserialize;
use crate::models::{Index, Model};
use std::{
borrow::Borrow,
collections::HashSet,
@@ -30,7 +29,7 @@ impl OurDB {
impl super::Db for OurDB {
type Error = tst::Error;
fn collection<M: crate::models::Model>(
fn collection<M: Model>(
&self,
) -> Result<impl super::Collection<&str, M>, super::Error<Self::Error>> {
Ok(self.clone())

View File

@@ -1,10 +1,13 @@
use heromodels_core::BaseModelData;
use heromodels_derive::model;
use serde::{Deserialize, Serialize};
use crate::models::core::model::{Model, BaseModelData, IndexKey};
/// Represents a comment on a model
#[derive(Debug, Clone, Serialize, Deserialize)]
#[model]
pub struct Comment {
pub base_data: BaseModelData,
#[index]
pub user_id: u32,
pub content: String,
}
@@ -24,34 +27,10 @@ impl Comment {
self.user_id = id;
self
}
/// Set the content
pub fn content(mut self, content: impl ToString) -> Self {
self.content = content.to_string();
self
}
}
// Implement the Model trait for Comment
impl Model for Comment {
fn db_prefix() -> &'static str {
"comment"
}
fn get_id(&self) -> u32 {
self.base_data.id
}
fn base_data_mut(&mut self) -> &mut BaseModelData {
&mut self.base_data
}
fn db_keys(&self) -> Vec<IndexKey> {
vec![
IndexKey {
name: "user_id",
value: self.user_id.to_string(),
},
]
}
}

View File

@@ -1,7 +1,7 @@
// Export submodules
pub mod model;
pub mod comment;
pub mod model;
// Re-export key types for convenience
pub use model::{Model, BaseModelData, IndexKey, IndexKeyBuilder, Index};
pub use comment::Comment;
pub use comment::Comment;

View File

@@ -1,223 +0,0 @@
use serde::{Deserialize, Serialize};
use std::fmt::Debug;
/// Represents an index key for a model
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct IndexKey {
/// The name of the index key
pub name: &'static str,
/// The value of the index key for a specific model instance
pub value: String,
}
/// Builder for IndexKey
pub struct IndexKeyBuilder {
name: &'static str,
value: String,
}
impl IndexKeyBuilder {
/// Create a new IndexKeyBuilder
pub fn new(name: &'static str) -> Self {
Self {
name,
value: String::new(),
}
}
/// Set the value for this index key
pub fn value(mut self, value: impl ToString) -> Self {
self.value = value.to_string();
self
}
/// Build the IndexKey
pub fn build(self) -> IndexKey {
IndexKey {
name: self.name,
value: self.value,
}
}
}
/// Unified trait for all models
pub trait Model:
Debug + Clone + Serialize + for<'de> Deserialize<'de> + Send + Sync + 'static
{
/// Get the database prefix for this model type
fn db_prefix() -> &'static str
where
Self: Sized;
/// Returns a list of index keys for this model instance
/// These keys will be used to create additional indexes in the TST
/// The default implementation returns an empty vector
/// Override this method to provide custom indexes
fn db_keys(&self) -> Vec<IndexKey> {
Vec::new()
}
/// Get the unique ID for this model
fn get_id(&self) -> u32;
/// Get a mutable reference to the base_data field
fn base_data_mut(&mut self) -> &mut BaseModelData;
/// Set the ID for this model
fn id(mut self, id: u32) -> Self
where
Self: Sized,
{
self.base_data_mut().id = id;
self
}
/// Build the model, updating the modified timestamp
fn build(mut self) -> Self
where
Self: Sized,
{
self.base_data_mut().update_modified();
self
}
}
/// An identifier for an index in the DB
pub trait Index {
/// The model for which this is an index in the database
type Model: Model;
type Key: ToString + ?Sized;
/// The key of this index
fn key() -> &'static str;
}
/// Base struct that all models should include
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BaseModelData {
/// Unique incremental ID per circle
pub id: u32,
/// Unix epoch timestamp for creation time
pub created_at: i64,
/// Unix epoch timestamp for last modification time
pub modified_at: i64,
/// List of comment IDs referencing Comment objects
pub comments: Vec<u32>,
}
impl BaseModelData {
/// Create a new BaseModelData instance
pub fn new(id: u32) -> Self {
let now = chrono::Utc::now().timestamp();
Self {
id,
created_at: now,
modified_at: now,
comments: Vec::new(),
}
}
/// Create a new BaseModelDataBuilder
pub fn builder(id: u32) -> BaseModelDataBuilder {
BaseModelDataBuilder::new(id)
}
/// Add a comment to this model
pub fn add_comment(&mut self, comment_id: u32) {
self.comments.push(comment_id);
self.modified_at = chrono::Utc::now().timestamp();
}
/// Remove a comment from this model
pub fn remove_comment(&mut self, comment_id: u32) {
self.comments.retain(|&id| id != comment_id);
self.update_modified();
}
/// Update the modified timestamp
pub fn update_modified(&mut self) {
self.modified_at = chrono::Utc::now().timestamp();
}
}
/// Builder for BaseModelData
pub struct BaseModelDataBuilder {
id: u32,
created_at: Option<i64>,
modified_at: Option<i64>,
comments: Vec<u32>,
}
impl BaseModelDataBuilder {
/// Create a new BaseModelDataBuilder
pub fn new(id: u32) -> Self {
Self {
id,
created_at: None,
modified_at: None,
comments: Vec::new(),
}
}
/// Set the created_at timestamp
pub fn created_at(mut self, timestamp: i64) -> Self {
self.created_at = Some(timestamp);
self
}
/// Set the modified_at timestamp
pub fn modified_at(mut self, timestamp: i64) -> Self {
self.modified_at = Some(timestamp);
self
}
/// Add a comment ID
pub fn add_comment(mut self, comment_id: u32) -> Self {
self.comments.push(comment_id);
self
}
/// Add multiple comment IDs
pub fn add_comments(mut self, comment_ids: Vec<u32>) -> Self {
self.comments.extend(comment_ids);
self
}
/// Build the BaseModelData
pub fn build(self) -> BaseModelData {
let now = chrono::Utc::now().timestamp();
BaseModelData {
id: self.id,
created_at: self.created_at.unwrap_or(now),
modified_at: self.modified_at.unwrap_or(now),
comments: self.comments,
}
}
}
/// Macro to implement Model for a struct that contains a base_data field of type BaseModelData
#[macro_export]
macro_rules! impl_model {
// Basic implementation with default db_keys
($type:ty, $prefix:expr) => {
impl $crate::core::model::Model for $type {
fn db_prefix() -> &'static str {
$prefix
}
fn get_id(&self) -> u32 {
self.base_data.id
}
fn base_data_mut(&mut self) -> &mut $crate::core::model::BaseModelData {
&mut self.base_data
}
}
};
}

View File

@@ -3,6 +3,6 @@ pub mod core;
pub mod userexample;
// Re-export key types for convenience
pub use core::model::{Model, BaseModelData, IndexKey, Index};
pub use core::Comment;
pub use userexample::User;
pub use userexample::User;

View File

@@ -1,4 +1,4 @@
use crate::models::core::model::BaseModelData;
use heromodels_core::BaseModelData;
use heromodels_derive::model;
use serde::{Deserialize, Serialize};
@@ -81,72 +81,3 @@ impl User {
self.is_active = true;
}
}
// 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<IndexKey> {
// 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"
// }
// }