Merge branch 'development_add_incremental_mode_to_heromodels'
This commit is contained in:
@@ -32,8 +32,16 @@ where
|
||||
/// 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>>;
|
||||
|
||||
/// Store an item in the DB.
|
||||
fn set(&self, value: &V) -> Result<(), Error<Self::Error>>;
|
||||
/// Store an item in the DB and return the assigned ID and the updated model.
|
||||
///
|
||||
/// # Important Notes
|
||||
/// - This method does not modify the original model passed as an argument.
|
||||
/// - For new models (with ID 0), an ID will be auto-generated by OurDB.
|
||||
/// - The returned model will have the correct ID and should be used instead of the original model.
|
||||
/// - The original model should not be used after calling this method, as it may have
|
||||
/// an inconsistent state compared to what's in the database.
|
||||
/// - ID 0 is reserved for new models and should not be used for existing models.
|
||||
fn set(&self, value: &V) -> Result<(u32, V), Error<Self::Error>>;
|
||||
|
||||
/// Delete all items from the db with a given index.
|
||||
fn delete<I, Q>(&self, key: &Q) -> Result<(), Error<Self::Error>>
|
||||
@@ -45,8 +53,11 @@ where
|
||||
/// Delete an object with a given ID
|
||||
fn delete_by_id(&self, id: u32) -> Result<(), Error<Self::Error>>;
|
||||
|
||||
/// Get all objects from the colelction
|
||||
/// Get all objects from the collection
|
||||
fn get_all(&self) -> Result<Vec<V>, Error<Self::Error>>;
|
||||
|
||||
/// Begin a transaction for this collection
|
||||
fn begin_transaction(&self) -> Result<Box<dyn Transaction<Error = Self::Error>>, Error<Self::Error>>;
|
||||
}
|
||||
|
||||
/// Errors returned by the DB implementation
|
||||
@@ -58,6 +69,14 @@ pub enum Error<E> {
|
||||
Decode(bincode::error::DecodeError),
|
||||
/// Error encoding a model for storage
|
||||
Encode(bincode::error::EncodeError),
|
||||
/// Invalid ID used (e.g., using ID 0 for an existing model)
|
||||
InvalidId(String),
|
||||
/// ID mismatch (e.g., expected ID 5 but got ID 6)
|
||||
IdMismatch(String),
|
||||
/// ID collision (e.g., trying to create a model with an ID that already exists)
|
||||
IdCollision(String),
|
||||
/// Type error (e.g., trying to get a model of the wrong type)
|
||||
TypeError,
|
||||
}
|
||||
|
||||
impl<E> From<bincode::error::DecodeError> for Error<E> {
|
||||
@@ -80,4 +99,20 @@ impl<E: std::fmt::Debug + std::fmt::Display> std::fmt::Display for Error<E> {
|
||||
Error::Encode(e) => write!(f, "Failed to encode model: {}", e),
|
||||
}
|
||||
}
|
||||
/// A transaction that can be committed or rolled back
|
||||
pub trait Transaction {
|
||||
/// Error type for transaction operations
|
||||
type Error: std::fmt::Debug;
|
||||
|
||||
/// Begin a transaction
|
||||
fn begin(&self) -> Result<(), Error<Self::Error>>;
|
||||
|
||||
/// Commit a transaction
|
||||
fn commit(&self) -> Result<(), Error<Self::Error>>;
|
||||
|
||||
/// Roll back a transaction
|
||||
fn rollback(&self) -> Result<(), Error<Self::Error>>;
|
||||
|
||||
/// Check if a transaction is active
|
||||
fn is_active(&self) -> bool;
|
||||
}
|
||||
|
@@ -1,3 +1,4 @@
|
||||
use crate::db::Transaction;
|
||||
use heromodels_core::{Index, Model};
|
||||
use ourdb::OurDBSetArgs;
|
||||
use serde::Deserialize;
|
||||
@@ -6,36 +7,123 @@ use std::{
|
||||
borrow::Borrow,
|
||||
collections::HashSet,
|
||||
path::PathBuf,
|
||||
sync::{Arc, Mutex},
|
||||
sync::{
|
||||
Arc, Mutex,
|
||||
atomic::{AtomicU32, Ordering},
|
||||
},
|
||||
};
|
||||
|
||||
/// Configuration for custom ID sequences
|
||||
pub struct IdSequence {
|
||||
/// The starting ID for the sequence
|
||||
start: u32,
|
||||
/// The increment for the sequence
|
||||
increment: u32,
|
||||
/// The current ID in the sequence
|
||||
current: AtomicU32,
|
||||
}
|
||||
|
||||
// Implement Clone manually since AtomicU32 doesn't implement Clone
|
||||
impl Clone for IdSequence {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
start: self.start,
|
||||
increment: self.increment,
|
||||
current: AtomicU32::new(self.current.load(Ordering::SeqCst)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl IdSequence {
|
||||
/// Create a new ID sequence with default values (start=1, increment=1)
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
start: 1,
|
||||
increment: 1,
|
||||
current: AtomicU32::new(1),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new ID sequence with custom values
|
||||
pub fn with_config(start: u32, increment: u32) -> Self {
|
||||
Self {
|
||||
start,
|
||||
increment,
|
||||
current: AtomicU32::new(start),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the next ID in the sequence
|
||||
pub fn next_id(&self) -> u32 {
|
||||
self.current.fetch_add(self.increment, Ordering::SeqCst)
|
||||
}
|
||||
|
||||
/// Reset the sequence to its starting value
|
||||
pub fn reset(&self) {
|
||||
self.current.store(self.start, Ordering::SeqCst);
|
||||
}
|
||||
|
||||
/// Set the current ID in the sequence
|
||||
pub fn set_current(&self, id: u32) {
|
||||
self.current.store(id, Ordering::SeqCst);
|
||||
}
|
||||
}
|
||||
|
||||
const BINCODE_CONFIG: bincode::config::Configuration = bincode::config::standard();
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct OurDB {
|
||||
index: Arc<Mutex<tst::TST>>,
|
||||
data: Arc<Mutex<ourdb::OurDB>>,
|
||||
// Mutex for ID generation to prevent race conditions
|
||||
id_lock: Arc<Mutex<()>>,
|
||||
// Custom ID sequence configuration
|
||||
id_sequence: Arc<IdSequence>,
|
||||
}
|
||||
|
||||
impl OurDB {
|
||||
/// Create a new instance of ourdb
|
||||
/// Create a new instance of ourdb with default ID sequence (start=1, increment=1)
|
||||
pub fn new(path: impl Into<PathBuf>, reset: bool) -> Result<Self, tst::Error> {
|
||||
Self::with_id_sequence(path, reset, IdSequence::new())
|
||||
}
|
||||
|
||||
/// Create a new instance of ourdb with a custom ID sequence
|
||||
pub fn with_id_sequence(
|
||||
path: impl Into<PathBuf>,
|
||||
reset: bool,
|
||||
id_sequence: IdSequence,
|
||||
) -> Result<Self, tst::Error> {
|
||||
let mut base_path = path.into();
|
||||
let mut data_path = base_path.clone();
|
||||
base_path.push("index");
|
||||
data_path.push("data");
|
||||
|
||||
let data_db = ourdb::OurDB::new(ourdb::OurDBConfig {
|
||||
incremental_mode: false,
|
||||
let mut data_db = ourdb::OurDB::new(ourdb::OurDBConfig {
|
||||
incremental_mode: true,
|
||||
path: data_path,
|
||||
file_size: None,
|
||||
keysize: None,
|
||||
reset: Some(reset),
|
||||
})?;
|
||||
let index_db = tst::TST::new(base_path.to_str().expect("Path is valid UTF-8"), reset)?;
|
||||
// If we're resetting the database, also reset the ID sequence
|
||||
if reset {
|
||||
id_sequence.reset();
|
||||
} else {
|
||||
// Otherwise, try to find the highest ID in the database and update the sequence
|
||||
// Since OurDB doesn't have a get_highest_id method, we'll use get_next_id instead
|
||||
// This is not ideal, but it's the best we can do with the current API
|
||||
let highest_id = data_db.get_next_id().unwrap_or(id_sequence.start);
|
||||
if highest_id >= id_sequence.start {
|
||||
id_sequence.set_current(highest_id + id_sequence.increment);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(OurDB {
|
||||
index: Arc::new(Mutex::new(index_db)),
|
||||
data: Arc::new(Mutex::new(data_db)),
|
||||
id_lock: Arc::new(Mutex::new(())),
|
||||
id_sequence: Arc::new(id_sequence),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -56,6 +144,20 @@ where
|
||||
{
|
||||
type Error = tst::Error;
|
||||
|
||||
/// Begin a transaction for this collection
|
||||
fn begin_transaction(
|
||||
&self,
|
||||
) -> Result<Box<dyn super::Transaction<Error = Self::Error>>, super::Error<Self::Error>> {
|
||||
// Create a new transaction
|
||||
let transaction = OurDBTransaction::new();
|
||||
|
||||
// Begin the transaction
|
||||
transaction.begin()?;
|
||||
|
||||
// Return the transaction
|
||||
Ok(Box::new(transaction))
|
||||
}
|
||||
|
||||
fn get<I, Q>(&self, key: &Q) -> Result<Vec<M>, super::Error<Self::Error>>
|
||||
where
|
||||
I: Index<Model = M>,
|
||||
@@ -87,73 +189,168 @@ where
|
||||
Self::get_ourdb_value(&mut data_db, id)
|
||||
}
|
||||
|
||||
fn set(&self, value: &M) -> Result<(), super::Error<Self::Error>> {
|
||||
// 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<M> = Self::get_ourdb_value(&mut data_db, value.get_id())?;
|
||||
let (indices_to_delete, indices_to_add) = if let Some(old_obj) = old_obj {
|
||||
let mut indices_to_delete = vec![];
|
||||
let mut indices_to_add = vec![];
|
||||
let old_indices = old_obj.db_keys();
|
||||
let new_indices = value.db_keys();
|
||||
for old_index in old_indices {
|
||||
for new_index in &new_indices {
|
||||
if old_index.name == new_index.name {
|
||||
if old_index.value != new_index.value {
|
||||
// different value now, remove index
|
||||
indices_to_delete.push(old_index);
|
||||
// and later add the new one
|
||||
indices_to_add.push(new_index.clone());
|
||||
break;
|
||||
fn set(&self, value: &M) -> Result<(u32, M), super::Error<Self::Error>> {
|
||||
// For now, we'll skip using transactions to avoid type inference issues
|
||||
// In a real implementation, you would use a proper transaction mechanism
|
||||
|
||||
// Use a result variable to track success/failure
|
||||
let 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<M> = Self::get_ourdb_value(&mut data_db, value.get_id())?;
|
||||
let (indices_to_delete, indices_to_add) = if let Some(ref old_obj) = old_obj {
|
||||
let mut indices_to_delete = vec![];
|
||||
let mut indices_to_add = vec![];
|
||||
let old_indices = old_obj.db_keys();
|
||||
let new_indices = value.db_keys();
|
||||
for old_index in old_indices {
|
||||
for new_index in &new_indices {
|
||||
if old_index.name == new_index.name {
|
||||
if old_index.value != new_index.value {
|
||||
// different value now, remove index
|
||||
indices_to_delete.push(old_index);
|
||||
// and later add the new one
|
||||
indices_to_add.push(new_index.clone());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// NOTE: we assume here that the index keys are stable, i.e. new index fields don't appear
|
||||
// and existing ones don't dissapear
|
||||
(indices_to_delete, indices_to_add)
|
||||
} else {
|
||||
(vec![], value.db_keys())
|
||||
};
|
||||
|
||||
let mut index_db = self.index.lock().expect("can lock index db");
|
||||
// First delete old indices which need to change
|
||||
for old_index in indices_to_delete {
|
||||
let key = Self::index_key(M::db_prefix(), old_index.name, &old_index.value);
|
||||
let raw_ids = index_db.get(&key)?;
|
||||
let (mut ids, _): (HashSet<u32>, _) =
|
||||
bincode::serde::decode_from_slice(&raw_ids, BINCODE_CONFIG)?;
|
||||
ids.remove(&value.get_id());
|
||||
if ids.is_empty() {
|
||||
// This was the last ID with this index value, remove index entirely
|
||||
index_db.delete(&key)?;
|
||||
// NOTE: we assume here that the index keys are stable, i.e. new index fields don't appear
|
||||
// and existing ones don't dissapear
|
||||
(indices_to_delete, indices_to_add)
|
||||
} else {
|
||||
// There are still objects left with this index value, write back updated set
|
||||
let raw_ids = bincode::serde::encode_to_vec(ids, BINCODE_CONFIG)?;
|
||||
index_db.set(&key, raw_ids)?;
|
||||
(vec![], value.db_keys())
|
||||
};
|
||||
|
||||
let mut index_db = self.index.lock().expect("can lock index db");
|
||||
// First delete old indices which need to change
|
||||
for old_index in indices_to_delete {
|
||||
let key = Self::index_key(M::db_prefix(), old_index.name, &old_index.value);
|
||||
let raw_ids = index_db.get(&key)?;
|
||||
let (mut ids, _): (HashSet<u32>, _) =
|
||||
bincode::serde::decode_from_slice(&raw_ids, BINCODE_CONFIG)?;
|
||||
ids.remove(&value.get_id());
|
||||
if ids.is_empty() {
|
||||
// This was the last ID with this index value, remove index entirely
|
||||
index_db.delete(&key)?;
|
||||
} else {
|
||||
// There are still objects left with this index value, write back updated set
|
||||
let raw_ids = bincode::serde::encode_to_vec(ids, BINCODE_CONFIG)?;
|
||||
index_db.set(&key, raw_ids)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// set or update the object
|
||||
let v = bincode::serde::encode_to_vec(value, BINCODE_CONFIG)?;
|
||||
let id = value.get_id();
|
||||
data_db.set(OurDBSetArgs {
|
||||
id: Some(id),
|
||||
data: &v,
|
||||
})?;
|
||||
// Get the current ID
|
||||
let id = value.get_id();
|
||||
|
||||
// Now add the new indices
|
||||
for index_key in indices_to_add {
|
||||
let key = Self::index_key(M::db_prefix(), index_key.name, &index_key.value);
|
||||
// Load the existing id set for the index or create a new set
|
||||
let mut existing_ids =
|
||||
Self::get_tst_value::<HashSet<u32>>(&mut index_db, &key)?.unwrap_or_default();
|
||||
existing_ids.insert(id);
|
||||
let encoded_ids = bincode::serde::encode_to_vec(existing_ids, BINCODE_CONFIG)?;
|
||||
index_db.set(&key, encoded_ids)?;
|
||||
}
|
||||
// Validate that ID 0 is only used for new models
|
||||
if id == 0 {
|
||||
// Check if this model already exists in the database
|
||||
// If it does, it's an error to use ID 0 for an existing model
|
||||
if let Some(existing) = Self::get_ourdb_value::<M>(&mut data_db, id)? {
|
||||
return Err(super::Error::InvalidId(format!(
|
||||
"ID 0 is reserved for new models. Found existing model with ID 0: {:?}",
|
||||
existing
|
||||
)));
|
||||
}
|
||||
} else {
|
||||
// Validate that IDs > 0 are only used for existing models
|
||||
// If the model doesn't exist, it's an error to use a specific ID
|
||||
if id > 0 && Self::get_ourdb_value::<M>(&mut data_db, id)?.is_none() {
|
||||
return Err(super::Error::InvalidId(format!(
|
||||
"ID {} does not exist in the database. Use ID 0 for new models.",
|
||||
id
|
||||
)));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
// Check for ID collisions when manually setting an ID
|
||||
if id > 0 && Self::get_ourdb_value::<M>(&mut data_db, id)?.is_some() {
|
||||
// This is only an error if we're trying to create a new model with this ID
|
||||
// If we're updating an existing model, this is fine
|
||||
if old_obj.is_none() {
|
||||
return Err(super::Error::IdCollision(format!(
|
||||
"ID {} already exists in the database",
|
||||
id
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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) };
|
||||
|
||||
// Thread-safe approach for handling ID assignment
|
||||
let assigned_id = if id == 0 {
|
||||
// For new objects, serialize with ID 0
|
||||
let v = bincode::serde::encode_to_vec(value, BINCODE_CONFIG)?;
|
||||
|
||||
// Save to OurDB with id_param = None to let OurDB auto-generate the ID
|
||||
let assigned_id = data_db.set(OurDBSetArgs {
|
||||
id: id_param,
|
||||
data: &v,
|
||||
})?;
|
||||
|
||||
// Now that we have the actual assigned ID, create a new model with the correct ID
|
||||
// and save it again to ensure the serialized data contains the correct ID
|
||||
let mut value_clone = value.clone();
|
||||
let base_data = value_clone.base_data_mut();
|
||||
base_data.id = assigned_id;
|
||||
|
||||
// Serialize the updated model
|
||||
let v = bincode::serde::encode_to_vec(&value_clone, BINCODE_CONFIG)?;
|
||||
|
||||
// Save again with the explicit ID
|
||||
data_db.set(OurDBSetArgs {
|
||||
id: Some(assigned_id),
|
||||
data: &v,
|
||||
})?;
|
||||
|
||||
// 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 {
|
||||
let key = Self::index_key(M::db_prefix(), index_key.name, &index_key.value);
|
||||
// Load the existing id set for the index or create a new set
|
||||
let mut existing_ids =
|
||||
Self::get_tst_value::<HashSet<u32>>(&mut index_db, &key)?.unwrap_or_default();
|
||||
// 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)?;
|
||||
}
|
||||
|
||||
// Get the updated model from the database
|
||||
let updated_model =
|
||||
Self::get_ourdb_value::<M>(&mut data_db, assigned_id)?.ok_or_else(|| {
|
||||
super::Error::InvalidId(format!(
|
||||
"Failed to retrieve model with ID {} after saving",
|
||||
assigned_id
|
||||
))
|
||||
})?;
|
||||
|
||||
// Return the assigned ID and the updated model
|
||||
Ok((assigned_id, updated_model))
|
||||
})();
|
||||
|
||||
// Return the result directly
|
||||
// In a real implementation, you would commit or rollback the transaction here
|
||||
result
|
||||
}
|
||||
|
||||
fn delete<I, Q>(&self, key: &Q) -> Result<(), super::Error<Self::Error>>
|
||||
@@ -295,6 +492,29 @@ impl OurDB {
|
||||
format!("{collection}::{index}::{value}")
|
||||
}
|
||||
|
||||
/// Reserve an ID for future use
|
||||
pub fn reserve_id(&self) -> u32 {
|
||||
// Acquire the ID lock to prevent race conditions
|
||||
let _id_lock = self.id_lock.lock().expect("Failed to acquire ID lock");
|
||||
|
||||
// Get the next ID from our custom sequence
|
||||
self.id_sequence.next_id()
|
||||
}
|
||||
|
||||
/// Reserve multiple IDs for future use
|
||||
pub fn reserve_ids(&self, count: u32) -> Vec<u32> {
|
||||
// Acquire the ID lock to prevent race conditions
|
||||
let _id_lock = self.id_lock.lock().expect("Failed to acquire ID lock");
|
||||
|
||||
// Get the next IDs from our custom sequence
|
||||
let mut ids = Vec::with_capacity(count as usize);
|
||||
for _ in 0..count {
|
||||
ids.push(self.id_sequence.next_id());
|
||||
}
|
||||
|
||||
ids
|
||||
}
|
||||
|
||||
/// Wrapper to load values from ourdb and transform a not found error in to Ok(None)
|
||||
fn get_ourdb_value<V>(
|
||||
data: &mut ourdb::OurDB,
|
||||
@@ -342,3 +562,86 @@ impl From<ourdb::Error> for super::Error<tst::Error> {
|
||||
super::Error::DB(tst::Error::OurDB(value))
|
||||
}
|
||||
}
|
||||
|
||||
/// A transaction for OurDB
|
||||
///
|
||||
/// Note: This is a simplified implementation that doesn't actually provide
|
||||
/// ACID guarantees. In a real implementation, you would need to use a proper
|
||||
/// transaction mechanism provided by the underlying database.
|
||||
///
|
||||
/// This struct implements Drop to ensure that transactions are properly closed.
|
||||
/// If a transaction is not explicitly committed or rolled back, it will be
|
||||
/// rolled back when the transaction is dropped.
|
||||
struct OurDBTransaction {
|
||||
active: std::sync::atomic::AtomicBool,
|
||||
}
|
||||
|
||||
impl OurDBTransaction {
|
||||
/// Create a new transaction
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
active: std::sync::atomic::AtomicBool::new(false),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for OurDBTransaction {
|
||||
fn drop(&mut self) {
|
||||
// If the transaction is still active when dropped, roll it back
|
||||
if self.active.load(std::sync::atomic::Ordering::SeqCst) {
|
||||
// We can't return an error from drop, so we just log it
|
||||
eprintln!(
|
||||
"Warning: Transaction was dropped without being committed or rolled back. Rolling back automatically."
|
||||
);
|
||||
self.active
|
||||
.store(false, std::sync::atomic::Ordering::SeqCst);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl super::Transaction for OurDBTransaction {
|
||||
type Error = tst::Error;
|
||||
|
||||
/// Begin the transaction
|
||||
fn begin(&self) -> Result<(), super::Error<Self::Error>> {
|
||||
// In a real implementation, you would start a transaction in the underlying database
|
||||
// For now, we just set the active flag
|
||||
self.active.store(true, std::sync::atomic::Ordering::SeqCst);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Commit the transaction
|
||||
fn commit(&self) -> Result<(), super::Error<Self::Error>> {
|
||||
// In a real implementation, you would commit the transaction in the underlying database
|
||||
// For now, we just check if the transaction is active
|
||||
if !self.active.load(std::sync::atomic::Ordering::SeqCst) {
|
||||
return Err(super::Error::InvalidId(
|
||||
"Cannot commit an inactive transaction".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
self.active
|
||||
.store(false, std::sync::atomic::Ordering::SeqCst);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Roll back the transaction
|
||||
fn rollback(&self) -> Result<(), super::Error<Self::Error>> {
|
||||
// In a real implementation, you would roll back the transaction in the underlying database
|
||||
// For now, we just check if the transaction is active
|
||||
if !self.active.load(std::sync::atomic::Ordering::SeqCst) {
|
||||
return Err(super::Error::InvalidId(
|
||||
"Cannot roll back an inactive transaction".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
self.active
|
||||
.store(false, std::sync::atomic::Ordering::SeqCst);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Check if the transaction is active
|
||||
fn is_active(&self) -> bool {
|
||||
self.active.load(std::sync::atomic::Ordering::SeqCst)
|
||||
}
|
||||
}
|
||||
|
@@ -60,8 +60,12 @@ pub struct Event {
|
||||
|
||||
impl Event {
|
||||
/// Creates a new event
|
||||
pub fn new(id: i64) -> Self {
|
||||
Self {
|
||||
pub fn new(
|
||||
id: i64,
|
||||
title: impl ToString,
|
||||
start_time: DateTime<Utc>,
|
||||
end_time: DateTime<Utc>,
|
||||
) -> Self {
|
||||
base_data: BaseModelData::new(id as u32),
|
||||
title: String::new(),
|
||||
description: None,
|
||||
@@ -114,7 +118,11 @@ impl Event {
|
||||
}
|
||||
|
||||
/// Reschedules the event to new start and end times
|
||||
pub fn reschedule(mut self, new_start_time: DateTime<Utc>, new_end_time: DateTime<Utc>) -> Self {
|
||||
pub fn reschedule(
|
||||
mut self,
|
||||
new_start_time: DateTime<Utc>,
|
||||
new_end_time: DateTime<Utc>,
|
||||
) -> Self {
|
||||
// Basic validation: end_time should be after start_time
|
||||
if new_end_time > new_start_time {
|
||||
self.start_time = new_start_time;
|
||||
@@ -148,11 +156,20 @@ pub struct Calendar {
|
||||
}
|
||||
|
||||
impl Calendar {
|
||||
/// Creates a new calendar
|
||||
pub fn new(id: u32) -> Self {
|
||||
/// Creates a new calendar with auto-generated ID
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `id` - Optional ID for the calendar (use None for auto-generated ID)
|
||||
/// * `name` - Name of the calendar
|
||||
pub fn new(id: Option<u32>, name: impl ToString) -> Self {
|
||||
let mut base_data = BaseModelData::new();
|
||||
if let Some(id) = id {
|
||||
base_data.update_id(id);
|
||||
}
|
||||
|
||||
Self {
|
||||
base_data: BaseModelData::new(id as u32),
|
||||
name: String::new(),
|
||||
base_data,
|
||||
name: name.to_string(),
|
||||
description: None,
|
||||
events: Vec::new(),
|
||||
}
|
||||
|
@@ -13,10 +13,10 @@ pub struct Comment {
|
||||
}
|
||||
|
||||
impl Comment {
|
||||
/// Create a new comment
|
||||
pub fn new(id: u32) -> 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(),
|
||||
}
|
||||
|
@@ -4,6 +4,8 @@ use serde::{Deserialize, Serialize};
|
||||
use rhai::{CustomType, TypeBuilder};
|
||||
use heromodels_derive::model;
|
||||
use heromodels_core::BaseModelData;
|
||||
use heromodels_derive::model;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::asset::Asset;
|
||||
|
||||
@@ -22,18 +24,32 @@ pub struct Account {
|
||||
}
|
||||
|
||||
impl Account {
|
||||
/// Create a new account
|
||||
/// Create a new account with auto-generated ID
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `id` - Optional ID for the account (use None for auto-generated ID)
|
||||
/// * `name` - Name of the account
|
||||
/// * `user_id` - ID of the user who owns the account
|
||||
/// * `description` - Description of the account
|
||||
/// * `ledger` - Ledger/blockchain where the account is located
|
||||
/// * `address` - Address of the account on the blockchain
|
||||
/// * `pubkey` - Public key
|
||||
pub fn new(
|
||||
id: u32,
|
||||
name: impl ToString,
|
||||
user_id: u32,
|
||||
description: impl ToString,
|
||||
ledger: impl ToString,
|
||||
address: impl ToString,
|
||||
pubkey: impl ToString
|
||||
id: Option<u32>,
|
||||
name: impl ToString,
|
||||
user_id: u32,
|
||||
description: impl ToString,
|
||||
ledger: impl ToString,
|
||||
address: impl ToString,
|
||||
pubkey: impl ToString,
|
||||
) -> Self {
|
||||
let mut base_data = BaseModelData::new();
|
||||
if let Some(id) = id {
|
||||
base_data.update_id(id);
|
||||
}
|
||||
|
||||
Self {
|
||||
base_data: BaseModelData::new(id),
|
||||
base_data,
|
||||
name: name.to_string(),
|
||||
user_id,
|
||||
description: description.to_string(),
|
||||
|
@@ -4,6 +4,8 @@ use serde::{Deserialize, Serialize};
|
||||
use rhai::{CustomType, TypeBuilder};
|
||||
use heromodels_derive::model;
|
||||
use heromodels_core::BaseModelData;
|
||||
use heromodels_derive::model;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// AssetType defines the type of blockchain asset
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
@@ -25,18 +27,27 @@ impl Default for AssetType {
|
||||
#[model] // Has base.Base in V spec
|
||||
pub struct Asset {
|
||||
pub base_data: BaseModelData,
|
||||
pub name: String, // Name of the asset
|
||||
pub description: String, // Description of the asset
|
||||
pub amount: f64, // Amount of the asset
|
||||
pub address: String, // Address of the asset on the blockchain or bank
|
||||
pub asset_type: AssetType, // Type of the asset
|
||||
pub decimals: u8, // Number of decimals of the asset
|
||||
pub name: String, // Name of the asset
|
||||
pub description: String, // Description of the asset
|
||||
pub amount: f64, // Amount of the asset
|
||||
pub address: String, // Address of the asset on the blockchain or bank
|
||||
pub asset_type: AssetType, // Type of the asset
|
||||
pub decimals: u8, // Number of decimals of the asset
|
||||
}
|
||||
|
||||
impl Asset {
|
||||
/// Create a new asset
|
||||
/// Create a new asset with auto-generated ID
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `id` - Optional ID for the asset (use None for auto-generated ID)
|
||||
/// * `name` - Name of the asset
|
||||
/// * `description` - Description of the asset
|
||||
/// * `amount` - Amount of the asset
|
||||
/// * `address` - Address of the asset on the blockchain or bank
|
||||
/// * `asset_type` - Type of the asset
|
||||
/// * `decimals` - Number of decimals of the asset
|
||||
pub fn new(
|
||||
id: u32,
|
||||
id: Option<u32>,
|
||||
name: impl ToString,
|
||||
description: impl ToString,
|
||||
amount: f64,
|
||||
@@ -44,8 +55,13 @@ impl Asset {
|
||||
asset_type: AssetType,
|
||||
decimals: u8,
|
||||
) -> Self {
|
||||
let mut base_data = BaseModelData::new();
|
||||
if let Some(id) = id {
|
||||
base_data.update_id(id);
|
||||
}
|
||||
|
||||
Self {
|
||||
base_data: BaseModelData::new(id),
|
||||
base_data,
|
||||
name: name.to_string(),
|
||||
description: description.to_string(),
|
||||
amount,
|
||||
@@ -73,14 +89,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(())
|
||||
}
|
||||
}
|
||||
|
@@ -5,6 +5,8 @@ use rhai::{CustomType, TypeBuilder};
|
||||
use chrono::{DateTime, Utc};
|
||||
use heromodels_core::BaseModelData;
|
||||
use heromodels_derive::model;
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::asset::AssetType;
|
||||
|
||||
@@ -55,11 +57,11 @@ impl Default for BidStatus {
|
||||
/// Bid represents a bid on an auction listing
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, CustomType)]
|
||||
pub struct Bid {
|
||||
pub listing_id: String, // ID of the listing this bid belongs to
|
||||
pub bidder_id: u32, // ID of the user who placed the bid
|
||||
pub amount: f64, // Bid amount
|
||||
pub currency: String, // Currency of the bid
|
||||
pub status: BidStatus, // Status of the bid
|
||||
pub listing_id: String, // ID of the listing this bid belongs to
|
||||
pub bidder_id: u32, // ID of the user who placed the bid
|
||||
pub amount: f64, // Bid amount
|
||||
pub currency: String, // Currency of the bid
|
||||
pub status: BidStatus, // Status of the bid
|
||||
pub created_at: DateTime<Utc>, // When the bid was created
|
||||
}
|
||||
|
||||
@@ -98,7 +100,7 @@ pub struct Listing {
|
||||
pub asset_id: String,
|
||||
pub asset_type: AssetType,
|
||||
pub seller_id: String,
|
||||
pub price: f64, // Initial price for fixed price, or starting price for auction
|
||||
pub price: f64, // Initial price for fixed price, or starting price for auction
|
||||
pub currency: String,
|
||||
pub listing_type: ListingType,
|
||||
pub status: ListingStatus,
|
||||
@@ -112,9 +114,23 @@ pub struct Listing {
|
||||
}
|
||||
|
||||
impl Listing {
|
||||
/// Create a new listing
|
||||
/// Create a new listing with auto-generated ID
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `id` - Optional ID for the listing (use None for auto-generated ID)
|
||||
/// * `title` - Title of the listing
|
||||
/// * `description` - Description of the listing
|
||||
/// * `asset_id` - ID of the asset being listed
|
||||
/// * `asset_type` - Type of the asset
|
||||
/// * `seller_id` - ID of the seller
|
||||
/// * `price` - Initial price for fixed price, or starting price for auction
|
||||
/// * `currency` - Currency of the price
|
||||
/// * `listing_type` - Type of the listing
|
||||
/// * `expires_at` - Optional expiration date
|
||||
/// * `tags` - Tags for the listing
|
||||
/// * `image_url` - Optional image URL
|
||||
pub fn new(
|
||||
id: u32,
|
||||
id: Option<u32>,
|
||||
title: impl ToString,
|
||||
description: impl ToString,
|
||||
asset_id: impl ToString,
|
||||
@@ -127,8 +143,13 @@ impl Listing {
|
||||
tags: Vec<String>,
|
||||
image_url: Option<impl ToString>,
|
||||
) -> Self {
|
||||
let mut base_data = BaseModelData::new();
|
||||
if let Some(id) = id {
|
||||
base_data.update_id(id);
|
||||
}
|
||||
|
||||
Self {
|
||||
base_data: BaseModelData::new(id),
|
||||
base_data,
|
||||
title: title.to_string(),
|
||||
description: description.to_string(),
|
||||
asset_id: asset_id.to_string(),
|
||||
@@ -154,32 +175,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)
|
||||
}
|
||||
|
||||
@@ -192,27 +213,33 @@ impl Listing {
|
||||
}
|
||||
|
||||
/// Complete a sale (fixed price or auction)
|
||||
pub fn complete_sale(mut self, buyer_id: impl ToString, sale_price: f64) -> Result<Self, &'static str> {
|
||||
pub fn complete_sale(
|
||||
mut self,
|
||||
buyer_id: impl ToString,
|
||||
sale_price: f64,
|
||||
) -> Result<Self, &'static str> {
|
||||
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 {
|
||||
if bid.bidder_id.to_string() == self.buyer_id.as_ref().unwrap().to_string() && bid.amount == sale_price {
|
||||
if bid.bidder_id.to_string() == self.buyer_id.as_ref().unwrap().to_string()
|
||||
&& bid.amount == sale_price
|
||||
{
|
||||
bid.status = BidStatus::Accepted;
|
||||
} else {
|
||||
bid.status = BidStatus::Rejected;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
@@ -221,16 +248,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)
|
||||
}
|
||||
|
||||
@@ -240,7 +267,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 {
|
||||
@@ -250,7 +277,7 @@ impl Listing {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
|
@@ -1,10 +1,10 @@
|
||||
// heromodels/src/models/governance/proposal.rs
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use heromodels_derive::model; // For #[model]
|
||||
use rhai_autobind_macros::rhai_model_export;
|
||||
use rhai::{CustomType, TypeBuilder};
|
||||
use rhai_autobind_macros::rhai_model_export;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use heromodels_core::BaseModelData;
|
||||
|
||||
@@ -13,11 +13,11 @@ use heromodels_core::BaseModelData;
|
||||
/// ProposalStatus defines the lifecycle status of a governance proposal itself
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub enum ProposalStatus {
|
||||
Draft, // Proposal is being prepared
|
||||
Active, // Proposal is active
|
||||
Approved, // Proposal has been formally approved
|
||||
Rejected, // Proposal has been formally rejected
|
||||
Cancelled,// Proposal was cancelled
|
||||
Draft, // Proposal is being prepared
|
||||
Active, // Proposal is active
|
||||
Approved, // Proposal has been formally approved
|
||||
Rejected, // Proposal has been formally rejected
|
||||
Cancelled, // Proposal was cancelled
|
||||
}
|
||||
|
||||
impl Default for ProposalStatus {
|
||||
@@ -26,7 +26,6 @@ impl Default for ProposalStatus {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// VoteEventStatus represents the status of the voting process for a proposal
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub enum VoteEventStatus {
|
||||
@@ -46,19 +45,21 @@ impl Default for VoteEventStatus {
|
||||
/// VoteOption represents a specific choice that can be voted on
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, CustomType)]
|
||||
pub struct VoteOption {
|
||||
pub id: u8, // Simple identifier for this option
|
||||
pub text: String, // Descriptive text of the option
|
||||
pub count: i64, // How many votes this option has received
|
||||
pub min_valid: Option<i64>, // Optional: minimum votes needed
|
||||
pub id: u8, // Simple identifier for this option
|
||||
pub text: String, // Descriptive text of the option
|
||||
pub count: i64, // How many votes this option has received
|
||||
pub min_valid: Option<i64>, // Optional: minimum votes needed,
|
||||
pub comment: Option<String>, // Optional: comment
|
||||
}
|
||||
|
||||
impl VoteOption {
|
||||
pub fn new(id: u8, text: impl ToString) -> Self {
|
||||
pub fn new(id: u8, text: impl ToString, comment: Option<impl ToString>) -> Self {
|
||||
Self {
|
||||
id,
|
||||
text: text.to_string(),
|
||||
count: 0,
|
||||
min_valid: None,
|
||||
comment: comment.map(|c| c.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -69,34 +70,52 @@ impl VoteOption {
|
||||
#[model] // Has base.Base in V spec
|
||||
pub struct Ballot {
|
||||
pub base_data: BaseModelData,
|
||||
pub user_id: u32, // The ID of the user who cast this ballot
|
||||
pub vote_option_id: u8, // The 'id' of the VoteOption chosen
|
||||
pub shares_count: i64, // Number of shares/tokens/voting power
|
||||
pub user_id: u32, // The ID of the user who cast this ballot
|
||||
pub vote_option_id: u8, // The 'id' of the VoteOption chosen
|
||||
pub shares_count: i64, // Number of shares/tokens/voting power
|
||||
pub comment: Option<String>, // Optional comment from the voter
|
||||
}
|
||||
|
||||
impl Ballot {
|
||||
pub fn new(id: u32, user_id: u32, vote_option_id: u8, shares_count: i64) -> Self {
|
||||
/// Create a new ballot with auto-generated ID
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `id` - Optional ID for the ballot (use None for auto-generated ID)
|
||||
/// * `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<u32>, user_id: u32, vote_option_id: u8, shares_count: i64) -> Self {
|
||||
let mut base_data = BaseModelData::new();
|
||||
if let Some(id) = id {
|
||||
base_data.update_id(id);
|
||||
}
|
||||
|
||||
Self {
|
||||
base_data: BaseModelData::new(id),
|
||||
base_data,
|
||||
user_id,
|
||||
vote_option_id,
|
||||
shares_count,
|
||||
comment: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Proposal represents a governance proposal that can be voted upon.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, CustomType)]
|
||||
#[rhai_model_export(db_type = "std::sync::Arc<crate::db::hero::OurDB>")]
|
||||
#[model] // Has base.Base in V spec
|
||||
pub struct Proposal {
|
||||
pub base_data: BaseModelData,
|
||||
pub creator_id: String, // User ID of the proposal creator
|
||||
pub creator_id: String, // User ID of the proposal creator
|
||||
pub creator_name: String, // User name of the proposal creator
|
||||
|
||||
pub title: String,
|
||||
pub description: String,
|
||||
pub status: ProposalStatus,
|
||||
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
|
||||
// Voting event aspects
|
||||
pub vote_start_date: DateTime<Utc>,
|
||||
pub vote_end_date: DateTime<Utc>,
|
||||
@@ -107,13 +126,41 @@ pub struct Proposal {
|
||||
}
|
||||
|
||||
impl Proposal {
|
||||
pub fn new(id: u32, creator_id: impl ToString, title: impl ToString, description: impl ToString, vote_start_date: DateTime<Utc>, vote_end_date: DateTime<Utc>) -> Self {
|
||||
/// Create a new proposal with auto-generated ID
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `id` - Optional ID for the proposal (use None for auto-generated ID)
|
||||
/// * `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<u32>,
|
||||
creator_id: impl ToString,
|
||||
creator_name: impl ToString,
|
||||
title: impl ToString,
|
||||
description: impl ToString,
|
||||
status: ProposalStatus,
|
||||
created_at: DateTime<Utc>,
|
||||
updated_at: DateTime<Utc>,
|
||||
vote_start_date: DateTime<Utc>,
|
||||
vote_end_date: DateTime<Utc>,
|
||||
) -> Self {
|
||||
let mut base_data = BaseModelData::new();
|
||||
if let Some(id) = id {
|
||||
base_data.update_id(id);
|
||||
}
|
||||
|
||||
Self {
|
||||
base_data: BaseModelData::new(id),
|
||||
base_data,
|
||||
creator_id: creator_id.to_string(),
|
||||
creator_name: creator_name.to_string(),
|
||||
title: title.to_string(),
|
||||
description: description.to_string(),
|
||||
status: ProposalStatus::Draft,
|
||||
status,
|
||||
created_at,
|
||||
updated_at,
|
||||
vote_start_date,
|
||||
vote_end_date,
|
||||
vote_status: VoteEventStatus::Open, // Default to open when created
|
||||
@@ -123,24 +170,41 @@ impl Proposal {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_option(mut self, option_id: u8, option_text: impl ToString) -> Self {
|
||||
let new_option = VoteOption::new(option_id, option_text);
|
||||
pub fn add_option(
|
||||
mut self,
|
||||
option_id: u8,
|
||||
option_text: impl ToString,
|
||||
comment: Option<impl ToString>,
|
||||
) -> Self {
|
||||
let new_option = VoteOption::new(option_id, option_text, comment);
|
||||
self.options.push(new_option);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn cast_vote(mut self, ballot_id: u32, user_id: u32, chosen_option_id: u8, shares: i64) -> Self {
|
||||
|
||||
pub fn cast_vote(
|
||||
mut self,
|
||||
ballot_id: Option<u32>,
|
||||
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;
|
||||
}
|
||||
if !self.options.iter().any(|opt| opt.id == chosen_option_id) {
|
||||
eprintln!("Chosen option ID {} does not exist for proposal '{}'", chosen_option_id, self.title);
|
||||
eprintln!(
|
||||
"Chosen option ID {} does not exist for proposal '{}'",
|
||||
chosen_option_id, self.title
|
||||
);
|
||||
return self;
|
||||
}
|
||||
if let Some(group) = &self.private_group {
|
||||
if !group.contains(&user_id) {
|
||||
eprintln!("User {} is not eligible to vote on proposal '{}'", user_id, self.title);
|
||||
eprintln!(
|
||||
"User {} is not eligible to vote on proposal '{}'",
|
||||
user_id, self.title
|
||||
);
|
||||
return self;
|
||||
}
|
||||
}
|
||||
@@ -148,7 +212,11 @@ impl Proposal {
|
||||
let new_ballot = Ballot::new(ballot_id, 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) {
|
||||
if let Some(option) = self
|
||||
.options
|
||||
.iter_mut()
|
||||
.find(|opt| opt.id == chosen_option_id)
|
||||
{
|
||||
option.count += shares;
|
||||
}
|
||||
self
|
||||
@@ -163,4 +231,65 @@ impl Proposal {
|
||||
self.vote_status = new_status;
|
||||
self
|
||||
}
|
||||
|
||||
/// Cast a vote with a comment
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `ballot_id` - Optional ID for the ballot (use None for auto-generated ID)
|
||||
/// * `user_id` - ID of the user who is casting the vote
|
||||
/// * `chosen_option_id` - ID of the vote option chosen
|
||||
/// * `shares` - Number of shares/tokens/voting power
|
||||
/// * `comment` - Comment from the voter explaining their vote
|
||||
pub fn cast_vote_with_comment(
|
||||
mut self,
|
||||
ballot_id: Option<u32>,
|
||||
user_id: u32,
|
||||
chosen_option_id: u8,
|
||||
shares: i64,
|
||||
comment: impl ToString,
|
||||
) -> Self {
|
||||
// First check if voting is open
|
||||
if self.vote_status != VoteEventStatus::Open {
|
||||
eprintln!("Voting is not open for proposal '{}'", self.title);
|
||||
return self;
|
||||
}
|
||||
|
||||
// Check if the option exists
|
||||
if !self.options.iter().any(|opt| opt.id == chosen_option_id) {
|
||||
eprintln!(
|
||||
"Chosen option ID {} does not exist for proposal '{}'",
|
||||
chosen_option_id, self.title
|
||||
);
|
||||
return self;
|
||||
}
|
||||
|
||||
// Check eligibility for private proposals
|
||||
if let Some(group) = &self.private_group {
|
||||
if !group.contains(&user_id) {
|
||||
eprintln!(
|
||||
"User {} is not eligible to vote on proposal '{}'",
|
||||
user_id, self.title
|
||||
);
|
||||
return self;
|
||||
}
|
||||
}
|
||||
|
||||
// Create a new ballot with the comment
|
||||
let mut new_ballot = Ballot::new(ballot_id, user_id, chosen_option_id, shares);
|
||||
new_ballot.comment = Some(comment.to_string());
|
||||
|
||||
// Add the ballot to the proposal
|
||||
self.ballots.push(new_ballot);
|
||||
|
||||
// Update the vote count for the chosen option
|
||||
if let Some(option) = self
|
||||
.options
|
||||
.iter_mut()
|
||||
.find(|opt| opt.id == chosen_option_id)
|
||||
{
|
||||
option.count += shares;
|
||||
}
|
||||
|
||||
self
|
||||
}
|
||||
}
|
||||
|
@@ -26,10 +26,10 @@ pub struct User {
|
||||
}
|
||||
|
||||
impl User {
|
||||
/// Create a new user
|
||||
pub fn new(id: u32) -> 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(),
|
||||
|
Reference in New Issue
Block a user