From a6768542518f55e1712492e6e7a4fc3663f8581a Mon Sep 17 00:00:00 2001 From: Mahmoud Emad Date: Sat, 17 May 2025 13:00:05 +0300 Subject: [PATCH] fix: Use incremental ID --- heromodels-derive/src/lib.rs | 2 +- heromodels/examples/basic_user_example.rs | 66 +++++++++++--------- heromodels/examples/simple_model_example.rs | 2 +- heromodels/src/db.rs | 5 +- heromodels/src/db/hero.rs | 61 +++++++++++++++--- heromodels/src/models/calendar/calendar.rs | 7 +-- heromodels/src/models/core/comment.rs | 9 +-- heromodels/src/models/finance/account.rs | 6 +- heromodels/src/models/finance/asset.rs | 11 ++-- heromodels/src/models/finance/marketplace.rs | 33 +++++----- heromodels/src/models/governance/proposal.rs | 18 +++--- heromodels/src/models/userexample/user.rs | 9 +-- heromodels_core/src/lib.rs | 36 +++++------ 13 files changed, 149 insertions(+), 116 deletions(-) diff --git a/heromodels-derive/src/lib.rs b/heromodels-derive/src/lib.rs index 0d0930b..94c62a5 100644 --- a/heromodels-derive/src/lib.rs +++ b/heromodels-derive/src/lib.rs @@ -130,7 +130,7 @@ pub fn model(_attr: TokenStream, item: TokenStream) -> TokenStream { } fn get_id(&self) -> u32 { - self.base_data.id.unwrap_or(0) + self.base_data.id } fn base_data_mut(&mut self) -> &mut heromodels_core::BaseModelData { diff --git a/heromodels/examples/basic_user_example.rs b/heromodels/examples/basic_user_example.rs index 2607492..e625a2d 100644 --- a/heromodels/examples/basic_user_example.rs +++ b/heromodels/examples/basic_user_example.rs @@ -33,68 +33,73 @@ fn main() { println!("Hero Models - Basic Usage Example"); println!("================================"); - // Create users with different ID configurations + // Create users with auto-generated IDs - // User 1: With explicit ID - let user1 = User::new(Some(1)) + // User 1 + let user1 = User::new() .username("johndoe") .email("john.doe@example.com") .full_name("John Doe") .is_active(false) .build(); - // User 2: With auto-generated ID - let user2 = User::new(None) + // User 2 + let user2 = User::new() .username("janesmith") .email("jane.smith@example.com") .full_name("Jane Smith") .is_active(true) .build(); - // User 3: With explicit ID - let user3 = User::new(Some(3)) + // User 3 + let user3 = User::new() .username("willism") .email("willis.masters@example.com") .full_name("Willis Masters") .is_active(true) .build(); - // User 4: With explicit ID - let user4 = User::new(Some(4)) + // User 4 + let user4 = User::new() .username("carrols") .email("carrol.smith@example.com") .full_name("Carrol Smith") .is_active(false) .build(); - // Save all users to database - db.collection().expect("can open user collection").set(&user1).expect("can set user"); - db.collection().expect("can open user collection").set(&user2).expect("can set user"); - db.collection().expect("can open user collection").set(&user3).expect("can set user"); - db.collection().expect("can open user collection").set(&user4).expect("can set user"); + // Save all users to database and get their assigned IDs + let user1_id = db.collection().expect("can open user collection").set(&user1).expect("can set user"); + let user2_id = db.collection().expect("can open user collection").set(&user2).expect("can set user"); + let user3_id = db.collection().expect("can open user collection").set(&user3).expect("can set user"); + let user4_id = db.collection().expect("can open user collection").set(&user4).expect("can set user"); - // Retrieve all users from database + println!("User 1 assigned ID: {}", user1_id); + println!("User 2 assigned ID: {}", user2_id); + println!("User 3 assigned ID: {}", user3_id); + println!("User 4 assigned ID: {}", user4_id); + + // Retrieve all users from database using the assigned IDs let db_user1 = db.collection::().expect("can open user collection") - .get_by_id(user1.get_id()).expect("can load user").expect("user should exist"); + .get_by_id(user1_id).expect("can load user").expect("user should exist"); let db_user2 = db.collection::().expect("can open user collection") - .get_by_id(user2.get_id()).expect("can load user").expect("user should exist"); + .get_by_id(user2_id).expect("can load user").expect("user should exist"); let db_user3 = db.collection::().expect("can open user collection") - .get_by_id(user3.get_id()).expect("can load user").expect("user should exist"); + .get_by_id(user3_id).expect("can load user").expect("user should exist"); let db_user4 = db.collection::().expect("can open user collection") - .get_by_id(user4.get_id()).expect("can load user").expect("user should exist"); + .get_by_id(user4_id).expect("can load user").expect("user should exist"); // Print all users retrieved from database println!("\n--- Users Retrieved from Database ---"); - println!("\n1. User with explicit ID (1):"); + println!("\n1. First user:"); print_user_details(&db_user1); - println!("\n2. User with auto-generated ID:"); + println!("\n2. Second user:"); print_user_details(&db_user2); - println!("\n3. User with explicit ID (3):"); + println!("\n3. Third user:"); print_user_details(&db_user3); - println!("\n4. User with explicit ID (4):"); + println!("\n4. Fourth user:"); print_user_details(&db_user4); // Demonstrate different ways to retrieve users from the database @@ -126,9 +131,11 @@ fn main() { // 3. Delete a user and show the updated results println!("\n3. After Deleting a User:"); + let user_to_delete_id = active_users[0].get_id(); + println!("Deleting user with ID: {}", user_to_delete_id); db.collection::() .expect("can open user collection") - .delete_by_id(active_users[0].get_id()) + .delete_by_id(user_to_delete_id) .expect("can delete existing user"); // Show remaining active users @@ -165,21 +172,24 @@ fn main() { // 1. Create and save a comment println!("\n1. Creating a Comment:"); - let comment = Comment::new(None) + let comment = Comment::new() .user_id(db_user1.get_id()) // commenter's user ID .content("This is a comment on the user") .build(); - db.collection() + // Save the comment and get its assigned ID + let comment_id = db.collection() .expect("can open comment collection") .set(&comment) .expect("can set comment"); - // 2. Retrieve the comment from database + println!("Comment assigned ID: {}", comment_id); + + // 2. Retrieve the comment from database using the assigned ID let db_comment = db .collection::() .expect("can open comment collection") - .get_by_id(comment.get_id()) + .get_by_id(comment_id) .expect("can load comment") .expect("comment should exist"); diff --git a/heromodels/examples/simple_model_example.rs b/heromodels/examples/simple_model_example.rs index c24f9b2..a5394b5 100644 --- a/heromodels/examples/simple_model_example.rs +++ b/heromodels/examples/simple_model_example.rs @@ -19,7 +19,7 @@ fn main() { println!("SimpleUser DB Prefix: {}", SimpleUser::db_prefix()); let user = SimpleUser { - base_data: BaseModelData::new(1), + base_data: BaseModelData::new(), login: "johndoe".to_string(), full_name: "John Doe".to_string(), }; diff --git a/heromodels/src/db.rs b/heromodels/src/db.rs index a593793..70ed851 100644 --- a/heromodels/src/db.rs +++ b/heromodels/src/db.rs @@ -32,8 +32,9 @@ where /// Get an object from its ID. This does not use an index lookup fn get_by_id(&self, id: u32) -> Result, Error>; - /// Store an item in the DB. - fn set(&self, value: &V) -> Result<(), Error>; + /// Store an item in the DB and return the assigned ID. + /// This method does not modify the original model. + fn set(&self, value: &V) -> Result>; /// Delete all items from the db with a given index. fn delete(&self, key: &Q) -> Result<(), Error> diff --git a/heromodels/src/db/hero.rs b/heromodels/src/db/hero.rs index 3d5d173..3abec38 100644 --- a/heromodels/src/db/hero.rs +++ b/heromodels/src/db/hero.rs @@ -26,7 +26,7 @@ impl OurDB { data_path.push("data"); let data_db = ourdb::OurDB::new(ourdb::OurDBConfig { - incremental_mode: false, + incremental_mode: true, path: data_path, file_size: None, keysize: None, @@ -87,7 +87,7 @@ where Self::get_ourdb_value(&mut data_db, id) } - fn set(&self, value: &M) -> Result<(), super::Error> { + fn set(&self, value: &M) -> Result> { // Before inserting the new object, check if an object with this ID already exists. If it does, we potentially need to update indices. let mut data_db = self.data.lock().expect("can lock data DB"); let old_obj: Option = Self::get_ourdb_value(&mut data_db, value.get_id())?; @@ -134,13 +134,52 @@ where } } - // set or update the object - let v = bincode::serde::encode_to_vec(value, BINCODE_CONFIG)?; + // Get the current ID let id = value.get_id(); - data_db.set(OurDBSetArgs { - id: Some(id), - data: &v, - })?; + + // If id is 0, it's a new object, so let OurDB auto-generate an ID + // Otherwise, it's an update to an existing object + let id_param = if id == 0 { None } else { Some(id) }; + + // For new objects (id == 0), we need to get the assigned ID from OurDB + // and update the model before serializing it + let assigned_id = if id == 0 { + // First, get the next ID that OurDB will assign + let next_id = data_db.get_next_id()?; + + // Create a mutable clone of the value and update its ID + // This is a bit of a hack, but we need to update the ID before serializing + let mut value_clone = value.clone(); + let base_data = value_clone.base_data_mut(); + base_data.update_id(next_id); + + // Now serialize the updated model + let v = bincode::serde::encode_to_vec(&value_clone, BINCODE_CONFIG)?; + + // Save to OurDB with the ID parameter set to None to let OurDB auto-generate the ID + let assigned_id = data_db.set(OurDBSetArgs { + id: id_param, + data: &v, + })?; + + // The assigned ID should match the next_id we got earlier + assert_eq!(assigned_id, next_id, "OurDB assigned a different ID than expected"); + + // Return the assigned ID + assigned_id + } else { + // For existing objects, just serialize and save + let v = bincode::serde::encode_to_vec(value, BINCODE_CONFIG)?; + + // Save to OurDB with the existing ID + let assigned_id = data_db.set(OurDBSetArgs { + id: id_param, + data: &v, + })?; + + // Return the existing ID + assigned_id + }; // Now add the new indices for index_key in indices_to_add { @@ -148,12 +187,14 @@ where // Load the existing id set for the index or create a new set let mut existing_ids = Self::get_tst_value::>(&mut index_db, &key)?.unwrap_or_default(); - existing_ids.insert(id); + // Use the assigned ID for new objects + existing_ids.insert(assigned_id); let encoded_ids = bincode::serde::encode_to_vec(existing_ids, BINCODE_CONFIG)?; index_db.set(&key, encoded_ids)?; } - Ok(()) + // Return the assigned ID + Ok(assigned_id) } fn delete(&self, key: &Q) -> Result<(), super::Error> diff --git a/heromodels/src/models/calendar/calendar.rs b/heromodels/src/models/calendar/calendar.rs index b42ddbd..a7eee92 100644 --- a/heromodels/src/models/calendar/calendar.rs +++ b/heromodels/src/models/calendar/calendar.rs @@ -139,14 +139,13 @@ pub struct Calendar { } impl Calendar { - /// Creates a new calendar + /// Creates a new calendar with auto-generated ID /// /// # Arguments - /// * `id` - Optional ID for the calendar. If None, the ID will be auto-generated. /// * `name` - Name of the calendar - pub fn new(id: Option, name: impl ToString) -> Self { + pub fn new(name: impl ToString) -> Self { Self { - base_data: BaseModelData::new(id), + base_data: BaseModelData::new(), name: name.to_string(), description: None, events: Vec::new(), diff --git a/heromodels/src/models/core/comment.rs b/heromodels/src/models/core/comment.rs index a43770f..28bcefd 100644 --- a/heromodels/src/models/core/comment.rs +++ b/heromodels/src/models/core/comment.rs @@ -13,13 +13,10 @@ pub struct Comment { } impl Comment { - /// Create a new comment - /// - /// # Arguments - /// * `id` - Optional ID for the comment. If None, the ID will be auto-generated. - pub fn new(id: Option) -> Self { + /// Create a new comment with auto-generated ID + pub fn new() -> Self { Self { - base_data: BaseModelData::new(id), + base_data: BaseModelData::new(), user_id: 0, content: String::new(), } diff --git a/heromodels/src/models/finance/account.rs b/heromodels/src/models/finance/account.rs index 14447ce..fe80b6d 100644 --- a/heromodels/src/models/finance/account.rs +++ b/heromodels/src/models/finance/account.rs @@ -21,10 +21,9 @@ pub struct Account { } impl Account { - /// Create a new account + /// Create a new account with auto-generated ID /// /// # Arguments - /// * `id` - Optional ID for the account. If None, the ID will be auto-generated. /// * `name` - Name of the account /// * `user_id` - ID of the user who owns the account /// * `description` - Description of the account @@ -32,7 +31,6 @@ impl Account { /// * `address` - Address of the account on the blockchain /// * `pubkey` - Public key pub fn new( - id: Option, name: impl ToString, user_id: u32, description: impl ToString, @@ -41,7 +39,7 @@ impl Account { pubkey: impl ToString ) -> Self { Self { - base_data: BaseModelData::new(id), + base_data: BaseModelData::new(), name: name.to_string(), user_id, description: description.to_string(), diff --git a/heromodels/src/models/finance/asset.rs b/heromodels/src/models/finance/asset.rs index 3da924a..9fd3195 100644 --- a/heromodels/src/models/finance/asset.rs +++ b/heromodels/src/models/finance/asset.rs @@ -33,9 +33,8 @@ pub struct Asset { } impl Asset { - /// Create a new asset + /// Create a new asset with auto-generated ID pub fn new( - id: Option, name: impl ToString, description: impl ToString, amount: f64, @@ -44,7 +43,7 @@ impl Asset { decimals: u8, ) -> Self { Self { - base_data: BaseModelData::new(id), + base_data: BaseModelData::new(), name: name.to_string(), description: description.to_string(), amount, @@ -72,14 +71,14 @@ impl Asset { if amount <= 0.0 { return Err("Transfer amount must be positive"); } - + if self.amount < amount { return Err("Insufficient balance for transfer"); } - + self.amount -= amount; target.amount += amount; - + Ok(()) } } diff --git a/heromodels/src/models/finance/marketplace.rs b/heromodels/src/models/finance/marketplace.rs index 4104f05..d236b3c 100644 --- a/heromodels/src/models/finance/marketplace.rs +++ b/heromodels/src/models/finance/marketplace.rs @@ -111,9 +111,8 @@ pub struct Listing { } impl Listing { - /// Create a new listing + /// Create a new listing with auto-generated ID pub fn new( - id: Option, title: impl ToString, description: impl ToString, asset_id: impl ToString, @@ -127,7 +126,7 @@ impl Listing { image_url: Option, ) -> Self { Self { - base_data: BaseModelData::new(id), + base_data: BaseModelData::new(), title: title.to_string(), description: description.to_string(), asset_id: asset_id.to_string(), @@ -153,32 +152,32 @@ impl Listing { if self.listing_type != ListingType::Auction { return Err("Bids can only be placed on auction listings"); } - + // Check if listing is active if self.status != ListingStatus::Active { return Err("Cannot place bid on inactive listing"); } - + // Check if bid amount is higher than current price if bid.amount <= self.price { return Err("Bid amount must be higher than current price"); } - + // Check if there are existing bids and if the new bid is higher if let Some(highest_bid) = self.highest_bid() { if bid.amount <= highest_bid.amount { return Err("Bid amount must be higher than current highest bid"); } } - + // Add the bid self.bids.push(bid); - + // Update the current price to the new highest bid if let Some(highest_bid) = self.highest_bid() { self.price = highest_bid.amount; } - + Ok(self) } @@ -195,12 +194,12 @@ impl Listing { if self.status != ListingStatus::Active { return Err("Cannot complete sale for inactive listing"); } - + self.status = ListingStatus::Sold; self.buyer_id = Some(buyer_id.to_string()); self.sale_price = Some(sale_price); self.sold_at = Some(Utc::now()); - + // If this was an auction, accept the winning bid and reject others if self.listing_type == ListingType::Auction { for bid in &mut self.bids { @@ -211,7 +210,7 @@ impl Listing { } } } - + Ok(self) } @@ -220,16 +219,16 @@ impl Listing { if self.status != ListingStatus::Active { return Err("Cannot cancel inactive listing"); } - + self.status = ListingStatus::Cancelled; - + // Cancel all active bids for bid in &mut self.bids { if bid.status == BidStatus::Active { bid.status = BidStatus::Cancelled; } } - + Ok(self) } @@ -239,7 +238,7 @@ impl Listing { if let Some(expires_at) = self.expires_at { if Utc::now() > expires_at { self.status = ListingStatus::Expired; - + // Cancel all active bids for bid in &mut self.bids { if bid.status == BidStatus::Active { @@ -249,7 +248,7 @@ impl Listing { } } } - + self } diff --git a/heromodels/src/models/governance/proposal.rs b/heromodels/src/models/governance/proposal.rs index 748421c..7e73cb3 100644 --- a/heromodels/src/models/governance/proposal.rs +++ b/heromodels/src/models/governance/proposal.rs @@ -75,16 +75,15 @@ pub struct Ballot { } impl Ballot { - /// Create a new ballot + /// Create a new ballot with auto-generated ID /// /// # Arguments - /// * `id` - Optional ID for the ballot. If None, the ID will be auto-generated. /// * `user_id` - ID of the user who cast this ballot /// * `vote_option_id` - ID of the vote option chosen /// * `shares_count` - Number of shares/tokens/voting power - pub fn new(id: Option, user_id: u32, vote_option_id: u8, shares_count: i64) -> Self { + pub fn new(user_id: u32, vote_option_id: u8, shares_count: i64) -> Self { Self { - base_data: BaseModelData::new(id), + base_data: BaseModelData::new(), user_id, vote_option_id, shares_count, @@ -114,18 +113,17 @@ pub struct Proposal { } impl Proposal { - /// Create a new proposal + /// Create a new proposal with auto-generated ID /// /// # Arguments - /// * `id` - Optional ID for the proposal. If None, the ID will be auto-generated. /// * `creator_id` - ID of the user who created the proposal /// * `title` - Title of the proposal /// * `description` - Description of the proposal /// * `vote_start_date` - Date when voting starts /// * `vote_end_date` - Date when voting ends - pub fn new(id: Option, creator_id: impl ToString, title: impl ToString, description: impl ToString, vote_start_date: DateTime, vote_end_date: DateTime) -> Self { + pub fn new(creator_id: impl ToString, title: impl ToString, description: impl ToString, vote_start_date: DateTime, vote_end_date: DateTime) -> Self { Self { - base_data: BaseModelData::new(id), + base_data: BaseModelData::new(), creator_id: creator_id.to_string(), title: title.to_string(), description: description.to_string(), @@ -145,7 +143,7 @@ impl Proposal { self } - pub fn cast_vote(mut self, ballot_id: Option, user_id: u32, chosen_option_id: u8, shares: i64) -> Self { + pub fn cast_vote(mut self, user_id: u32, chosen_option_id: u8, shares: i64) -> Self { if self.vote_status != VoteEventStatus::Open { eprintln!("Voting is not open for proposal '{}'", self.title); return self; @@ -161,7 +159,7 @@ impl Proposal { } } - let new_ballot = Ballot::new(ballot_id, user_id, chosen_option_id, shares); + let new_ballot = Ballot::new(user_id, chosen_option_id, shares); self.ballots.push(new_ballot); if let Some(option) = self.options.iter_mut().find(|opt| opt.id == chosen_option_id) { diff --git a/heromodels/src/models/userexample/user.rs b/heromodels/src/models/userexample/user.rs index 8455aa3..c9f68f0 100644 --- a/heromodels/src/models/userexample/user.rs +++ b/heromodels/src/models/userexample/user.rs @@ -26,13 +26,10 @@ pub struct User { } impl User { - /// Create a new user - /// - /// # Arguments - /// * `id` - Optional ID for the user. If None, the ID will be auto-generated. - pub fn new(id: Option) -> Self { + /// Create a new user with auto-generated ID + pub fn new() -> Self { Self { - base_data: BaseModelData::new(id), + base_data: BaseModelData::new(), username: String::new(), email: String::new(), full_name: String::new(), diff --git a/heromodels_core/src/lib.rs b/heromodels_core/src/lib.rs index db63ccf..8386423 100644 --- a/heromodels_core/src/lib.rs +++ b/heromodels_core/src/lib.rs @@ -59,21 +59,11 @@ pub trait Model: } /// Get the unique ID for this model - /// Returns 0 if the ID is None 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: Option) -> Self - where - Self: Sized, - { - self.base_data_mut().id = id; - self - } - /// Build the model, updating the modified timestamp fn build(mut self) -> Self where @@ -98,8 +88,8 @@ pub trait Index { /// Base struct that all models should include #[derive(Debug, Clone, Serialize, Deserialize)] pub struct BaseModelData { - /// Unique incremental ID per circle - pub id: Option, + /// Unique incremental ID - will be auto-generated by OurDB + pub id: u32, /// Unix epoch timestamp for creation time pub created_at: i64, @@ -112,11 +102,12 @@ pub struct BaseModelData { } impl BaseModelData { - /// Create a new BaseModelData instance - pub fn new(id: Option) -> Self { + /// Create a new BaseModelData instance with ID set to 0 + /// The ID will be auto-generated by OurDB when the model is saved + pub fn new() -> Self { let now = chrono::Utc::now().timestamp(); Self { - id, + id: 0, // This will be replaced by OurDB with an auto-generated ID created_at: now, modified_at: now, comments: Vec::new(), @@ -124,8 +115,8 @@ impl BaseModelData { } /// Create a new BaseModelDataBuilder - pub fn builder(id: Option) -> BaseModelDataBuilder { - BaseModelDataBuilder::new(id) + pub fn builder() -> BaseModelDataBuilder { + BaseModelDataBuilder::new() } /// Add a comment to this model @@ -144,11 +135,15 @@ impl BaseModelData { pub fn update_modified(&mut self) { self.modified_at = chrono::Utc::now().timestamp(); } + + /// Update the ID of this model + pub fn update_id(&mut self, id: u32) { + self.id = id; + } } /// Builder for BaseModelData pub struct BaseModelDataBuilder { - id: Option, created_at: Option, modified_at: Option, comments: Vec, @@ -156,9 +151,8 @@ pub struct BaseModelDataBuilder { impl BaseModelDataBuilder { /// Create a new BaseModelDataBuilder - pub fn new(id: Option) -> Self { + pub fn new() -> Self { Self { - id, created_at: None, modified_at: None, comments: Vec::new(), @@ -193,7 +187,7 @@ impl BaseModelDataBuilder { pub fn build(self) -> BaseModelData { let now = chrono::Utc::now().timestamp(); BaseModelData { - id: self.id, + id: 0, // This will be replaced by OurDB with an auto-generated ID created_at: self.created_at.unwrap_or(now), modified_at: self.modified_at.unwrap_or(now), comments: self.comments,