diff --git a/herodb/Cargo.toml b/herodb/Cargo.toml index 2dd1c57..c228f10 100644 --- a/herodb/Cargo.toml +++ b/herodb/Cargo.toml @@ -19,7 +19,7 @@ tempfile = "3.8" poem = "1.3.55" poem-openapi = { version = "2.0.11", features = ["swagger-ui"] } tokio = { version = "1", features = ["full"] } -rhai = "1.15.1" +rhai = "1.21.0" paste = "1.0" [[example]] diff --git a/herodb/src/cmd/dbexample2/main.rs b/herodb/src/cmd/dbexample2/main.rs index d7e7215..e9ce238 100644 --- a/herodb/src/cmd/dbexample2/main.rs +++ b/herodb/src/cmd/dbexample2/main.rs @@ -1,13 +1,12 @@ -use chrono::{DateTime, Utc, Duration}; +use chrono::{DateTime, Duration, Utc}; use herodb::db::{DB, DBBuilder}; use herodb::models::biz::{ - Currency, CurrencyBuilder, - Product, ProductBuilder, ProductComponent, ProductComponentBuilder, - ProductType, ProductStatus, - Sale, SaleBuilder, SaleItem, SaleItemBuilder, SaleStatus + Currency, CurrencyBuilder, Product, ProductBuilder, ProductComponent, ProductComponentBuilder, + ProductStatus, ProductType, Sale, SaleBuilder, SaleItem, SaleItemBuilder, SaleStatus, }; -use std::path::PathBuf; +use rhai::{Engine, packages::Package}; use std::fs; +use std::path::PathBuf; fn main() -> Result<(), Box> { println!("DB Example 2: Using Builder Pattern and Model-Specific Methods"); @@ -21,25 +20,170 @@ fn main() -> Result<(), Box> { fs::create_dir_all(&db_path)?; println!("Database path: {:?}", db_path); + let mut engine = Engine::new(); + + engine + .build_type::() + .build_type::() + .build_type::() + .build_type::() + .build_type::() + .build_type::() + .build_type::() + .build_type::() + .build_type::(); + + // Register currency builder methods + engine.register_fn("new_currency_builder", CurrencyBuilder::new); + engine.register_fn("amount", CurrencyBuilder::amount); + engine.register_fn("currency_code", CurrencyBuilder::currency_code::); + engine.register_fn("build", CurrencyBuilder::build); + + // Register method to verify currency + engine.register_fn("amount", Currency::amount); + + // Register product component builder methods + engine.register_fn( + "new_product_component_builder", + ProductComponentBuilder::new, + ); + engine.register_fn("id", ProductComponentBuilder::id); + engine.register_fn("name", ProductComponentBuilder::name::); + engine.register_fn( + "description", + ProductComponentBuilder::description::, + ); + engine.register_fn("quantity", ProductComponentBuilder::quantity); + engine.register_fn("build", ProductComponentBuilder::build); + + // Register product builder methods + engine.register_fn("new_product_builder", ProductBuilder::new); + engine.register_fn("id", ProductBuilder::id); + engine.register_fn("name", ProductBuilder::name::); + engine.register_fn("description", ProductBuilder::description::); + engine.register_fn("price", ProductBuilder::price); + engine.register_fn("type", ProductBuilder::type_); + engine.register_fn("category", ProductBuilder::category::); + engine.register_fn("status", ProductBuilder::status); + engine.register_fn("max_amount", ProductBuilder::max_amount); + engine.register_fn("validity_days", ProductBuilder::validity_days); + engine.register_fn("add_component", ProductBuilder::add_component); + engine.register_fn("build", ProductBuilder::build); + + // Register db builder methods + engine.register_fn("new_db_builder", DBBuilder::new::); + engine.register_fn("register_currency", DBBuilder::register_model::); + engine.register_fn("register_product", DBBuilder::register_model::); + engine.register_fn("register_sale", DBBuilder::register_model::); + engine.register_fn("currency_code", CurrencyBuilder::currency_code::); + engine.register_fn("build", DBBuilder::build); + + // Register db methods + engine.register_fn("insert_currency", DB::insert_currency); + engine.register_fn("insert_product", DB::insert_product); + + let script = r#" + let usd = new_currency_builder() + .amount(0.0) + .currency_code("USD") + .build(); + + // Can we access and print this from the actual Currency? + print(usd.amount()); + + let db = new_db_builder("./tmp/dbexample2") + .register_product() + .register_currency() + .register_sale() + .build(); + + db.insert_currency(usd); + + let component1 = new_product_component_builder() + .id(101) + .name("Basic Support") + .description("24/7 email support") + .quantity(1) + .build(); + + let component2 = new_product_component_builder() + .id(102) + .name("Premium Support") + .description("24/7 phone and email support") + .quantity(1) + .build(); + + // Create products using the builder + // let product1 = new_product_builder() + // .id(1) + // .name("Standard Plan") + // .description("Our standard service offering") + // .price( + // new_currency_builder() + // .amount(29.99) + // .currency_code("USD") + // .build() + // ) + // .type_(ProductType::Service) + // .category("Subscription") + // .status(ProductStatus::Available) + // .max_amount(1000) + // .validity_days(30) + // .add_component(component1) + // .build(); + // + // let product2 = new_product_builder() + // .id(2) + // .name("Premium Plan") + // .description("Our premium service offering with priority support") + // .price( + // new_currency_builder() + // .amount(99.99) + // .currency_code("USD") + // .build() + // ) + // .type_(ProductType::Service) + // .category("Subscription") + // .status(ProductStatus::Available) + // .max_amount(500) + // .validity_days(30) + // .add_component(component2) + // .build(); + + // Insert products using model-specific methods + // db.insert_product(product1); + // db.insert_product(product2); + "#; + + engine.eval::<()>(script)?; + // Create a database instance with our models registered - let db = DBBuilder::new(&db_path) + let mut db = DBBuilder::new(&db_path) .register_model::() .register_model::() .register_model::() .build()?; + // Check if the currency created in the script is actually present, if it is this value should + // be 1 (NOTE: it will be :) ). + let currencies = db.list_currencies()?; + println!("Found {} currencies in db", currencies.len()); + for currency in currencies { + println!("{} {}", currency.amount, currency.currency_code); + } + println!("\n1. Creating Products with Builder Pattern"); println!("----------------------------------------"); - // Create a currency using the builder - let usd = CurrencyBuilder::new() - .amount(0.0) // Initial amount - .currency_code("USD") - .build()?; - - // Insert the currency - db.insert_currency(&usd)?; - println!("Currency created: ${:.2} {}", usd.amount, usd.currency_code); + // // Create a currency using the builder + // let usd = CurrencyBuilder::new() + // .amount(0.0) // Initial amount + // .currency_code("USD") + // .build()?; + // + // // Insert the currency + // db.insert_currency(usd.clone())?; + // println!("Currency created: ${:.2} {}", usd.amount, usd.currency_code); // Create product components using the builder let component1 = ProductComponentBuilder::new() @@ -55,16 +199,17 @@ fn main() -> Result<(), Box> { .description("24/7 phone and email support") .quantity(1) .build()?; - // Create products using the builder let product1 = ProductBuilder::new() .id(1) .name("Standard Plan") .description("Our standard service offering") - .price(CurrencyBuilder::new() - .amount(29.99) - .currency_code("USD") - .build()?) + .price( + CurrencyBuilder::new() + .amount(29.99) + .currency_code("USD") + .build()?, + ) .type_(ProductType::Service) .category("Subscription") .status(ProductStatus::Available) @@ -77,10 +222,12 @@ fn main() -> Result<(), Box> { .id(2) .name("Premium Plan") .description("Our premium service offering with priority support") - .price(CurrencyBuilder::new() - .amount(99.99) - .currency_code("USD") - .build()?) + .price( + CurrencyBuilder::new() + .amount(99.99) + .currency_code("USD") + .build()?, + ) .type_(ProductType::Service) .category("Subscription") .status(ProductStatus::Available) @@ -90,18 +237,27 @@ fn main() -> Result<(), Box> { .build()?; // Insert products using model-specific methods - db.insert_product(&product1)?; - db.insert_product(&product2)?; + db.insert_product(product1.clone())?; + db.insert_product(product2.clone())?; - println!("Product created: {} (${:.2})", product1.name, product1.price.amount); - println!("Product created: {} (${:.2})", product2.name, product2.price.amount); + println!( + "Product created: {} (${:.2})", + product1.name, product1.price.amount + ); + println!( + "Product created: {} (${:.2})", + product2.name, product2.price.amount + ); println!("\n2. Retrieving Products"); println!("--------------------"); // Retrieve products using model-specific methods let retrieved_product1 = db.get_product(1)?; - println!("Retrieved: {} (${:.2})", retrieved_product1.name, retrieved_product1.price.amount); + println!( + "Retrieved: {} (${:.2})", + retrieved_product1.name, retrieved_product1.price.amount + ); println!("Components:"); for component in &retrieved_product1.components { println!(" - {} ({})", component.name, component.description); @@ -114,10 +270,15 @@ fn main() -> Result<(), Box> { let all_products = db.list_products()?; println!("Found {} products:", all_products.len()); for product in all_products { - println!(" - {} (${:.2}, {})", - product.name, + println!( + " - {} (${:.2}, {})", + product.name, product.price.amount, - if product.is_purchasable() { "Available" } else { "Unavailable" } + if product.is_purchasable() { + "Available" + } else { + "Unavailable" + } ); } @@ -126,17 +287,19 @@ fn main() -> Result<(), Box> { // Create a sale using the builder let now = Utc::now(); - + let item1 = SaleItemBuilder::new() .id(201) .sale_id(1) .product_id(1) .name("Standard Plan") .quantity(1) - .unit_price(CurrencyBuilder::new() - .amount(29.99) - .currency_code("USD") - .build()?) + .unit_price( + CurrencyBuilder::new() + .amount(29.99) + .currency_code("USD") + .build()?, + ) .active_till(now + Duration::days(30)) .build()?; @@ -151,11 +314,10 @@ fn main() -> Result<(), Box> { .build()?; // Insert the sale using model-specific methods - db.insert_sale(&sale)?; - println!("Sale created: #{} for {} (${:.2})", - sale.id, - sale.buyer_name, - sale.total_amount.amount + db.insert_sale(sale.clone())?; + println!( + "Sale created: #{} for {} (${:.2})", + sale.id, sale.buyer_name, sale.total_amount.amount ); println!("\n5. Updating a Sale"); @@ -163,12 +325,15 @@ fn main() -> Result<(), Box> { // Retrieve the sale, update it, and save it back let mut retrieved_sale = db.get_sale(1)?; - println!("Retrieved sale: #{} with status {:?}", retrieved_sale.id, retrieved_sale.status); - + println!( + "Retrieved sale: #{} with status {:?}", + retrieved_sale.id, retrieved_sale.status + ); + // Update the status retrieved_sale.update_status(SaleStatus::Completed); - db.insert_sale(&retrieved_sale)?; - + db.insert_sale(retrieved_sale.clone())?; + println!("Updated sale status to {:?}", retrieved_sale.status); println!("\n6. Deleting Objects"); @@ -187,4 +352,4 @@ fn main() -> Result<(), Box> { println!("\nExample completed successfully!"); Ok(()) -} \ No newline at end of file +} diff --git a/herodb/src/db/base.rs b/herodb/src/db/base.rs index 30da0fd..444cf58 100644 --- a/herodb/src/db/base.rs +++ b/herodb/src/db/base.rs @@ -1,5 +1,6 @@ use bincode; use brotli::{CompressorReader, Decompressor}; +use rhai::CustomType; use serde::{Deserialize, Serialize}; use sled; use std::fmt::Debug; @@ -38,15 +39,11 @@ pub trait Storable: Serialize + for<'de> Deserialize<'de> + Sized { let mut compressed = Vec::new(); // Default Brotli parameters: quality 5, lgwin 22 (window size) const BROTLI_QUALITY: u32 = 5; - const BROTLI_LGWIN: u32 = 22; + const BROTLI_LGWIN: u32 = 22; const BUFFER_SIZE: usize = 4096; // 4KB buffer - let mut compressor = CompressorReader::new( - &encoded[..], - BUFFER_SIZE, - BROTLI_QUALITY, - BROTLI_LGWIN - ); + let mut compressor = + CompressorReader::new(&encoded[..], BUFFER_SIZE, BROTLI_QUALITY, BROTLI_LGWIN); compressor.read_to_end(&mut compressed)?; Ok(compressed) @@ -56,7 +53,7 @@ pub trait Storable: Serialize + for<'de> Deserialize<'de> + Sized { fn load_from_bytes(data: &[u8]) -> SledDBResult { let mut decompressed = Vec::new(); const BUFFER_SIZE: usize = 4096; // 4KB buffer - + let mut decompressor = Decompressor::new(data, BUFFER_SIZE); decompressor.read_to_end(&mut decompressed)?; @@ -140,8 +137,8 @@ impl SledDB { Ok(models) } - /// Provides access to the underlying Sled Db instance for advanced operations. - pub fn raw_db(&self) -> &sled::Db { + /// Provides access to the underlying Sled Db instance for advanced operations. + pub fn raw_db(&self) -> &sled::Db { &self.db } } diff --git a/herodb/src/db/db.rs b/herodb/src/db/db.rs index fa124ba..e7bc94b 100644 --- a/herodb/src/db/db.rs +++ b/herodb/src/db/db.rs @@ -1,10 +1,11 @@ use crate::db::base::*; +use bincode; +use rhai::{CustomType, EvalAltResult, TypeBuilder}; use std::any::TypeId; use std::collections::HashMap; +use std::fmt::Debug; use std::path::{Path, PathBuf}; use std::sync::{Arc, Mutex, RwLock}; -use std::fmt::Debug; -use bincode; /// Represents a single database operation in a transaction #[derive(Debug, Clone)] @@ -24,7 +25,7 @@ pub trait AnyDbOperations: Send + Sync { fn delete(&self, id: &str) -> SledDBResult<()>; fn get_any(&self, id: &str) -> SledDBResult>; fn list_any(&self) -> SledDBResult>; - fn insert_any(&self, model: &dyn std::any::Any) -> SledDBResult<()>; + fn insert_any(&self, model: &dyn std::any::Any) -> SledDBResult<()>; fn insert_any_raw(&self, serialized: &[u8]) -> SledDBResult<()>; } @@ -33,17 +34,17 @@ impl AnyDbOperations for SledDB { fn delete(&self, id: &str) -> SledDBResult<()> { self.delete(id) } - + fn get_any(&self, id: &str) -> SledDBResult> { let result = self.get(id)?; Ok(Box::new(result)) } - + fn list_any(&self) -> SledDBResult> { let result = self.list()?; Ok(Box::new(result)) } - + fn insert_any(&self, model: &dyn std::any::Any) -> SledDBResult<()> { // Downcast to the specific type T match model.downcast_ref::() { @@ -51,7 +52,7 @@ impl AnyDbOperations for SledDB { None => Err(SledDBError::TypeError), } } - + fn insert_any_raw(&self, serialized: &[u8]) -> SledDBResult<()> { // Deserialize the bytes into model of type T let model: T = bincode::deserialize(serialized)?; @@ -77,23 +78,25 @@ impl TransactionState { } /// Main DB manager that automatically handles all root models +#[derive(Clone, CustomType)] pub struct DB { db_path: PathBuf, - + // Type map for generic operations - type_map: HashMap>, - + type_map: HashMap>, + // Locks to ensure thread safety for key areas _write_locks: Arc>>, - + // Transaction state - transaction: RwLock>, + transaction: Arc>>, } /// Builder for DB that allows registering models +#[derive(Clone, CustomType)] pub struct DBBuilder { base_path: PathBuf, - model_registrations: Vec>, + model_registrations: Vec>, } /// Trait for model registration @@ -129,33 +132,45 @@ impl DBBuilder { model_registrations: Vec::new(), } } - + + pub fn with_path>(base_path: P) -> Self { + Self { + base_path: base_path.into(), + model_registrations: Vec::new(), + } + } + /// Register a model type with the DB pub fn register_model(mut self) -> Self { - self.model_registrations.push(Box::new(SledModelRegistration::::new())); + self.model_registrations + .push(Arc::new(SledModelRegistration::::new())); self } - + /// Build the DB with the registered models - pub fn build(self) -> SledDBResult { + pub fn build(self) -> Result> { let base_path = self.base_path; - + // Ensure base directory exists if !base_path.exists() { - std::fs::create_dir_all(&base_path)?; + std::fs::create_dir_all(&base_path).map_err(|e| { + EvalAltResult::ErrorSystem("Could not create base dir".to_string(), Box::new(e)) + })?; } - + // Register all models - let mut type_map: HashMap> = HashMap::new(); - + let mut type_map: HashMap> = HashMap::new(); + for registration in self.model_registrations { - let (type_id, db) = registration.register(&base_path)?; - type_map.insert(type_id, db); + let (type_id, db) = registration.register(&base_path).map_err(|e| { + EvalAltResult::ErrorSystem("Could not register type".to_string(), Box::new(e)) + })?; + type_map.insert(type_id, db.into()); } - + let _write_locks = Arc::new(Mutex::new(HashMap::new())); - let transaction = RwLock::new(None); - + let transaction = Arc::new(RwLock::new(None)); + Ok(DB { db_path: base_path, type_map, @@ -169,15 +184,15 @@ impl DB { /// Create a new empty DB instance without any models pub fn new>(base_path: P) -> SledDBResult { let base_path = base_path.into(); - + // Ensure base directory exists if !base_path.exists() { std::fs::create_dir_all(&base_path)?; } - + let _write_locks = Arc::new(Mutex::new(HashMap::new())); - let transaction = RwLock::new(None); - + let transaction = Arc::new(RwLock::new(None)); + Ok(Self { db_path: base_path, type_map: HashMap::new(), @@ -185,25 +200,27 @@ impl DB { transaction, }) } - + // Transaction-related methods - + /// Begin a new transaction pub fn begin_transaction(&self) -> SledDBResult<()> { let mut tx = self.transaction.write().unwrap(); if tx.is_some() { - return Err(SledDBError::GeneralError("Transaction already in progress".into())); + return Err(SledDBError::GeneralError( + "Transaction already in progress".into(), + )); } *tx = Some(TransactionState::new()); Ok(()) } - + /// Check if a transaction is active pub fn has_active_transaction(&self) -> bool { let tx = self.transaction.read().unwrap(); tx.is_some() && tx.as_ref().unwrap().active } - + /// Apply a set operation with the serialized data - bypass transaction check fn apply_set_operation(&self, model_type: TypeId, serialized: &[u8]) -> SledDBResult<()> { // Get the database operations for this model type @@ -211,39 +228,47 @@ impl DB { // Just pass the raw serialized data to a special raw insert method return db_ops.insert_any_raw(serialized); } - - Err(SledDBError::GeneralError(format!("No DB registered for type ID {:?}", model_type))) + + Err(SledDBError::GeneralError(format!( + "No DB registered for type ID {:?}", + model_type + ))) } /// Commit the current transaction, applying all operations pub fn commit_transaction(&self) -> SledDBResult<()> { let mut tx_guard = self.transaction.write().unwrap(); - + if let Some(tx_state) = tx_guard.take() { if !tx_state.active { return Err(SledDBError::GeneralError("Transaction not active".into())); } - + // Execute all operations in the transaction for op in tx_state.operations { match op { - DbOperation::Set { model_type, serialized } => { + DbOperation::Set { + model_type, + serialized, + } => { self.apply_set_operation(model_type, &serialized)?; - }, + } DbOperation::Delete { model_type, id } => { - let db_ops = self.type_map.get(&model_type) + let db_ops = self + .type_map + .get(&model_type) .ok_or_else(|| SledDBError::TypeError)?; db_ops.delete(&id)?; } } } - + Ok(()) } else { Err(SledDBError::GeneralError("No active transaction".into())) } } - + /// Rollback the current transaction, discarding all operations pub fn rollback_transaction(&self) -> SledDBResult<()> { let mut tx = self.transaction.write().unwrap(); @@ -253,79 +278,85 @@ impl DB { *tx = None; Ok(()) } - + /// Get the path to the database pub fn path(&self) -> &PathBuf { &self.db_path } - + // Generic methods that work with any supported model type - + /// Insert a model instance into its appropriate database based on type pub fn set(&self, model: &T) -> SledDBResult<()> { // Try to acquire a write lock on the transaction let mut tx_guard = self.transaction.write().unwrap(); - + // Check if there's an active transaction if let Some(tx_state) = tx_guard.as_mut() { if tx_state.active { // Serialize the model for later use let serialized = bincode::serialize(model)?; - + // Record a Set operation in the transaction tx_state.operations.push(DbOperation::Set { model_type: TypeId::of::(), serialized, }); - + return Ok(()); } } - + // If we got here, either there's no transaction or it's not active // Drop the write lock before doing a direct database operation drop(tx_guard); - + // Execute directly match self.type_map.get(&TypeId::of::()) { Some(db_ops) => db_ops.insert_any(model), None => Err(SledDBError::TypeError), } } - + /// Check the transaction state for the given type and id fn check_transaction(&self, id: &str) -> Option, SledDBError>> { // Try to acquire a read lock on the transaction let tx_guard = self.transaction.read().unwrap(); - + if let Some(tx_state) = tx_guard.as_ref() { if !tx_state.active { return None; } - + let type_id = TypeId::of::(); let id_str = id.to_string(); - + // Process operations in reverse order (last operation wins) for op in tx_state.operations.iter().rev() { match op { // First check if this ID has been deleted in the transaction - DbOperation::Delete { model_type, id: op_id } => { + DbOperation::Delete { + model_type, + id: op_id, + } => { if *model_type == type_id && op_id == id { // Return NotFound error for deleted records return Some(Err(SledDBError::NotFound(id.to_string()))); } - }, + } // Then check if it has been set in the transaction - DbOperation::Set { model_type, serialized } => { + DbOperation::Set { + model_type, + serialized, + } => { if *model_type == type_id { // Try to deserialize and check the ID match bincode::deserialize::(serialized) { Ok(model) => { - if model.get_id() == id_str { - return Some(Ok(Some(model))); + if model.get_id() == id_str { + return Some(Ok(Some(model))); + } } - }, Err(_) => continue, // Skip if deserialization fails } } @@ -333,7 +364,7 @@ impl DB { } } } - + // Not found in transaction (continue to database) None } @@ -348,7 +379,7 @@ impl DB { Ok(None) => {} // Should never happen } } - + // If no pending value, look up from the database match self.type_map.get(&TypeId::of::()) { Some(db_ops) => { @@ -358,16 +389,16 @@ impl DB { Ok(t) => Ok(*t), Err(_) => Err(SledDBError::TypeError), } - }, + } None => Err(SledDBError::TypeError), } } - + /// Delete a model instance by its ID and type pub fn delete(&self, id: &str) -> SledDBResult<()> { // Try to acquire a write lock on the transaction let mut tx_guard = self.transaction.write().unwrap(); - + // Check if there's an active transaction if let Some(tx_state) = tx_guard.as_mut() { if tx_state.active { @@ -376,22 +407,22 @@ impl DB { model_type: TypeId::of::(), id: id.to_string(), }); - + return Ok(()); } } - + // If we got here, either there's no transaction or it's not active // Drop the write lock before doing a direct database operation drop(tx_guard); - + // Execute directly match self.type_map.get(&TypeId::of::()) { Some(db_ops) => db_ops.delete(id), None => Err(SledDBError::TypeError), } } - + /// List all model instances of a specific type pub fn list(&self) -> SledDBResult> { // Look up the correct DB operations for type T in our type map @@ -403,24 +434,27 @@ impl DB { Ok(vec_t) => Ok(*vec_t), Err(_) => Err(SledDBError::TypeError), } - }, + } None => Err(SledDBError::TypeError), } } - + // Register a model type with this DB instance pub fn register(&mut self) -> SledDBResult<()> { let db_path = self.db_path.join(T::db_prefix()); let db: SledDB = SledDB::open(db_path)?; - self.type_map.insert(TypeId::of::(), Box::new(db)); + self.type_map.insert(TypeId::of::(), Arc::new(db)); Ok(()) } - + // Get a typed handle to a registered model DB pub fn db_for(&self) -> SledDBResult<&dyn AnyDbOperations> { match self.type_map.get(&TypeId::of::()) { Some(db) => Ok(&**db), - None => Err(SledDBError::GeneralError(format!("No DB registered for type {}", std::any::type_name::()))), + None => Err(SledDBError::GeneralError(format!( + "No DB registered for type {}", + std::any::type_name::() + ))), } } } diff --git a/herodb/src/db/macros.rs b/herodb/src/db/macros.rs index e77a289..aa784a3 100644 --- a/herodb/src/db/macros.rs +++ b/herodb/src/db/macros.rs @@ -5,25 +5,27 @@ macro_rules! impl_model_methods { impl DB { paste::paste! { /// Insert a model instance into the database - pub fn [](&self, item: &$model) -> SledDBResult<()> { - self.set(item) + pub fn [](&mut self, item: $model) -> Result<(), Box> { + Ok(self.set(&item).map_err(|e| { + rhai::EvalAltResult::ErrorSystem("could not insert $singular".to_string(), Box::new(e)) + })?) } /// Get a model instance by its ID - pub fn [](&self, id: u32) -> SledDBResult<$model> { + pub fn [](&mut self, id: i64) -> SledDBResult<$model> { self.get::<$model>(&id.to_string()) } /// Delete a model instance by its ID - pub fn [](&self, id: u32) -> SledDBResult<()> { + pub fn [](&mut self, id: i64) -> SledDBResult<()> { self.delete::<$model>(&id.to_string()) } /// List all model instances - pub fn [](&self) -> SledDBResult> { + pub fn [](&mut self) -> SledDBResult> { self.list::<$model>() } } } }; -} \ No newline at end of file +} diff --git a/herodb/src/models/biz/currency.rs b/herodb/src/models/biz/currency.rs index 28464a0..ab74617 100644 --- a/herodb/src/models/biz/currency.rs +++ b/herodb/src/models/biz/currency.rs @@ -1,9 +1,10 @@ -use chrono::{DateTime, Utc, Duration}; -use serde::{Deserialize, Serialize}; -use crate::db::base::{SledModel, Storable}; // Import Sled traits from db module +use crate::db::base::{SledModel, Storable}; +use chrono::{DateTime, Duration, Utc}; +use rhai::{CustomType, EvalAltResult, TypeBuilder}; +use serde::{Deserialize, Serialize}; // Import Sled traits from db module /// Currency represents a monetary value with amount and currency code -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, CustomType)] pub struct Currency { pub amount: f64, pub currency_code: String, @@ -17,9 +18,14 @@ impl Currency { currency_code, } } + + pub fn amount(&mut self) -> f64 { + self.amount + } } /// Builder for Currency +#[derive(Clone, CustomType)] pub struct CurrencyBuilder { amount: Option, currency_code: Option, @@ -47,7 +53,7 @@ impl CurrencyBuilder { } /// Build the Currency object - pub fn build(self) -> Result { + pub fn build(self) -> Result> { Ok(Currency { amount: self.amount.ok_or("amount is required")?, currency_code: self.currency_code.ok_or("currency_code is required")?, diff --git a/herodb/src/models/biz/product.rs b/herodb/src/models/biz/product.rs index 678f601..c19ed83 100644 --- a/herodb/src/models/biz/product.rs +++ b/herodb/src/models/biz/product.rs @@ -1,7 +1,7 @@ -use chrono::{DateTime, Utc, Duration}; -use serde::{Deserialize, Serialize}; -use crate::db::base::{SledModel, Storable}; // Import Sled traits from db module - +use crate::db::base::{SledModel, Storable}; +use chrono::{DateTime, Duration, Utc}; +use rhai::{CustomType, EvalAltResult, TypeBuilder, export_module}; +use serde::{Deserialize, Serialize}; // Import Sled traits from db module /// ProductType represents the type of a product #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] @@ -20,17 +20,17 @@ pub enum ProductStatus { /// ProductComponent represents a component of a product #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ProductComponent { - pub id: u32, + pub id: i64, pub name: String, pub description: String, - pub quantity: i32, + pub quantity: i64, pub created_at: DateTime, pub updated_at: DateTime, } impl ProductComponent { /// Create a new product component with default timestamps - pub fn new(id: u32, name: String, description: String, quantity: i32) -> Self { + pub fn new(id: i64, name: String, description: String, quantity: i64) -> Self { let now = Utc::now(); Self { id, @@ -44,11 +44,12 @@ impl ProductComponent { } /// Builder for ProductComponent +#[derive(Clone, CustomType)] pub struct ProductComponentBuilder { - id: Option, + id: Option, name: Option, description: Option, - quantity: Option, + quantity: Option, created_at: Option>, updated_at: Option>, } @@ -67,7 +68,7 @@ impl ProductComponentBuilder { } /// Set the id - pub fn id(mut self, id: u32) -> Self { + pub fn id(mut self, id: i64) -> Self { self.id = Some(id); self } @@ -85,7 +86,7 @@ impl ProductComponentBuilder { } /// Set the quantity - pub fn quantity(mut self, quantity: i32) -> Self { + pub fn quantity(mut self, quantity: i64) -> Self { self.quantity = Some(quantity); self } @@ -103,7 +104,7 @@ impl ProductComponentBuilder { } /// Build the ProductComponent object - pub fn build(self) -> Result { + pub fn build(self) -> Result> { let now = Utc::now(); Ok(ProductComponent { id: self.id.ok_or("id is required")?, @@ -117,9 +118,9 @@ impl ProductComponentBuilder { } /// Product represents a product or service offered by the Freezone -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, CustomType)] pub struct Product { - pub id: u32, + pub id: i64, pub name: String, pub description: String, pub price: Currency, @@ -128,7 +129,7 @@ pub struct Product { pub status: ProductStatus, pub created_at: DateTime, pub updated_at: DateTime, - pub max_amount: u16, // means allows us to define how many max of this there are + pub max_amount: i64, // means allows us to define how many max of this there are pub purchase_till: DateTime, pub active_till: DateTime, // after this product no longer active if e.g. a service pub components: Vec, @@ -139,14 +140,14 @@ pub struct Product { impl Product { /// Create a new product with default timestamps pub fn new( - id: u32, + id: i64, name: String, description: String, price: Currency, type_: ProductType, category: String, status: ProductStatus, - max_amount: u16, + max_amount: i64, validity_days: i64, // How many days the product is valid after purchase ) -> Self { let now = Utc::now(); @@ -167,30 +168,30 @@ impl Product { components: Vec::new(), } } - + /// Add a component to this product pub fn add_component(&mut self, component: ProductComponent) { self.components.push(component); self.updated_at = Utc::now(); } - + /// Update the purchase availability timeframe pub fn set_purchase_period(&mut self, purchase_till: DateTime) { self.purchase_till = purchase_till; self.updated_at = Utc::now(); } - + /// Update the active timeframe pub fn set_active_period(&mut self, active_till: DateTime) { self.active_till = active_till; self.updated_at = Utc::now(); } - + /// Check if the product is available for purchase pub fn is_purchasable(&self) -> bool { self.status == ProductStatus::Available && Utc::now() <= self.purchase_till } - + /// Check if the product is still active (for services) pub fn is_active(&self) -> bool { Utc::now() <= self.active_till @@ -198,8 +199,9 @@ impl Product { } /// Builder for Product +#[derive(Clone, CustomType)] pub struct ProductBuilder { - id: Option, + id: Option, name: Option, description: Option, price: Option, @@ -208,7 +210,7 @@ pub struct ProductBuilder { status: Option, created_at: Option>, updated_at: Option>, - max_amount: Option, + max_amount: Option, purchase_till: Option>, active_till: Option>, components: Vec, @@ -237,7 +239,7 @@ impl ProductBuilder { } /// Set the id - pub fn id(mut self, id: u32) -> Self { + pub fn id(mut self, id: i64) -> Self { self.id = Some(id); self } @@ -279,7 +281,7 @@ impl ProductBuilder { } /// Set the max amount - pub fn max_amount(mut self, max_amount: u16) -> Self { + pub fn max_amount(mut self, max_amount: i64) -> Self { self.max_amount = Some(max_amount); self } @@ -313,13 +315,15 @@ impl ProductBuilder { let now = Utc::now(); let created_at = self.created_at.unwrap_or(now); let updated_at = self.updated_at.unwrap_or(now); - + // Calculate purchase_till and active_till based on validity_days if not set directly let purchase_till = self.purchase_till.unwrap_or(now + Duration::days(365)); let active_till = if let Some(validity_days) = self.validity_days { - self.active_till.unwrap_or(now + Duration::days(validity_days)) + self.active_till + .unwrap_or(now + Duration::days(validity_days)) } else { - self.active_till.ok_or("Either active_till or validity_days must be provided")? + self.active_till + .ok_or("Either active_till or validity_days must be provided")? }; Ok(Product { diff --git a/herodb/src/models/biz/sale.rs b/herodb/src/models/biz/sale.rs index bcfede1..dda10f7 100644 --- a/herodb/src/models/biz/sale.rs +++ b/herodb/src/models/biz/sale.rs @@ -1,7 +1,8 @@ -use crate::models::biz::Currency; // Use crate:: for importing from the module -use crate::db::base::{SledModel, Storable}; // Import Sled traits from db module +use crate::db::base::{SledModel, Storable}; +use crate::models::biz::Currency; // Use crate:: for importing from the module // Import Sled traits from db module // use super::db::Model; // Removed old Model trait import use chrono::{DateTime, Utc}; +use rhai::{CustomType, TypeBuilder}; use serde::{Deserialize, Serialize}; // use std::collections::HashMap; // Removed unused import @@ -43,7 +44,7 @@ impl SaleItem { amount, currency_code: unit_price.currency_code.clone(), }; - + Self { id, sale_id, @@ -58,6 +59,7 @@ impl SaleItem { } /// Builder for SaleItem +#[derive(Clone, CustomType)] pub struct SaleItemBuilder { id: Option, sale_id: Option, @@ -130,7 +132,7 @@ impl SaleItemBuilder { pub fn build(self) -> Result { let unit_price = self.unit_price.ok_or("unit_price is required")?; let quantity = self.quantity.ok_or("quantity is required")?; - + // Calculate subtotal let amount = unit_price.amount * quantity as f64; let subtotal = Currency { @@ -152,7 +154,7 @@ impl SaleItemBuilder { } /// Sale represents a sale of products or services -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, CustomType)] pub struct Sale { pub id: u32, pub company_id: u32, @@ -184,7 +186,10 @@ impl Sale { company_id, buyer_name, buyer_email, - total_amount: Currency { amount: 0.0, currency_code }, + total_amount: Currency { + amount: 0.0, + currency_code, + }, status, sale_date: now, created_at: now, @@ -192,12 +197,12 @@ impl Sale { items: Vec::new(), } } - + /// Add an item to the sale and update the total amount pub fn add_item(&mut self, item: SaleItem) { // Make sure the item's sale_id matches this sale assert_eq!(self.id, item.sale_id, "Item sale_id must match sale id"); - + // Update the total amount if self.items.is_empty() { // First item, initialize the total amount with the same currency @@ -210,14 +215,14 @@ impl Sale { // (Assumes all items have the same currency) self.total_amount.amount += item.subtotal.amount; } - + // Add the item to the list self.items.push(item); - + // Update the sale timestamp self.updated_at = Utc::now(); } - + /// Update the status of the sale pub fn update_status(&mut self, status: SaleStatus) { self.status = status; @@ -226,6 +231,7 @@ impl Sale { } /// Builder for Sale +#[derive(Clone, CustomType)] pub struct SaleBuilder { id: Option, company_id: Option, @@ -311,20 +317,20 @@ impl SaleBuilder { let now = Utc::now(); let id = self.id.ok_or("id is required")?; let currency_code = self.currency_code.ok_or("currency_code is required")?; - + // Initialize with empty total amount let mut total_amount = Currency { amount: 0.0, currency_code: currency_code.clone(), }; - + // Calculate total amount from items for item in &self.items { // Make sure the item's sale_id matches this sale if item.sale_id != id { return Err("Item sale_id must match sale id"); } - + if total_amount.amount == 0.0 { // First item, initialize the total amount with the same currency total_amount = Currency {