Inject some builders in script

Signed-off-by: Lee Smet <lee.smet@hotmail.com>
This commit is contained in:
Lee Smet 2025-04-04 15:59:30 +02:00
parent 04233e6f1a
commit 53d8d184c4
Signed by untrusted user who does not match committer: lee
GPG Key ID: 72CBFB5FDA7FE025
8 changed files with 404 additions and 190 deletions

View File

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

View File

@ -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<dyn std::error::Error>> {
println!("DB Example 2: Using Builder Pattern and Model-Specific Methods");
@ -21,25 +20,170 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
fs::create_dir_all(&db_path)?;
println!("Database path: {:?}", db_path);
let mut engine = Engine::new();
engine
.build_type::<Product>()
.build_type::<ProductBuilder>()
.build_type::<ProductComponentBuilder>()
.build_type::<Currency>()
.build_type::<CurrencyBuilder>()
.build_type::<Sale>()
.build_type::<SaleBuilder>()
.build_type::<DBBuilder>()
.build_type::<DB>();
// 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::<String>);
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::<String>);
engine.register_fn(
"description",
ProductComponentBuilder::description::<String>,
);
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::<String>);
engine.register_fn("description", ProductBuilder::description::<String>);
engine.register_fn("price", ProductBuilder::price);
engine.register_fn("type", ProductBuilder::type_);
engine.register_fn("category", ProductBuilder::category::<String>);
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::<String>);
engine.register_fn("register_currency", DBBuilder::register_model::<Currency>);
engine.register_fn("register_product", DBBuilder::register_model::<Product>);
engine.register_fn("register_sale", DBBuilder::register_model::<Sale>);
engine.register_fn("currency_code", CurrencyBuilder::currency_code::<String>);
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::<Product>()
.register_model::<Currency>()
.register_model::<Sale>()
.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<dyn std::error::Error>> {
.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<dyn std::error::Error>> {
.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<dyn std::error::Error>> {
.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<dyn std::error::Error>> {
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<dyn std::error::Error>> {
// 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<dyn std::error::Error>> {
.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<dyn std::error::Error>> {
// 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<dyn std::error::Error>> {
println!("\nExample completed successfully!");
Ok(())
}
}

View File

@ -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<Self> {
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<T: SledModel> SledDB<T> {
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
}
}

View File

@ -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<Box<dyn std::any::Any>>;
fn list_any(&self) -> SledDBResult<Box<dyn std::any::Any>>;
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<T: SledModel> AnyDbOperations for SledDB<T> {
fn delete(&self, id: &str) -> SledDBResult<()> {
self.delete(id)
}
fn get_any(&self, id: &str) -> SledDBResult<Box<dyn std::any::Any>> {
let result = self.get(id)?;
Ok(Box::new(result))
}
fn list_any(&self) -> SledDBResult<Box<dyn std::any::Any>> {
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::<T>() {
@ -51,7 +52,7 @@ impl<T: SledModel> AnyDbOperations for SledDB<T> {
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<TypeId, Box<dyn AnyDbOperations>>,
type_map: HashMap<TypeId, Arc<dyn AnyDbOperations>>,
// Locks to ensure thread safety for key areas
_write_locks: Arc<Mutex<HashMap<String, bool>>>,
// Transaction state
transaction: RwLock<Option<TransactionState>>,
transaction: Arc<RwLock<Option<TransactionState>>>,
}
/// Builder for DB that allows registering models
#[derive(Clone, CustomType)]
pub struct DBBuilder {
base_path: PathBuf,
model_registrations: Vec<Box<dyn ModelRegistration>>,
model_registrations: Vec<Arc<dyn ModelRegistration>>,
}
/// Trait for model registration
@ -129,33 +132,45 @@ impl DBBuilder {
model_registrations: Vec::new(),
}
}
pub fn with_path<P: Into<PathBuf>>(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<T: SledModel>(mut self) -> Self {
self.model_registrations.push(Box::new(SledModelRegistration::<T>::new()));
self.model_registrations
.push(Arc::new(SledModelRegistration::<T>::new()));
self
}
/// Build the DB with the registered models
pub fn build(self) -> SledDBResult<DB> {
pub fn build(self) -> Result<DB, Box<EvalAltResult>> {
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<TypeId, Box<dyn AnyDbOperations>> = HashMap::new();
let mut type_map: HashMap<TypeId, Arc<dyn AnyDbOperations>> = 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<P: Into<PathBuf>>(base_path: P) -> SledDBResult<Self> {
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<T: SledModel>(&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::<T>(),
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::<T>()) {
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<T: SledModel>(&self, id: &str) -> Option<Result<Option<T>, 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::<T>();
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::<T>(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::<T>()) {
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<T: SledModel>(&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::<T>(),
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::<T>()) {
Some(db_ops) => db_ops.delete(id),
None => Err(SledDBError::TypeError),
}
}
/// List all model instances of a specific type
pub fn list<T: SledModel>(&self) -> SledDBResult<Vec<T>> {
// 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<T: SledModel>(&mut self) -> SledDBResult<()> {
let db_path = self.db_path.join(T::db_prefix());
let db: SledDB<T> = SledDB::open(db_path)?;
self.type_map.insert(TypeId::of::<T>(), Box::new(db));
self.type_map.insert(TypeId::of::<T>(), Arc::new(db));
Ok(())
}
// Get a typed handle to a registered model DB
pub fn db_for<T: SledModel>(&self) -> SledDBResult<&dyn AnyDbOperations> {
match self.type_map.get(&TypeId::of::<T>()) {
Some(db) => Ok(&**db),
None => Err(SledDBError::GeneralError(format!("No DB registered for type {}", std::any::type_name::<T>()))),
None => Err(SledDBError::GeneralError(format!(
"No DB registered for type {}",
std::any::type_name::<T>()
))),
}
}
}

View File

@ -5,25 +5,27 @@ macro_rules! impl_model_methods {
impl DB {
paste::paste! {
/// Insert a model instance into the database
pub fn [<insert_ $singular>](&self, item: &$model) -> SledDBResult<()> {
self.set(item)
pub fn [<insert_ $singular>](&mut self, item: $model) -> Result<(), Box<rhai::EvalAltResult>> {
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 [<get_ $singular>](&self, id: u32) -> SledDBResult<$model> {
pub fn [<get_ $singular>](&mut self, id: i64) -> SledDBResult<$model> {
self.get::<$model>(&id.to_string())
}
/// Delete a model instance by its ID
pub fn [<delete_ $singular>](&self, id: u32) -> SledDBResult<()> {
pub fn [<delete_ $singular>](&mut self, id: i64) -> SledDBResult<()> {
self.delete::<$model>(&id.to_string())
}
/// List all model instances
pub fn [<list_ $plural>](&self) -> SledDBResult<Vec<$model>> {
pub fn [<list_ $plural>](&mut self) -> SledDBResult<Vec<$model>> {
self.list::<$model>()
}
}
}
};
}
}

View File

@ -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<f64>,
currency_code: Option<String>,
@ -47,7 +53,7 @@ impl CurrencyBuilder {
}
/// Build the Currency object
pub fn build(self) -> Result<Currency, &'static str> {
pub fn build(self) -> Result<Currency, Box<EvalAltResult>> {
Ok(Currency {
amount: self.amount.ok_or("amount is required")?,
currency_code: self.currency_code.ok_or("currency_code is required")?,

View File

@ -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<Utc>,
pub updated_at: DateTime<Utc>,
}
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<u32>,
id: Option<i64>,
name: Option<String>,
description: Option<String>,
quantity: Option<i32>,
quantity: Option<i64>,
created_at: Option<DateTime<Utc>>,
updated_at: Option<DateTime<Utc>>,
}
@ -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<ProductComponent, &'static str> {
pub fn build(self) -> Result<ProductComponent, Box<EvalAltResult>> {
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<Utc>,
pub updated_at: DateTime<Utc>,
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<Utc>,
pub active_till: DateTime<Utc>, // after this product no longer active if e.g. a service
pub components: Vec<ProductComponent>,
@ -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<Utc>) {
self.purchase_till = purchase_till;
self.updated_at = Utc::now();
}
/// Update the active timeframe
pub fn set_active_period(&mut self, active_till: DateTime<Utc>) {
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<u32>,
id: Option<i64>,
name: Option<String>,
description: Option<String>,
price: Option<Currency>,
@ -208,7 +210,7 @@ pub struct ProductBuilder {
status: Option<ProductStatus>,
created_at: Option<DateTime<Utc>>,
updated_at: Option<DateTime<Utc>>,
max_amount: Option<u16>,
max_amount: Option<i64>,
purchase_till: Option<DateTime<Utc>>,
active_till: Option<DateTime<Utc>>,
components: Vec<ProductComponent>,
@ -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 {

View File

@ -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<u32>,
sale_id: Option<u32>,
@ -130,7 +132,7 @@ impl SaleItemBuilder {
pub fn build(self) -> Result<SaleItem, &'static str> {
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<u32>,
company_id: Option<u32>,
@ -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 {