diff --git a/heromodels/.DS_Store b/heromodels/.DS_Store new file mode 100644 index 0000000..6862bea Binary files /dev/null and b/heromodels/.DS_Store differ diff --git a/heromodels/Cargo.lock b/heromodels/Cargo.lock index d8cd394..4d7c0f9 100644 --- a/heromodels/Cargo.lock +++ b/heromodels/Cargo.lock @@ -2,14 +2,6 @@ # It is not intended for manual editing. version = 4 -[[package]] -name = "adapter_macros" -version = "0.1.0" -dependencies = [ - "chrono", - "rhai", -] - [[package]] name = "ahash" version = "0.8.12" @@ -177,12 +169,6 @@ dependencies = [ "wasi 0.14.2+wasi-0.2.4", ] -[[package]] -name = "heck" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" - [[package]] name = "heck" version = "0.5.0" @@ -193,21 +179,19 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" name = "heromodels" version = "0.1.0" dependencies = [ - "adapter_macros", "bincode", "chrono", "heromodels-derive", "heromodels_core", "ourdb", "rhai", - "rhai_autobind_macros", "rhai_client_macros", - "rhai_wrapper", "serde", "serde_json", "strum", "strum_macros", "tst", + "uuid", ] [[package]] @@ -419,16 +403,6 @@ dependencies = [ "thin-vec", ] -[[package]] -name = "rhai_autobind_macros" -version = "0.1.0" -dependencies = [ - "heck 0.4.1", - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "rhai_client_macros" version = "0.1.0" @@ -450,25 +424,6 @@ dependencies = [ "syn", ] -[[package]] -name = "rhai_macros_derive" -version = "0.1.0" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "rhai_wrapper" -version = "0.1.0" -dependencies = [ - "chrono", - "rhai", - "rhai_macros_derive", - "serde", -] - [[package]] name = "rust_decimal" version = "1.37.1" @@ -570,7 +525,7 @@ version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" dependencies = [ - "heck 0.5.0", + "heck", "proc-macro2", "quote", "rustversion", @@ -643,6 +598,17 @@ version = "0.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d49784317cd0d1ee7ec5c716dd598ec5b4483ea832a2dced265471cc0f690ae" +[[package]] +name = "uuid" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d" +dependencies = [ + "getrandom 0.3.3", + "js-sys", + "wasm-bindgen", +] + [[package]] name = "version_check" version = "0.9.5" diff --git a/heromodels/Cargo.toml b/heromodels/Cargo.toml index 67c5be5..392a73d 100644 --- a/heromodels/Cargo.toml +++ b/heromodels/Cargo.toml @@ -14,13 +14,11 @@ ourdb = { path = "../ourdb" } tst = { path = "../tst" } heromodels-derive = { path = "../heromodels-derive" } heromodels_core = { path = "../heromodels_core" } -rhai_autobind_macros = { path = "../../rhaj/rhai_autobind_macros" } -rhai_wrapper = { path = "../../rhaj/rhai_wrapper" } rhai = { version = "1.21.0", features = ["std", "sync", "decimal", "internals"] } # Added "decimal" feature, sync for Arc> -adapter_macros = { path = "../adapter_macros" } rhai_client_macros = { path = "../rhai_client_macros" } strum = "0.26" strum_macros = "0.26" +uuid = { version = "1.17.0", features = ["v4"] } [features] default = [] diff --git a/heromodels/data/0.db b/heromodels/data/0.db index c68be08..531af47 100644 Binary files a/heromodels/data/0.db and b/heromodels/data/0.db differ diff --git a/heromodels/data/lookup/data b/heromodels/data/lookup/data index 42244a8..bdcc1ae 100644 Binary files a/heromodels/data/lookup/data and b/heromodels/data/lookup/data differ diff --git a/heromodels/examples/biz_rhai/biz.rs b/heromodels/examples/biz_rhai/biz.rhai similarity index 100% rename from heromodels/examples/biz_rhai/biz.rs rename to heromodels/examples/biz_rhai/biz.rhai diff --git a/heromodels/examples/calendar_rhai/calendar.rhai b/heromodels/examples/calendar_rhai/calendar.rhai index f5886ab..08f79bc 100644 --- a/heromodels/examples/calendar_rhai/calendar.rhai +++ b/heromodels/examples/calendar_rhai/calendar.rhai @@ -3,7 +3,7 @@ let db = get_db(); // Create a new calendar using the constructor and builder methods print("Creating a new calendar (ID will be DB-assigned) via registered constructor..."); -let calendar = new_calendar(). // ID removed +let calendar = new_calendar("My first calendar"). // ID removed name("My First Calendar"). description("A calendar for testing Rhai integration"); diff --git a/heromodels/examples/calendar_rhai/example.rs b/heromodels/examples/calendar_rhai/example.rs index bab62cd..08b5501 100644 --- a/heromodels/examples/calendar_rhai/example.rs +++ b/heromodels/examples/calendar_rhai/example.rs @@ -1,8 +1,11 @@ use heromodels::db::hero::OurDB; +use heromodels::models::calendar::{Attendee, AttendanceStatus, Calendar, Event}; use heromodels::models::calendar::rhai::register_rhai_engine_functions; use rhai::Engine; use std::sync::Arc; use std::{fs, path::Path}; +use rhai_wrapper::wrap_vec_return; + fn main() -> Result<(), Box> { // Initialize Rhai engine @@ -14,6 +17,7 @@ fn main() -> Result<(), Box> { // Register the Calendar type with Rhai // This function is generated by the #[rhai_model_export] attribute Calendar::register_rhai_bindings_for_calendar(&mut engine, db.clone()); + register_rhai_engine_functions(&mut engine, db.clone()); // Register a function to get the database instance engine.register_fn("get_db", move || db.clone()); diff --git a/heromodels/index/0.db b/heromodels/index/0.db index 029921e..7f8214c 100644 Binary files a/heromodels/index/0.db and b/heromodels/index/0.db differ diff --git a/heromodels/index/lookup/data b/heromodels/index/lookup/data index 5b3c62c..708d4df 100644 Binary files a/heromodels/index/lookup/data and b/heromodels/index/lookup/data differ diff --git a/heromodels/src/herodb/model_methods.rs b/heromodels/src/herodb/model_methods.rs index 86734c4..c802cb7 100644 --- a/heromodels/src/herodb/model_methods.rs +++ b/heromodels/src/herodb/model_methods.rs @@ -9,6 +9,15 @@ use crate::models::gov::{ // ComplianceRequirement, ComplianceDocument, ComplianceAudit - These don't exist }; use crate::models::circle::{Circle, Member, Name, Wallet}; // Remove Asset +use crate::models::calendar::{Event, Calendar}; + +// Implement model-specific methods for Event +impl_model_methods!(Event, event, events); + +// Implement model-specific methods for Calendar +impl_model_methods!(Calendar, calendar, calendars); + +impl_model_methods!(Attendee, attendee, attendees); // Implement model-specific methods for Product impl_model_methods!(Product, product, products); diff --git a/heromodels/src/lib.rs b/heromodels/src/lib.rs index 8bc137e..410dde7 100644 --- a/heromodels/src/lib.rs +++ b/heromodels/src/lib.rs @@ -6,3 +6,6 @@ pub mod db; // Re-export the procedural macro pub use heromodels_derive::model; + +// Re-export BaseModelData from heromodels_core +pub use heromodels_core::BaseModelData; diff --git a/heromodels/src/models/biz/company.rs b/heromodels/src/models/biz/company.rs index 7fcd02d..441e446 100644 --- a/heromodels/src/models/biz/company.rs +++ b/heromodels/src/models/biz/company.rs @@ -1,6 +1,8 @@ use serde::{Deserialize, Serialize}; use heromodels_core::{BaseModelData, Model, IndexKey, IndexKeyBuilder, Index}; use rhai::{CustomType, TypeBuilder}; // For #[derive(CustomType)] +use heromodels_core::BaseModelDataOps; +use heromodels_derive::model; // --- Enums --- @@ -34,7 +36,8 @@ impl Default for BusinessType { // --- Company Struct --- -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, CustomType)] // Added CustomType +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, CustomType, Default)] // Added CustomType +#[model] pub struct Company { pub base_data: BaseModelData, pub name: String, @@ -51,31 +54,6 @@ pub struct Company { pub status: CompanyStatus, } -// --- Model Trait Implementation --- - -impl Model for Company { - fn db_prefix() -> &'static str { - "company" - } - - fn get_id(&self) -> u32 { - self.base_data.id - } - - fn base_data_mut(&mut self) -> &mut BaseModelData { - &mut self.base_data - } - - // Override db_keys to provide custom indexes if needed - fn db_keys(&self) -> Vec { - vec![ - IndexKeyBuilder::new("name").value(self.name.clone()).build(), - IndexKeyBuilder::new("registration_number").value(self.registration_number.clone()).build(), - // Add other relevant keys, e.g., by status or type if frequently queried - ] - } -} - // --- Index Implementations (Example) --- pub struct CompanyNameIndex; @@ -98,13 +76,19 @@ impl Index for CompanyRegistrationNumberIndex { // --- Builder Pattern --- +impl BaseModelDataOps for Company { + fn get_base_data_mut(&mut self) -> &mut BaseModelData { + &mut self.base_data + } +} + impl Company { - pub fn new(name: String, registration_number: String, incorporation_date: i64) -> Self { // incorporation_date to i64 + pub fn new() -> Self { Self { base_data: BaseModelData::new(), - name, - registration_number, - incorporation_date, // This is i64 now + name: String::new(), + registration_number: String::new(), + incorporation_date: 0, fiscal_year_end: String::new(), email: String::new(), phone: String::new(), @@ -116,29 +100,44 @@ impl Company { status: CompanyStatus::default(), } } - - pub fn fiscal_year_end(mut self, fiscal_year_end: String) -> Self { - self.fiscal_year_end = fiscal_year_end; + + pub fn name(mut self, name: impl ToString) -> Self { + self.name = name.to_string(); + self + } + + pub fn registration_number(mut self, registration_number: impl ToString) -> Self { + self.registration_number = registration_number.to_string(); + self + } + + pub fn incorporation_date(mut self, incorporation_date: i64) -> Self { + self.incorporation_date = incorporation_date; self } - pub fn email(mut self, email: String) -> Self { - self.email = email; + pub fn fiscal_year_end(mut self, fiscal_year_end: impl ToString) -> Self { + self.fiscal_year_end = fiscal_year_end.to_string(); self } - pub fn phone(mut self, phone: String) -> Self { - self.phone = phone; + pub fn email(mut self, email: impl ToString) -> Self { + self.email = email.to_string(); self } - pub fn website(mut self, website: String) -> Self { - self.website = website; + pub fn phone(mut self, phone: impl ToString) -> Self { + self.phone = phone.to_string(); self } - pub fn address(mut self, address: String) -> Self { - self.address = address; + pub fn website(mut self, website: impl ToString) -> Self { + self.website = website.to_string(); + self + } + + pub fn address(mut self, address: impl ToString) -> Self { + self.address = address.to_string(); self } @@ -147,13 +146,13 @@ impl Company { self } - pub fn industry(mut self, industry: String) -> Self { - self.industry = industry; + pub fn industry(mut self, industry: impl ToString) -> Self { + self.industry = industry.to_string(); self } - pub fn description(mut self, description: String) -> Self { - self.description = description; + pub fn description(mut self, description: impl ToString) -> Self { + self.description = description.to_string(); self } @@ -162,17 +161,5 @@ impl Company { self } - // Setter for base_data fields if needed directly, though usually handled by Model trait or new() - // Builder methods for created_at and modified_at can be more specific if needed, - // but Model::build() updates modified_at, and BaseModelData::new() sets created_at. - // If direct setting is required for tests or specific scenarios: - pub fn set_base_created_at(mut self, created_at: i64) -> Self { - self.base_data.created_at = created_at; - self - } - - pub fn set_base_modified_at(mut self, modified_at: i64) -> Self { - self.base_data.modified_at = modified_at; - self - } + // Base data operations are now handled by BaseModelDataOps trait } diff --git a/heromodels/src/models/biz/product.rs b/heromodels/src/models/biz/product.rs index 031a593..a87cb2d 100644 --- a/heromodels/src/models/biz/product.rs +++ b/heromodels/src/models/biz/product.rs @@ -1,6 +1,8 @@ use serde::{Serialize, Deserialize}; use heromodels_core::BaseModelData; use heromodels_core::Model; +use heromodels_core::BaseModelDataOps; +use heromodels_derive::model; // ProductType represents the type of a product #[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)] @@ -27,18 +29,18 @@ pub struct ProductComponent { } impl ProductComponent { - // Minimal constructor - pub fn new(name: String) -> Self { + // Minimal constructor with no parameters + pub fn new() -> Self { Self { - name, + name: String::new(), description: String::new(), quantity: 1, // Default quantity to 1 } } // Builder methods - pub fn description(mut self, description: String) -> Self { - self.description = description; + pub fn description(mut self, description: impl ToString) -> Self { + self.description = description.to_string(); self } @@ -47,14 +49,15 @@ impl ProductComponent { self } - pub fn name(mut self, name: String) -> Self { - self.name = name; + pub fn name(mut self, name: impl ToString) -> Self { + self.name = name.to_string(); self } } // Product represents a product or service offered -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)] +#[model] pub struct Product { pub base_data: BaseModelData, pub name: String, @@ -69,20 +72,6 @@ pub struct Product { pub components: Vec, } -impl Model for Product { - fn get_id(&self) -> u32 { - self.base_data.id - } - - fn db_prefix() -> &'static str { - "prod" - } - - fn base_data_mut(&mut self) -> &mut BaseModelData { - &mut self.base_data - } -} - impl Product { pub fn new() -> Self { Self { @@ -101,13 +90,13 @@ impl Product { } // Builder methods - pub fn name(mut self, name: String) -> Self { - self.name = name; + pub fn name(mut self, name: impl ToString) -> Self { + self.name = name.to_string(); self } - pub fn description(mut self, description: String) -> Self { - self.description = description; + pub fn description(mut self, description: impl ToString) -> Self { + self.description = description.to_string(); self } @@ -121,8 +110,8 @@ impl Product { self } - pub fn category(mut self, category: String) -> Self { - self.category = category; + pub fn category(mut self, category: impl ToString) -> Self { + self.category = category.to_string(); self } @@ -156,24 +145,5 @@ impl Product { self } - // BaseModelData field setters - pub fn set_base_created_at(mut self, time: i64) -> Self { - self.base_data.created_at = time; - self - } - - pub fn set_base_modified_at(mut self, time: i64) -> Self { - self.base_data.modified_at = time; - self - } - - pub fn add_base_comment_id(mut self, comment_id: u32) -> Self { - self.base_data.comments.push(comment_id); - self - } - - pub fn set_base_comment_ids(mut self, comment_ids: Vec) -> Self { - self.base_data.comments = comment_ids; - self - } + // BaseModelData field operations are now handled by BaseModelDataOps trait } diff --git a/heromodels/src/models/biz/rhai.rs b/heromodels/src/models/biz/rhai.rs index 7a8c2ec..6fe6081 100644 --- a/heromodels/src/models/biz/rhai.rs +++ b/heromodels/src/models/biz/rhai.rs @@ -1,332 +1,628 @@ -use rhai::{Engine, Module, Dynamic, EvalAltResult, Position}; +use rhai::plugin::*; +use rhai::{Engine, Module, Dynamic, EvalAltResult, Position, INT}; use std::sync::Arc; +use std::mem; use crate::db::Collection; // For db.set and db.get_by_id use crate::db::hero::OurDB; +use crate::db::Db; + use super::company::{Company, CompanyStatus, BusinessType}; use crate::models::biz::shareholder::{Shareholder, ShareholderType}; use crate::models::biz::product::{Product, ProductType, ProductStatus, ProductComponent}; use crate::models::biz::sale::{Sale, SaleItem, SaleStatus}; use heromodels_core::Model; -// Helper function to convert i64 to u32, returning a Rhai error if conversion fails -fn id_from_i64(id_val: i64) -> Result> { - u32::try_from(id_val).map_err(|_| { +type RhaiCompany = Company; +type RhaiShareholder = Shareholder; +type RhaiProduct = Product; +type RhaiProductComponent = ProductComponent; +type RhaiSale = Sale; +type RhaiSaleItem = SaleItem; + +// Helper to convert i64 from Rhai to u32 for IDs +fn id_from_i64_to_u32(id_i64: i64) -> Result> { + u32::try_from(id_i64).map_err(|_| Box::new(EvalAltResult::ErrorArithmetic( - format!("Failed to convert i64 '{}' to u32 for ID", id_val), - Position::NONE, + format!("Failed to convert ID '{}' to u32", id_i64).into(), + Position::NONE )) - }) + ) +} + +#[export_module] +mod rhai_biz_module { + // --- Company Functions --- + #[rhai_fn(name = "new_company")] + pub fn new_company() -> RhaiCompany { + Company::new() + } + + // Company builder methods + #[rhai_fn(name = "name", return_raw, global, pure)] + pub fn company_name(company: &mut RhaiCompany, name: String) -> Result> { + let owned_company = mem::take(company); + *company = owned_company.name(name); + Ok(company.clone()) + } + + #[rhai_fn(name = "fiscal_year_end", return_raw, global, pure)] + pub fn company_fiscal_year_end(company: &mut RhaiCompany, fiscal_year_end: String) -> Result> { + let owned_company = mem::take(company); + *company = owned_company.fiscal_year_end(fiscal_year_end); + Ok(company.clone()) + } + + #[rhai_fn(name = "registration_number", return_raw, global, pure)] + pub fn company_registration_number(company: &mut RhaiCompany, reg_num: String) -> Result> { + let owned_company = mem::take(company); + *company = owned_company.registration_number(reg_num); + Ok(company.clone()) + } + + #[rhai_fn(name = "incorporation_date", return_raw, global, pure)] + pub fn company_incorporation_date(company: &mut RhaiCompany, date: i64) -> Result> { + let owned_company = mem::take(company); + *company = owned_company.incorporation_date(date); + Ok(company.clone()) + } + + #[rhai_fn(name = "status", return_raw, global, pure)] + pub fn company_status(company: &mut RhaiCompany, status: CompanyStatus) -> Result> { + let owned_company = mem::take(company); + *company = owned_company.status(status); + Ok(company.clone()) + } + + #[rhai_fn(name = "business_type", return_raw, global, pure)] + pub fn company_business_type(company: &mut RhaiCompany, business_type: BusinessType) -> Result> { + let owned_company = mem::take(company); + *company = owned_company.business_type(business_type); + Ok(company.clone()) + } + + // Company getters + #[rhai_fn(name = "get_company_id")] + pub fn get_company_id(company: &mut RhaiCompany) -> i64 { + company.get_id() as i64 + } + + #[rhai_fn(name = "get_company_name")] + pub fn get_company_name(company: &mut RhaiCompany) -> String { + company.name.clone() + } + + #[rhai_fn(name = "get_company_created_at")] + pub fn get_company_created_at(company: &mut RhaiCompany) -> i64 { + company.base_data.created_at + } + + #[rhai_fn(name = "get_company_modified_at")] + pub fn get_company_modified_at(company: &mut RhaiCompany) -> i64 { + company.base_data.modified_at + } + + #[rhai_fn(name = "get_company_registration_number")] + pub fn get_company_registration_number(company: &mut RhaiCompany) -> String { + company.registration_number.clone() + } + + #[rhai_fn(name = "get_company_fiscal_year_end")] + pub fn get_company_fiscal_year_end(company: &mut RhaiCompany) -> String { + company.fiscal_year_end.clone() + } + + #[rhai_fn(name = "get_company_incorporation_date")] + pub fn get_company_incorporation_date(company: &mut RhaiCompany) -> i64 { + company.incorporation_date + } + + #[rhai_fn(name = "get_company_status")] + pub fn get_company_status(company: &mut RhaiCompany) -> CompanyStatus { + company.status.clone() + } + + #[rhai_fn(name = "get_company_business_type")] + pub fn get_company_business_type(company: &mut RhaiCompany) -> BusinessType { + company.business_type.clone() + } + + // --- Shareholder Functions --- + #[rhai_fn(name = "new_shareholder")] + pub fn new_shareholder() -> RhaiShareholder { + Shareholder::new() + } + + // Shareholder builder methods + #[rhai_fn(name = "name", return_raw, global, pure)] + pub fn shareholder_name(shareholder: &mut RhaiShareholder, name: String) -> Result> { + let owned_shareholder = mem::take(shareholder); + *shareholder = owned_shareholder.name(name); + Ok(shareholder.clone()) + } + + #[rhai_fn(name = "company_id", return_raw, global, pure)] + pub fn shareholder_company_id(shareholder: &mut RhaiShareholder, company_id: i64) -> Result> { + let company_id_u32 = id_from_i64_to_u32(company_id)?; + let owned_shareholder = mem::take(shareholder); + *shareholder = owned_shareholder.company_id(company_id_u32); + Ok(shareholder.clone()) + } + + #[rhai_fn(name = "share_count", return_raw, global, pure)] + pub fn shareholder_share_count(shareholder: &mut RhaiShareholder, share_count: f64) -> Result> { + let owned_shareholder = mem::take(shareholder); + shareholder.shares = share_count; + Ok(shareholder.clone()) + } + + #[rhai_fn(name = "type_", return_raw, global, pure)] + pub fn shareholder_type(shareholder: &mut RhaiShareholder, type_: ShareholderType) -> Result> { + let owned_shareholder = mem::take(shareholder); + *shareholder = owned_shareholder.type_(type_); + Ok(shareholder.clone()) + } + + // Shareholder getters + #[rhai_fn(name = "get_shareholder_id")] + pub fn get_shareholder_id(shareholder: &mut RhaiShareholder) -> i64 { + shareholder.get_id() as i64 + } + + #[rhai_fn(name = "get_shareholder_name")] + pub fn get_shareholder_name(shareholder: &mut RhaiShareholder) -> String { + shareholder.name.clone() + } + + #[rhai_fn(name = "get_shareholder_company_id")] + pub fn get_shareholder_company_id(shareholder: &mut RhaiShareholder) -> i64 { + shareholder.company_id as i64 + } + + #[rhai_fn(name = "get_shareholder_share_count")] + pub fn get_shareholder_share_count(shareholder: &mut RhaiShareholder) -> i64 { + shareholder.shares as i64 + } + + #[rhai_fn(name = "get_shareholder_type")] + pub fn get_shareholder_type(shareholder: &mut RhaiShareholder) -> ShareholderType { + shareholder.type_.clone() + } + + // --- ProductComponent Functions --- + #[rhai_fn(name = "new_product_component")] + pub fn new_product_component() -> RhaiProductComponent { + ProductComponent::new() + } + + // ProductComponent builder methods + #[rhai_fn(name = "name", return_raw, global, pure)] + pub fn product_component_name(component: &mut RhaiProductComponent, name: String) -> Result> { + let owned_component = mem::take(component); + *component = owned_component.name(name); + Ok(component.clone()) + } + + #[rhai_fn(name = "description", return_raw, global, pure)] + pub fn product_component_description(component: &mut RhaiProductComponent, description: String) -> Result> { + let owned_component = mem::take(component); + *component = owned_component.description(description); + Ok(component.clone()) + } + + #[rhai_fn(name = "quantity", return_raw, global, pure)] + pub fn product_component_quantity(component: &mut RhaiProductComponent, quantity: i64) -> Result> { + let owned_component = mem::take(component); + *component = owned_component.quantity(quantity as u32); + Ok(component.clone()) + } + + // ProductComponent getters + #[rhai_fn(name = "get_product_component_name")] + pub fn get_product_component_name(component: &mut RhaiProductComponent) -> String { + component.name.clone() + } + + #[rhai_fn(name = "get_product_component_description")] + pub fn get_product_component_description(component: &mut RhaiProductComponent) -> String { + component.description.clone() + } + + #[rhai_fn(name = "get_product_component_quantity")] + pub fn get_product_component_quantity(component: &mut RhaiProductComponent) -> i64 { + component.quantity as i64 + } + + // --- Product Functions --- + #[rhai_fn(name = "new_product")] + pub fn new_product() -> RhaiProduct { + Product::new() + } + + // Product builder methods + #[rhai_fn(name = "name", return_raw, global, pure)] + pub fn product_name(product: &mut RhaiProduct, name: String) -> Result> { + let owned_product = mem::take(product); + *product = owned_product.name(name); + Ok(product.clone()) + } + + #[rhai_fn(name = "description", return_raw, global, pure)] + pub fn product_description(product: &mut RhaiProduct, description: String) -> Result> { + let owned_product = mem::take(product); + *product = owned_product.description(description); + Ok(product.clone()) + } + + #[rhai_fn(name = "price", return_raw, global, pure)] + pub fn product_price(product: &mut RhaiProduct, price: f64) -> Result> { + let owned_product = mem::take(product); + *product = owned_product.price(price); + Ok(product.clone()) + } + + #[rhai_fn(name = "type_", return_raw, global, pure)] + pub fn product_type(product: &mut RhaiProduct, type_: ProductType) -> Result> { + let owned_product = mem::take(product); + *product = owned_product.type_(type_); + Ok(product.clone()) + } + + #[rhai_fn(name = "category", return_raw, global, pure)] + pub fn product_category(product: &mut RhaiProduct, category: String) -> Result> { + let owned_product = mem::take(product); + *product = owned_product.category(category); + Ok(product.clone()) + } + + #[rhai_fn(name = "status", return_raw, global, pure)] + pub fn product_status(product: &mut RhaiProduct, status: ProductStatus) -> Result> { + let owned_product = mem::take(product); + *product = owned_product.status(status); + Ok(product.clone()) + } + + #[rhai_fn(name = "max_amount", return_raw, global, pure)] + pub fn product_max_amount(product: &mut RhaiProduct, max_amount: i64) -> Result> { + let owned_product = mem::take(product); + *product = owned_product.max_amount(max_amount as u16); + Ok(product.clone()) + } + + #[rhai_fn(name = "purchase_till", return_raw, global, pure)] + pub fn product_purchase_till(product: &mut RhaiProduct, purchase_till: i64) -> Result> { + let owned_product = mem::take(product); + *product = owned_product.purchase_till(purchase_till); + Ok(product.clone()) + } + + #[rhai_fn(name = "active_till", return_raw, global, pure)] + pub fn product_active_till(product: &mut RhaiProduct, active_till: i64) -> Result> { + let owned_product = mem::take(product); + *product = owned_product.active_till(active_till); + Ok(product.clone()) + } + + #[rhai_fn(name = "add_component", return_raw, global, pure)] + pub fn product_add_component(product: &mut RhaiProduct, component: RhaiProductComponent) -> Result> { + let owned_product = mem::take(product); + *product = owned_product.add_component(component); + Ok(product.clone()) + } + + #[rhai_fn(name = "components", return_raw, global, pure)] + pub fn product_components(product: &mut RhaiProduct, components: Vec) -> Result> { + let owned_product = mem::take(product); + *product = owned_product.components(components); + Ok(product.clone()) + } + + // Product getters + #[rhai_fn(name = "get_product_id")] + pub fn get_product_id(product: &mut RhaiProduct) -> i64 { + product.get_id() as i64 + } + + #[rhai_fn(name = "get_product_name")] + pub fn get_product_name(product: &mut RhaiProduct) -> String { + product.name.clone() + } + + #[rhai_fn(name = "get_product_description")] + pub fn get_product_description(product: &mut RhaiProduct) -> String { + product.description.clone() + } + + #[rhai_fn(name = "get_product_price")] + pub fn get_product_price(product: &mut RhaiProduct) -> f64 { + product.price + } + + #[rhai_fn(name = "get_product_type")] + pub fn get_product_type(product: &mut RhaiProduct) -> ProductType { + product.type_.clone() + } + + #[rhai_fn(name = "get_product_category")] + pub fn get_product_category(product: &mut RhaiProduct) -> String { + product.category.clone() + } + + #[rhai_fn(name = "get_product_status")] + pub fn get_product_status(product: &mut RhaiProduct) -> ProductStatus { + product.status.clone() + } + + #[rhai_fn(name = "get_product_max_amount")] + pub fn get_product_max_amount(product: &mut RhaiProduct) -> i64 { + product.max_amount as i64 + } + + #[rhai_fn(name = "get_product_purchase_till")] + pub fn get_product_purchase_till(product: &mut RhaiProduct) -> i64 { + product.purchase_till + } + + #[rhai_fn(name = "get_product_active_till")] + pub fn get_product_active_till(product: &mut RhaiProduct) -> i64 { + product.active_till + } + + #[rhai_fn(name = "get_product_components")] + pub fn get_product_components(product: &mut RhaiProduct) -> Vec { + product.components.clone() + } + + #[rhai_fn(name = "get_product_created_at")] + pub fn get_product_created_at(product: &mut RhaiProduct) -> i64 { + product.base_data.created_at + } + + #[rhai_fn(name = "get_product_modified_at")] + pub fn get_product_modified_at(product: &mut RhaiProduct) -> i64 { + product.base_data.modified_at + } + + #[rhai_fn(name = "get_product_comments")] + pub fn get_product_comments(product: &mut RhaiProduct) -> Vec { + product.base_data.comments.iter().map(|&id| id as i64).collect() + } + + // --- SaleItem Functions --- + #[rhai_fn(name = "new_sale_item")] + pub fn new_sale_item() -> RhaiSaleItem { + SaleItem::new() + } + + // SaleItem builder methods + #[rhai_fn(name = "name", return_raw, global, pure)] + pub fn sale_item_name(item: &mut RhaiSaleItem, name: String) -> Result> { + let owned_item = mem::take(item); + *item = owned_item.name(name); + Ok(item.clone()) + } + + #[rhai_fn(name = "price", return_raw, global, pure)] + pub fn sale_item_price(item: &mut RhaiSaleItem, price: f64) -> Result> { + let owned_item = mem::take(item); + item.unit_price = price; + Ok(item.clone()) + } + + #[rhai_fn(name = "quantity", return_raw, global, pure)] + pub fn sale_item_quantity(item: &mut RhaiSaleItem, quantity: i64) -> Result> { + let owned_item = mem::take(item); + *item = owned_item.quantity(quantity.try_into().unwrap()); + Ok(item.clone()) + } + + #[rhai_fn(name = "product_id", return_raw, global, pure)] + pub fn sale_item_product_id(item: &mut RhaiSaleItem, product_id: i64) -> Result> { + let product_id_u32 = id_from_i64_to_u32(product_id)?; + let owned_item = mem::take(item); + *item = owned_item.product_id(product_id_u32); + Ok(item.clone()) + } + + // SaleItem getters + #[rhai_fn(name = "get_sale_item_name")] + pub fn get_sale_item_name(item: &mut RhaiSaleItem) -> String { + item.name.clone() + } + + #[rhai_fn(name = "get_sale_item_price")] + pub fn get_sale_item_price(item: &mut RhaiSaleItem) -> f64 { + item.unit_price + } + + #[rhai_fn(name = "get_sale_item_quantity")] + pub fn get_sale_item_quantity(item: &mut RhaiSaleItem) -> i64 { + item.quantity as i64 + } + + #[rhai_fn(name = "get_sale_item_product_id")] + pub fn get_sale_item_product_id(item: &mut RhaiSaleItem) -> i64 { + item.product_id as i64 + } + + // --- Sale Functions --- + #[rhai_fn(name = "new_sale")] + pub fn new_sale() -> RhaiSale { + Sale::new() + } + + #[rhai_fn(name = "transaction_id", return_raw, global, pure)] + pub fn sale_transaction_id(sale: &mut RhaiSale, transaction_id: u32) -> Result> { + let owned_sale = mem::take(sale); + sale.transaction_id = transaction_id; + Ok(sale.clone()) + } + + #[rhai_fn(name = "status", return_raw, global, pure)] + pub fn sale_status(sale: &mut RhaiSale, status: SaleStatus) -> Result> { + let owned_sale = mem::take(sale); + *sale = owned_sale.status(status); + Ok(sale.clone()) + } + + #[rhai_fn(name = "add_item", return_raw, global, pure)] + pub fn sale_add_item(sale: &mut RhaiSale, item: RhaiSaleItem) -> Result> { + let owned_sale = mem::take(sale); + *sale = owned_sale.add_item(item); + Ok(sale.clone()) + } + + #[rhai_fn(name = "items", return_raw, global, pure)] + pub fn sale_items(sale: &mut RhaiSale, items: Vec) -> Result> { + let owned_sale = mem::take(sale); + *sale = owned_sale.items(items); + Ok(sale.clone()) + } + + // Sale getters + #[rhai_fn(name = "get_sale_id")] + pub fn get_sale_id(sale: &mut RhaiSale) -> i64 { + sale.get_id() as i64 + } + + #[rhai_fn(name = "get_sale_transaction_id")] + pub fn get_sale_transaction_id(sale: &mut RhaiSale) -> u32 { + sale.transaction_id + } + + #[rhai_fn(name = "get_sale_status")] + pub fn get_sale_status(sale: &mut RhaiSale) -> SaleStatus { + sale.status.clone() + } + + #[rhai_fn(name = "get_sale_items")] + pub fn get_sale_items(sale: &mut RhaiSale) -> Vec { + sale.items.clone() + } + + #[rhai_fn(name = "get_sale_created_at")] + pub fn get_sale_created_at(sale: &mut RhaiSale) -> i64 { + sale.base_data.created_at + } + + #[rhai_fn(name = "get_sale_modified_at")] + pub fn get_sale_modified_at(sale: &mut RhaiSale) -> i64 { + sale.base_data.modified_at + } + + #[rhai_fn(name = "get_sale_comments")] + pub fn get_sale_comments(sale: &mut RhaiSale) -> Vec { + sale.base_data.comments.iter().map(|&id| id as i64).collect() + } } pub fn register_biz_rhai_module(engine: &mut Engine, db: Arc) { - let module = Module::new(); - - // --- Enum Constants: CompanyStatus --- - let mut status_constants_module = Module::new(); - status_constants_module.set_var("Active", Dynamic::from(CompanyStatus::Active.clone())); - status_constants_module.set_var("Inactive", Dynamic::from(CompanyStatus::Inactive.clone())); - status_constants_module.set_var("Suspended", Dynamic::from(CompanyStatus::Suspended.clone())); - engine.register_static_module("CompanyStatusConstants", status_constants_module.into()); - engine.register_type_with_name::("CompanyStatus"); - - // --- Enum Constants: BusinessType --- - let mut business_type_constants_module = Module::new(); - business_type_constants_module.set_var("Coop", Dynamic::from(BusinessType::Coop.clone())); - business_type_constants_module.set_var("Single", Dynamic::from(BusinessType::Single.clone())); - business_type_constants_module.set_var("Twin", Dynamic::from(BusinessType::Twin.clone())); - business_type_constants_module.set_var("Starter", Dynamic::from(BusinessType::Starter.clone())); - business_type_constants_module.set_var("Global", Dynamic::from(BusinessType::Global.clone())); - engine.register_static_module("BusinessTypeConstants", business_type_constants_module.into()); - engine.register_type_with_name::("BusinessType"); - - // --- Company --- - engine.register_type_with_name::("Company"); - - // Constructor - engine.register_fn("new_company", |name: String, registration_number: String, incorporation_date: i64| -> Result> { Ok(Company::new(name, registration_number, incorporation_date)) }); - - // Getters for Company - engine.register_get("id", |company: &mut Company| -> Result> { Ok(company.get_id() as i64) }); - engine.register_get("created_at", |company: &mut Company| -> Result> { Ok(company.base_data.created_at) }); - engine.register_get("modified_at", |company: &mut Company| -> Result> { Ok(company.base_data.modified_at) }); - engine.register_get("name", |company: &mut Company| -> Result> { Ok(company.name.clone()) }); - engine.register_get("registration_number", |company: &mut Company| -> Result> { Ok(company.registration_number.clone()) }); - engine.register_get("incorporation_date", |company: &mut Company| -> Result> { Ok(company.incorporation_date as i64) }); - engine.register_get("fiscal_year_end", |company: &mut Company| -> Result> { Ok(company.fiscal_year_end.clone()) }); - engine.register_get("email", |company: &mut Company| -> Result> { Ok(company.email.clone()) }); - engine.register_get("phone", |company: &mut Company| -> Result> { Ok(company.phone.clone()) }); - engine.register_get("website", |company: &mut Company| -> Result> { Ok(company.website.clone()) }); - engine.register_get("address", |company: &mut Company| -> Result> { Ok(company.address.clone()) }); - engine.register_get("business_type", |company: &mut Company| -> Result> { Ok(company.business_type.clone()) }); - engine.register_get("industry", |company: &mut Company| -> Result> { Ok(company.industry.clone()) }); - engine.register_get("description", |company: &mut Company| -> Result> { Ok(company.description.clone()) }); - engine.register_get("status", |company: &mut Company| -> Result> { Ok(company.status.clone()) }); - // Builder methods for Company - engine.register_fn("fiscal_year_end", |company: Company, fiscal_year_end: String| -> Result> { Ok(company.fiscal_year_end(fiscal_year_end)) }); - engine.register_fn("email", |company: Company, email: String| -> Result> { Ok(company.email(email)) }); - engine.register_fn("phone", |company: Company, phone: String| -> Result> { Ok(company.phone(phone)) }); - engine.register_fn("website", |company: Company, website: String| -> Result> { Ok(company.website(website)) }); - engine.register_fn("address", |company: Company, address: String| -> Result> { Ok(company.address(address)) }); - engine.register_fn("business_type", |company: Company, business_type: BusinessType| -> Result> { Ok(company.business_type(business_type)) }); - engine.register_fn("industry", |company: Company, industry: String| -> Result> { Ok(company.industry(industry)) }); - engine.register_fn("description", |company: Company, description: String| -> Result> { Ok(company.description(description)) }); - engine.register_fn("status", |company: Company, status: CompanyStatus| -> Result> { Ok(company.status(status)) }); - engine.register_fn("set_base_created_at", |company: Company, created_at: i64| -> Result> { Ok(company.set_base_created_at(created_at)) }); - engine.register_fn("set_base_modified_at", |company: Company, modified_at: i64| -> Result> { Ok(company.set_base_modified_at(modified_at)) }); - - // --- Enum Constants: ShareholderType --- - let mut shareholder_type_constants_module = Module::new(); - shareholder_type_constants_module.set_var("Individual", Dynamic::from(ShareholderType::Individual.clone())); - shareholder_type_constants_module.set_var("Corporate", Dynamic::from(ShareholderType::Corporate.clone())); - engine.register_static_module("ShareholderTypeConstants", shareholder_type_constants_module.into()); - engine.register_type_with_name::("ShareholderType"); - - // --- Shareholder --- - engine.register_type_with_name::("Shareholder"); - - // Constructor for Shareholder (minimal, takes only ID) - engine.register_fn("new_shareholder", || -> Result> { Ok(Shareholder::new()) }); - - // Getters for Shareholder - engine.register_get("id", |shareholder: &mut Shareholder| -> Result> { Ok(shareholder.get_id() as i64) }); - engine.register_get("created_at", |shareholder: &mut Shareholder| -> Result> { Ok(shareholder.base_data.created_at) }); - engine.register_get("modified_at", |shareholder: &mut Shareholder| -> Result> { Ok(shareholder.base_data.modified_at) }); - engine.register_get("company_id", |shareholder: &mut Shareholder| -> Result> { Ok(shareholder.company_id as i64) }); - engine.register_get("user_id", |shareholder: &mut Shareholder| -> Result> { Ok(shareholder.user_id as i64) }); - engine.register_get("name", |shareholder: &mut Shareholder| -> Result> { Ok(shareholder.name.clone()) }); - engine.register_get("shares", |shareholder: &mut Shareholder| -> Result> { Ok(shareholder.shares) }); - engine.register_get("percentage", |shareholder: &mut Shareholder| -> Result> { Ok(shareholder.percentage) }); - engine.register_get("type_", |shareholder: &mut Shareholder| -> Result> { Ok(shareholder.type_.clone()) }); - engine.register_get("since", |shareholder: &mut Shareholder| -> Result> { Ok(shareholder.since) }); - - // Builder methods for Shareholder - engine.register_fn("company_id", |shareholder: Shareholder, company_id: i64| -> Result> { Ok(shareholder.company_id(company_id as u32)) }); - engine.register_fn("user_id", |shareholder: Shareholder, user_id: i64| -> Result> { Ok(shareholder.user_id(user_id as u32)) }); - engine.register_fn("name", |shareholder: Shareholder, name: String| -> Result> { Ok(shareholder.name(name)) }); - engine.register_fn("shares", |shareholder: Shareholder, shares: f64| -> Result> { Ok(shareholder.shares(shares)) }); - engine.register_fn("percentage", |shareholder: Shareholder, percentage: f64| -> Result> { Ok(shareholder.percentage(percentage)) }); - engine.register_fn("type_", |shareholder: Shareholder, type_: ShareholderType| -> Result> { Ok(shareholder.type_(type_)) }); - engine.register_fn("since", |shareholder: Shareholder, since: i64| -> Result> { Ok(shareholder.since(since)) }); - engine.register_fn("set_base_created_at", |shareholder: Shareholder, created_at: i64| -> Result> { Ok(shareholder.set_base_created_at(created_at)) }); - engine.register_fn("set_base_modified_at", |shareholder: Shareholder, modified_at: i64| -> Result> { Ok(shareholder.set_base_modified_at(modified_at)) }); - - - // --- Enum Constants: ProductType --- - let mut product_type_constants_module = Module::new(); - product_type_constants_module.set_var("Product", Dynamic::from(ProductType::Product.clone())); - product_type_constants_module.set_var("Service", Dynamic::from(ProductType::Service.clone())); - engine.register_static_module("ProductTypeConstants", product_type_constants_module.into()); - engine.register_type_with_name::("ProductType"); - - // --- Enum Constants: ProductStatus --- - let mut product_status_constants_module = Module::new(); - product_status_constants_module.set_var("Available", Dynamic::from(ProductStatus::Available.clone())); - product_status_constants_module.set_var("Unavailable", Dynamic::from(ProductStatus::Unavailable.clone())); - engine.register_static_module("ProductStatusConstants", product_status_constants_module.into()); - engine.register_type_with_name::("ProductStatus"); - - // --- Enum Constants: SaleStatus --- - let mut sale_status_module = Module::new(); - sale_status_module.set_var("Pending", Dynamic::from(SaleStatus::Pending.clone())); - sale_status_module.set_var("Completed", Dynamic::from(SaleStatus::Completed.clone())); - sale_status_module.set_var("Cancelled", Dynamic::from(SaleStatus::Cancelled.clone())); - engine.register_static_module("SaleStatusConstants", sale_status_module.into()); - engine.register_type_with_name::("SaleStatus"); - - // --- ProductComponent --- - engine.register_type_with_name::("ProductComponent") - .register_fn("new_product_component", |name: String| -> Result> { Ok(ProductComponent::new(name)) }) - .register_get("name", |pc: &mut ProductComponent| -> Result> { Ok(pc.name.clone()) }) - .register_fn("name", |pc: ProductComponent, name: String| -> Result> { Ok(pc.name(name)) }) - .register_get("description", |pc: &mut ProductComponent| -> Result> { Ok(pc.description.clone()) }) - .register_fn("description", |pc: ProductComponent, description: String| -> Result> { Ok(pc.description(description)) }) - .register_get("quantity", |pc: &mut ProductComponent| -> Result> { Ok(pc.quantity as i64) }) - .register_fn("quantity", |pc: ProductComponent, quantity: i64| -> Result> { Ok(pc.quantity(quantity as u32)) }); - - // --- Product --- - engine.register_type_with_name::("Product") - .register_fn("new_product", || -> Result> { Ok(Product::new()) }) - // Getters for Product - .register_get("id", |p: &mut Product| -> Result> { Ok(p.base_data.id as i64) }) - .register_get("name", |p: &mut Product| -> Result> { Ok(p.name.clone()) }) - .register_get("description", |p: &mut Product| -> Result> { Ok(p.description.clone()) }) - .register_get("price", |p: &mut Product| -> Result> { Ok(p.price) }) - .register_get("type_", |p: &mut Product| -> Result> { Ok(p.type_.clone()) }) - .register_get("category", |p: &mut Product| -> Result> { Ok(p.category.clone()) }) - .register_get("status", |p: &mut Product| -> Result> { Ok(p.status.clone()) }) - .register_get("max_amount", |p: &mut Product| -> Result> { Ok(p.max_amount as i64) }) - .register_get("purchase_till", |p: &mut Product| -> Result> { Ok(p.purchase_till) }) - .register_get("active_till", |p: &mut Product| -> Result> { Ok(p.active_till) }) - .register_get("components", |p: &mut Product| -> Result> { - let rhai_array = p.components.iter().cloned().map(Dynamic::from).collect::(); - Ok(rhai_array) - }) - // Getters for BaseModelData fields - .register_get("created_at", |p: &mut Product| -> Result> { Ok(p.base_data.created_at) }) - .register_get("modified_at", |p: &mut Product| -> Result> { Ok(p.base_data.modified_at) }) - .register_get("comments", |p: &mut Product| -> Result, Box> { Ok(p.base_data.comments.iter().map(|&id| id as i64).collect()) }) - // Builder methods for Product - .register_fn("name", |p: Product, name: String| -> Result> { Ok(p.name(name)) }) - .register_fn("description", |p: Product, description: String| -> Result> { Ok(p.description(description)) }) - .register_fn("price", |p: Product, price: f64| -> Result> { Ok(p.price(price)) }) - .register_fn("type_", |p: Product, type_: ProductType| -> Result> { Ok(p.type_(type_)) }) - .register_fn("category", |p: Product, category: String| -> Result> { Ok(p.category(category)) }) - .register_fn("status", |p: Product, status: ProductStatus| -> Result> { Ok(p.status(status)) }) - .register_fn("max_amount", |p: Product, max_amount: i64| -> Result> { Ok(p.max_amount(max_amount as u16)) }) - .register_fn("purchase_till", |p: Product, purchase_till: i64| -> Result> { Ok(p.purchase_till(purchase_till)) }) - .register_fn("active_till", |p: Product, active_till: i64| -> Result> { Ok(p.active_till(active_till)) }) - .register_fn("add_component", |p: Product, component: ProductComponent| -> Result> { Ok(p.add_component(component)) }) - .register_fn("components", |p: Product, components: Vec| -> Result> { Ok(p.components(components)) }) - .register_fn("set_base_created_at", |p: Product, time: i64| -> Result> { Ok(p.set_base_created_at(time)) }) - .register_fn("set_base_modified_at", |p: Product, time: i64| -> Result> { Ok(p.set_base_modified_at(time)) }) - .register_fn("add_base_comment_id", |p: Product, comment_id: i64| -> Result> { Ok(p.add_base_comment_id(id_from_i64(comment_id)?)) }) - .register_fn("set_base_comment_ids", |p: Product, comment_ids: Vec| -> Result> { - let u32_ids = comment_ids.into_iter().map(id_from_i64).collect::, _>>()?; - Ok(p.set_base_comment_ids(u32_ids)) - }); - - // --- SaleItem --- - engine.register_type_with_name::("SaleItem"); - engine.register_fn("new_sale_item", |product_id_i64: i64, name: String, quantity_i64: i64, unit_price: f64, subtotal: f64| -> Result> { - Ok(SaleItem::new(id_from_i64(product_id_i64)?, name, quantity_i64 as i32, unit_price, subtotal)) - }); - - // Getters for SaleItem - engine.register_get("product_id", |si: &mut SaleItem| -> Result> { Ok(si.product_id as i64) }); - engine.register_get("name", |si: &mut SaleItem| -> Result> { Ok(si.name.clone()) }); - engine.register_get("quantity", |si: &mut SaleItem| -> Result> { Ok(si.quantity as i64) }); - engine.register_get("unit_price", |si: &mut SaleItem| -> Result> { Ok(si.unit_price) }); - engine.register_get("subtotal", |si: &mut SaleItem| -> Result> { Ok(si.subtotal) }); - engine.register_get("service_active_until", |si: &mut SaleItem| -> Result, Box> { Ok(si.service_active_until) }); - - // Builder-style methods for SaleItem - engine.register_type_with_name::("SaleItem") - .register_fn("product_id", |item: SaleItem, product_id_i64: i64| -> Result> { Ok(item.product_id(id_from_i64(product_id_i64)?)) }) - .register_fn("name", |item: SaleItem, name: String| -> Result> { Ok(item.name(name)) }) - .register_fn("quantity", |item: SaleItem, quantity_i64: i64| -> Result> { Ok(item.quantity(quantity_i64 as i32)) }) - .register_fn("unit_price", |item: SaleItem, unit_price: f64| -> Result> { Ok(item.unit_price(unit_price)) }) - .register_fn("subtotal", |item: SaleItem, subtotal: f64| -> Result> { Ok(item.subtotal(subtotal)) }) - .register_fn("service_active_until", |item: SaleItem, until: Option| -> Result> { Ok(item.service_active_until(until)) }); - - // --- Sale --- - engine.register_type_with_name::("Sale"); - engine.register_fn("new_sale", |company_id_i64: i64, buyer_name: String, buyer_email: String, total_amount: f64, status: SaleStatus, sale_date: i64| -> Result> { Ok(Sale::new(id_from_i64(company_id_i64)?, buyer_name, buyer_email, total_amount, status, sale_date)) }); - - // Getters for Sale - engine.register_get("id", |s: &mut Sale| -> Result> { Ok(s.get_id() as i64) }); - engine.register_get("customer_id", |s: &mut Sale| -> Result> { Ok(s.company_id as i64) }); - engine.register_get("buyer_name", |s: &mut Sale| -> Result> { Ok(s.buyer_name.clone()) }); - engine.register_get("buyer_email", |s: &mut Sale| -> Result> { Ok(s.buyer_email.clone()) }); - engine.register_get("total_amount", |s: &mut Sale| -> Result> { Ok(s.total_amount) }); - engine.register_get("status", |s: &mut Sale| -> Result> { Ok(s.status.clone()) }); - engine.register_get("sale_date", |s: &mut Sale| -> Result> { Ok(s.sale_date) }); - engine.register_get("items", |s: &mut Sale| -> Result> { - Ok(s.items.iter().cloned().map(Dynamic::from).collect::()) - }); - engine.register_get("notes", |s: &mut Sale| -> Result> { Ok(s.notes.clone()) }); - engine.register_get("created_at", |s: &mut Sale| -> Result> { Ok(s.base_data.created_at) }); - engine.register_get("modified_at", |s: &mut Sale| -> Result> { Ok(s.base_data.modified_at) }); - // engine.register_get("uuid", |s: &mut Sale| -> Result, Box> { Ok(s.base_data().uuid.clone()) }); // UUID not in BaseModelData - engine.register_get("comments", |s: &mut Sale| -> Result> { - Ok(s.base_data.comments.iter().map(|&id| Dynamic::from(id as i64)).collect::()) - }); - - // Builder-style methods for Sale - engine.register_type_with_name::("Sale") - .register_fn("customer_id", |s: Sale, customer_id_i64: i64| -> Result> { Ok(s.company_id(id_from_i64(customer_id_i64)?)) }) - .register_fn("buyer_name", |s: Sale, buyer_name: String| -> Result> { Ok(s.buyer_name(buyer_name)) }) - .register_fn("buyer_email", |s: Sale, buyer_email: String| -> Result> { Ok(s.buyer_email(buyer_email)) }) - .register_fn("total_amount", |s: Sale, total_amount: f64| -> Result> { Ok(s.total_amount(total_amount)) }) - .register_fn("status", |s: Sale, status: SaleStatus| -> Result> { Ok(s.status(status)) }) - .register_fn("sale_date", |s: Sale, sale_date: i64| -> Result> { Ok(s.sale_date(sale_date)) }) - .register_fn("add_item", |s: Sale, item: SaleItem| -> Result> { Ok(s.add_item(item)) }) - .register_fn("items", |s: Sale, items: Vec| -> Result> { Ok(s.items(items)) }) - .register_fn("notes", |s: Sale, notes: String| -> Result> { Ok(s.notes(notes)) }) - .register_fn("set_base_id", |s: Sale, id_i64: i64| -> Result> { Ok(s.set_base_id(id_from_i64(id_i64)?)) }) - // .register_fn("set_base_uuid", |s: Sale, uuid: Option| -> Result> { Ok(s.set_base_uuid(uuid)) }) // UUID not in BaseModelData - .register_fn("set_base_created_at", |s: Sale, time: i64| -> Result> { Ok(s.set_base_created_at(time)) }) - .register_fn("set_base_modified_at", |s: Sale, time: i64| -> Result> { Ok(s.set_base_modified_at(time)) }) - .register_fn("add_base_comment", |s: Sale, comment_id_i64: i64| -> Result> { Ok(s.add_base_comment(id_from_i64(comment_id_i64)?)) }) - .register_fn("set_base_comments", |s: Sale, comment_ids: Vec| -> Result> { - let u32_ids = comment_ids.into_iter().map(id_from_i64).collect::, _>>()?; - Ok(s.set_base_comments(u32_ids)) - }); - - // DB functions for Product - let captured_db_for_set_product = Arc::clone(&db); - engine.register_fn("set_product", move |product: Product| -> Result> { - let original_id_for_error = product.get_id(); - captured_db_for_set_product.set(&product) - .map(|(_id, updated_product)| updated_product) - .map_err(|e| { - Box::new(EvalAltResult::ErrorRuntime(format!("Failed to set Product (Original ID: {}): {}", original_id_for_error, e).into(), Position::NONE)) - }) - }); - - let captured_db_for_get_prod = Arc::clone(&db); - engine.register_fn("get_product_by_id", move |id_i64: i64| -> Result> { - let id_u32 = id_i64 as u32; - captured_db_for_get_prod.get_by_id(id_u32) - .map_err(|e| Box::new(EvalAltResult::ErrorRuntime(format!("Error getting Product (ID: {}): {}", id_u32, e).into(), Position::NONE)))? - .ok_or_else(|| Box::new(EvalAltResult::ErrorRuntime(format!("Product with ID {} not found", id_u32).into(), Position::NONE))) - }); - - // DB functions for Sale - let captured_db_for_set_sale = Arc::clone(&db); - engine.register_fn("set_sale", move |sale: Sale| -> Result> { - let original_id_for_error = sale.get_id(); - captured_db_for_set_sale.set(&sale) - .map(|(_id, updated_sale)| updated_sale) - .map_err(|e| { - Box::new(EvalAltResult::ErrorRuntime(format!("Failed to set Sale (Original ID: {}): {}", original_id_for_error, e).into(), Position::NONE)) - }) - }); - - let captured_db_for_get_sale = Arc::clone(&db); - engine.register_fn("get_sale_by_id", move |id_i64: i64| -> Result> { - let id_u32 = id_from_i64(id_i64)?; - captured_db_for_get_sale.get_by_id(id_u32) - .map_err(|e| Box::new(EvalAltResult::ErrorRuntime(format!("Error getting Sale (ID: {}): {}", id_u32, e).into(), Position::NONE)))? - .ok_or_else(|| Box::new(EvalAltResult::ErrorRuntime(format!("Sale with ID {} not found", id_u32).into(), Position::NONE))) - }); - - // Mock DB functions for Shareholder - let captured_db_for_set_shareholder = Arc::clone(&db); - engine.register_fn("set_shareholder", move |shareholder: Shareholder| -> Result> { - let original_id_for_error = shareholder.get_id(); - captured_db_for_set_shareholder.set(&shareholder) - .map(|(_id, updated_shareholder)| updated_shareholder) - .map_err(|e| { - Box::new(EvalAltResult::ErrorRuntime(format!("Failed to set Shareholder (Original ID: {}): {}", original_id_for_error, e).into(), Position::NONE)) - }) - }); - - let captured_db_for_get_sh = Arc::clone(&db); - engine.register_fn("get_shareholder_by_id", move |id_i64: i64| -> Result> { - let id_u32 = id_i64 as u32; - captured_db_for_get_sh.get_by_id(id_u32) - .map_err(|e| Box::new(EvalAltResult::ErrorRuntime(format!("Error getting Shareholder (ID: {}): {}", id_u32, e).into(), Position::NONE)))? - .ok_or_else(|| Box::new(EvalAltResult::ErrorRuntime(format!("Shareholder with ID {} not found", id_u32).into(), Position::NONE))) - }); - - // Mock DB functions for Company - let captured_db_for_set_company = Arc::clone(&db); - engine.register_fn("set_company", move |company: Company| -> Result> { - let original_id_for_error = company.get_id(); // Capture ID before it's potentially changed by DB - captured_db_for_set_company.set(&company) - .map(|(_id, updated_company)| updated_company) // Use the model returned by db.set() - .map_err(|e| { - Box::new(EvalAltResult::ErrorRuntime(format!("Failed to set Company (Original ID: {}): {}", original_id_for_error, e).into(), Position::NONE)) - }) - }); - - let captured_db_for_get = Arc::clone(&db); - engine.register_fn("get_company_by_id", move |id_i64: i64| -> Result> { - let id_u32 = id_i64 as u32; // Assuming direct conversion is fine, or use a helper like in flow - captured_db_for_get.get_by_id(id_u32) - .map_err(|e| Box::new(EvalAltResult::ErrorRuntime(format!("Error getting Company (ID: {}): {}", id_u32, e).into(), Position::NONE)))? - .ok_or_else(|| Box::new(EvalAltResult::ErrorRuntime(format!("Company with ID {} not found", id_u32).into(), Position::NONE))) - }); - + // Register the exported module globally + let module = exported_module!(rhai_biz_module); engine.register_global_module(module.into()); + + // Create a new module for database operations + let mut db_module = Module::new(); + + // Database operations will obtain fresh collection handles directly. + + // Add database functions for Company + let db_for_set_company = Arc::clone(&db); + db_module.set_native_fn("set_company", move |company: Company| -> Result> { + let company_collection_set = db_for_set_company.collection::().expect("Failed to get company collection for set in closure"); + company_collection_set.set(&company) + .map(|(id_val, _)| id_val as INT) + .map_err(|e| Box::new(EvalAltResult::ErrorRuntime( + format!("Failed to save company: {:?}", e).into(), + Position::NONE + ))) + }); + + let db_for_get_company = Arc::clone(&db); + db_module.set_native_fn("get_company_by_id", move |id: INT| -> Result> { + let company_collection_get = db_for_get_company.collection::().expect("Failed to get company collection for get in closure"); + let id_u32 = id_from_i64_to_u32(id)?; + company_collection_get.get_by_id(id_u32) + .map(Dynamic::from) + .map_err(|e| Box::new(EvalAltResult::ErrorRuntime( + format!("Failed to get company with id {}: {:?}", id, e).into(), + Position::NONE + ))) + }); + + // Add database functions for Shareholder + let db_for_set_shareholder = Arc::clone(&db); + db_module.set_native_fn("set_shareholder", move |shareholder: Shareholder| -> Result> { + let shareholder_collection_set = db_for_set_shareholder.collection::().expect("Failed to get shareholder collection for set in closure"); + shareholder_collection_set.set(&shareholder) + .map(|(id_val, _)| id_val as INT) + .map_err(|e| Box::new(EvalAltResult::ErrorRuntime( + format!("Failed to save shareholder: {:?}", e).into(), + Position::NONE + ))) + }); + + let db_for_get_shareholder = Arc::clone(&db); + db_module.set_native_fn("get_shareholder_by_id", move |id: INT| -> Result> { + let shareholder_collection_get = db_for_get_shareholder.collection::().expect("Failed to get shareholder collection for get in closure"); + let id_u32 = id_from_i64_to_u32(id)?; + shareholder_collection_get.get_by_id(id_u32) + .map(Dynamic::from) + .map_err(|e| Box::new(EvalAltResult::ErrorRuntime( + format!("Failed to get shareholder with id {}: {:?}", id, e).into(), + Position::NONE + ))) + }); + + // Add database functions for Product + let db_for_set_product = Arc::clone(&db); + db_module.set_native_fn("set_product", move |product: Product| -> Result> { + let product_collection_set = db_for_set_product.collection::().expect("Failed to get product collection for set in closure"); + product_collection_set.set(&product) + .map(|(id_val, _)| id_val as INT) + .map_err(|e| Box::new(EvalAltResult::ErrorRuntime( + format!("Failed to save product: {:?}", e).into(), + Position::NONE + ))) + }); + + let db_for_get_product = Arc::clone(&db); + db_module.set_native_fn("get_product_by_id", move |id: INT| -> Result> { + let product_collection_get = db_for_get_product.collection::().expect("Failed to get product collection for get in closure"); + let id_u32 = id_from_i64_to_u32(id)?; + product_collection_get.get_by_id(id_u32) + .map(Dynamic::from) + .map_err(|e| Box::new(EvalAltResult::ErrorRuntime( + format!("Failed to get product with id {}: {:?}", id, e).into(), + Position::NONE + ))) + }); + + // Add database functions for Sale + let db_for_set_sale = Arc::clone(&db); + db_module.set_native_fn("set_sale", move |sale: Sale| -> Result> { + let sale_collection_set = db_for_set_sale.collection::().expect("Failed to get sale collection for set in closure"); + sale_collection_set.set(&sale) + .map(|(id_val, _)| id_val as INT) + .map_err(|e| Box::new(EvalAltResult::ErrorRuntime( + format!("Failed to save sale: {:?}", e).into(), + Position::NONE + ))) + }); + + let db_for_get_sale = Arc::clone(&db); + db_module.set_native_fn("get_sale_by_id", move |id: INT| -> Result> { + let sale_collection_get = db_for_get_sale.collection::().expect("Failed to get sale collection for get in closure"); + let id_u32 = id_from_i64_to_u32(id)?; + sale_collection_get.get_by_id(id_u32) + .map(Dynamic::from) + .map_err(|e| Box::new(EvalAltResult::ErrorRuntime( + format!("Failed to get sale with id {}: {:?}", id, e).into(), + Position::NONE + ))) + }); + + // Register the database module globally + engine.register_global_module(db_module.into()); + + println!("Successfully registered biz Rhai module using export_module approach."); } diff --git a/heromodels/src/models/biz/sale.rs b/heromodels/src/models/biz/sale.rs index 2d6d39f..a699329 100644 --- a/heromodels/src/models/biz/sale.rs +++ b/heromodels/src/models/biz/sale.rs @@ -1,5 +1,5 @@ use serde::{Deserialize, Serialize}; -use heromodels_core::{BaseModelData, Model}; +use heromodels_core::{BaseModelData, Model, BaseModelDataOps}; /// Represents the status of a sale. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] @@ -16,7 +16,7 @@ impl Default for SaleStatus { } /// Represents an individual item within a Sale. -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)] pub struct SaleItem { pub product_id: u32, pub name: String, // Denormalized product name at time of sale @@ -27,14 +27,14 @@ pub struct SaleItem { } impl SaleItem { - /// Creates a new `SaleItem`. - pub fn new(product_id: u32, name: String, quantity: i32, unit_price: f64, subtotal: f64) -> Self { + /// Creates a new `SaleItem` with default values. + pub fn new() -> Self { SaleItem { - product_id, - name, - quantity, - unit_price, - subtotal, + product_id: 0, + name: String::new(), + quantity: 0, + unit_price: 0.0, + subtotal: 0.0, service_active_until: None, } } @@ -45,8 +45,8 @@ impl SaleItem { self } - pub fn name(mut self, name: String) -> Self { - self.name = name; + pub fn name(mut self, name: impl ToString) -> Self { + self.name = name.to_string(); self } @@ -72,12 +72,12 @@ impl SaleItem { } /// Represents a sale of products or services. -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)] pub struct Sale { pub base_data: BaseModelData, pub company_id: u32, - pub buyer_name: String, - pub buyer_email: String, + pub buyer_id: u32, + pub transaction_id: u32, pub total_amount: f64, pub status: SaleStatus, pub sale_date: i64, @@ -99,24 +99,23 @@ impl Model for Sale { } } +impl BaseModelDataOps for Sale { + fn get_base_data_mut(&mut self) -> &mut BaseModelData { + &mut self.base_data + } +} + impl Sale { - /// Creates a new `Sale`. - pub fn new( - company_id: u32, - buyer_name: String, - buyer_email: String, - total_amount: f64, - status: SaleStatus, - sale_date: i64, - ) -> Self { + /// Creates a new `Sale` with default values. + pub fn new() -> Self { Sale { base_data: BaseModelData::new(), - company_id, - buyer_name, - buyer_email, - total_amount, - status, - sale_date, + company_id: 0, + buyer_id: 0, + transaction_id: 0, + total_amount: 0.0, + status: SaleStatus::default(), + sale_date: 0, items: Vec::new(), notes: String::new(), } @@ -128,13 +127,13 @@ impl Sale { self } - pub fn buyer_name(mut self, buyer_name: String) -> Self { - self.buyer_name = buyer_name; + pub fn buyer_id(mut self, buyer_id: u32) -> Self { + self.buyer_id = buyer_id; self } - pub fn buyer_email(mut self, buyer_email: String) -> Self { - self.buyer_email = buyer_email; + pub fn transaction_id(mut self, transaction_id: u32) -> Self { + self.transaction_id = transaction_id; self } @@ -163,40 +162,10 @@ impl Sale { self } - pub fn notes(mut self, notes: String) -> Self { - self.notes = notes; + pub fn notes(mut self, notes: impl ToString) -> Self { + self.notes = notes.to_string(); self } - // Builder methods for BaseModelData fields, prefixed with base_ - pub fn set_base_id(mut self, id: u32) -> Self { - self.base_data.id = id; - self - } - - // UUID is not part of BaseModelData directly in heromodels_core - // pub fn set_base_uuid(mut self, uuid: Option) -> Self { - // self.base_data.uuid = uuid; // Assuming uuid field exists if needed elsewhere - // self - // } - - pub fn set_base_created_at(mut self, created_at: i64) -> Self { - self.base_data.created_at = created_at; - self - } - - pub fn set_base_modified_at(mut self, modified_at: i64) -> Self { - self.base_data.modified_at = modified_at; - self - } - - pub fn add_base_comment(mut self, comment_id: u32) -> Self { - self.base_data.comments.push(comment_id); - self - } - - pub fn set_base_comments(mut self, comments: Vec) -> Self { - self.base_data.comments = comments; - self - } + // BaseModelData operations are now handled by BaseModelDataOps trait } diff --git a/heromodels/src/models/biz/shareholder.rs b/heromodels/src/models/biz/shareholder.rs index e449301..ffc8f53 100644 --- a/heromodels/src/models/biz/shareholder.rs +++ b/heromodels/src/models/biz/shareholder.rs @@ -1,5 +1,6 @@ use serde::{Deserialize, Serialize}; -use heromodels_core::{BaseModelData, Model}; +use heromodels_core::{BaseModelData, Model, BaseModelDataOps}; +use heromodels_derive::model; #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub enum ShareholderType { @@ -13,7 +14,8 @@ impl Default for ShareholderType { } } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +#[model] pub struct Shareholder { pub base_data: BaseModelData, pub company_id: u32, @@ -50,8 +52,8 @@ impl Shareholder { self } - pub fn name(mut self, name: String) -> Self { - self.name = name; + pub fn name(mut self, name: impl ToString) -> Self { + self.name = name.to_string(); self } @@ -75,28 +77,5 @@ impl Shareholder { self } - // Base data setters if needed for Rhai or specific scenarios - pub fn set_base_created_at(mut self, created_at: i64) -> Self { - self.base_data.created_at = created_at; - self - } - - pub fn set_base_modified_at(mut self, modified_at: i64) -> Self { - self.base_data.modified_at = modified_at; - self - } -} - -impl Model for Shareholder { - fn db_prefix() -> &'static str { - "shareholder" - } - - fn get_id(&self) -> u32 { - self.base_data.id - } - - fn base_data_mut(&mut self) -> &mut BaseModelData { - &mut self.base_data - } -} + // Base data operations are now handled by BaseModelDataOps trait +} \ No newline at end of file diff --git a/heromodels/src/models/calendar/calendar.rs b/heromodels/src/models/calendar/calendar.rs index 2ca849c..2eca950 100644 --- a/heromodels/src/models/calendar/calendar.rs +++ b/heromodels/src/models/calendar/calendar.rs @@ -1,21 +1,46 @@ use chrono::{DateTime, Utc}; use heromodels_core::BaseModelData; use heromodels_derive::model; -use rhai_autobind_macros::rhai_model_export; +// Temporarily removed to fix compilation issues +// use rhai_autobind_macros::rhai_model_export; use rhai::{CustomType, TypeBuilder}; use serde::{Deserialize, Serialize}; /// Represents the status of an attendee for an event -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)] pub enum AttendanceStatus { Accepted, Declined, Tentative, + #[default] NoResponse, } +impl AttendanceStatus { + /// Convert a string to an AttendanceStatus + pub fn from_string(s: &str) -> Result { + match s { + "Accepted" => Ok(AttendanceStatus::Accepted), + "Declined" => Ok(AttendanceStatus::Declined), + "Tentative" => Ok(AttendanceStatus::Tentative), + "NoResponse" => Ok(AttendanceStatus::NoResponse), + _ => Err(format!("Invalid attendance status: '{}'", s)), + } + } + + /// Convert an AttendanceStatus to a string + pub fn to_string(&self) -> String { + match self { + AttendanceStatus::Accepted => "Accepted".to_string(), + AttendanceStatus::Declined => "Declined".to_string(), + AttendanceStatus::Tentative => "Tentative".to_string(), + AttendanceStatus::NoResponse => "NoResponse".to_string(), + } + } +} + /// Represents an attendee of an event -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, CustomType)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, CustomType, Default)] pub struct Attendee { /// ID of the user attending // Assuming user_id might be queryable @@ -39,34 +64,51 @@ impl Attendee { } /// Represents an event in a calendar +#[model] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, CustomType)] pub struct Event { /// Base model data - #[serde(flatten)] pub base_data: BaseModelData, /// Title of the event + #[index] pub title: String, /// Optional description of the event pub description: Option, - /// Start time of the event - pub start_time: DateTime, - /// End time of the event - pub end_time: DateTime, + /// Start time of the event (Unix timestamp) + pub start_time: i64, + /// End time of the event (Unix timestamp) + pub end_time: i64, /// List of attendees for the event pub attendees: Vec, /// Optional location of the event pub location: Option, } -impl Event { - /// Creates a new event - pub fn new(title: impl ToString, start_time: DateTime, end_time: DateTime) -> Self { +impl Default for Event { + fn default() -> Self { + let now = chrono::Utc::now().timestamp(); Self { base_data: BaseModelData::new(), - title: title.to_string(), + title: String::new(), description: None, - start_time, - end_time, + start_time: now, + end_time: now + 3600, // Add 1 hour in seconds + attendees: Vec::new(), + location: None, + } + } +} + +impl Event { + /// Creates a new event + pub fn new() -> Self { + let now = chrono::Utc::now().timestamp(); + Self { + base_data: BaseModelData::new(), + title: String::new(), + description: None, + start_time: now, + end_time: now + 3600, // Add 1 hour in seconds attendees: Vec::new(), location: None, } @@ -116,8 +158,8 @@ impl Event { /// Reschedules the event to new start and end times pub fn reschedule( mut self, - new_start_time: DateTime, - new_end_time: DateTime, + new_start_time: i64, + new_end_time: i64, ) -> Self { // Basic validation: end_time should be after start_time if new_end_time > new_start_time { @@ -130,17 +172,18 @@ impl Event { } /// Represents a calendar with events -#[rhai_model_export( - db_type = "std::sync::Arc", -)] +// Temporarily removed rhai_model_export macro to fix compilation issues +// #[rhai_model_export( +// db_type = "std::sync::Arc", +// )] +#[derive(Debug, Clone, Serialize, Deserialize, Default)] #[model] -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, CustomType)] pub struct Calendar { /// Base model data - #[serde(flatten)] pub base_data: BaseModelData, /// Name of the calendar + #[index] pub name: String, /// Optional description of the calendar diff --git a/heromodels/src/models/calendar/mod.rs b/heromodels/src/models/calendar/mod.rs index 35fae98..fb0216a 100644 --- a/heromodels/src/models/calendar/mod.rs +++ b/heromodels/src/models/calendar/mod.rs @@ -4,4 +4,4 @@ pub mod rhai; // Re-export Calendar, Event, Attendee, and AttendanceStatus from the inner calendar module (calendar.rs) within src/models/calendar/mod.rs pub use self::calendar::{Calendar, Event, Attendee, AttendanceStatus}; -pub use rhai::register_rhai_engine_functions as register_calendar_rhai_module; +pub use rhai::register_calendar_rhai_module; diff --git a/heromodels/src/models/calendar/rhai.rs b/heromodels/src/models/calendar/rhai.rs index d4e1f12..07de7c8 100644 --- a/heromodels/src/models/calendar/rhai.rs +++ b/heromodels/src/models/calendar/rhai.rs @@ -1,90 +1,362 @@ -use rhai::{Engine, EvalAltResult, NativeCallContext, ImmutableString}; +use rhai::plugin::*; +use rhai::{Engine, EvalAltResult, Position, Module, INT, Dynamic, Array}; use std::sync::Arc; +use std::mem; +use chrono::{DateTime, Utc}; +use crate::db::Db; -use heromodels_core::BaseModelData; +use super::calendar::{Event, Attendee, Calendar, AttendanceStatus}; +type RhaiEvent = Event; +type RhaiAttendee = Attendee; +type RhaiCalendar = Calendar; use crate::db::hero::OurDB; -use super::calendar::{Calendar, Event, Attendee, AttendanceStatus}; -use adapter_macros::{adapt_rhai_i64_input_fn, adapt_rhai_i64_input_method}; -use adapter_macros::rhai_timestamp_helpers; +use crate::db::Collection; -// Helper function for get_all_calendars registration - - -fn new_calendar_rhai(name: String) -> Result> { - Ok(Calendar::new(None, name)) +// Helper to convert i64 from Rhai to u32 for IDs +fn id_from_i64_to_u32(id_i64: i64) -> Result> { + u32::try_from(id_i64).map_err(|_| + Box::new(EvalAltResult::ErrorArithmetic( + format!("Failed to convert ID '{}' to u32", id_i64).into(), + Position::NONE + )) + ) } -fn new_event_rhai( - context: NativeCallContext, - title_rhai: ImmutableString, - start_time_ts: i64, - end_time_ts: i64, -) -> Result> { - let start_time = rhai_timestamp_helpers::rhai_timestamp_to_datetime(start_time_ts) - .map_err(|e_str| Box::new(EvalAltResult::ErrorRuntime( - format!("Failed to convert start_time for Event: {}", e_str).into(), - context.position(), - )))?; - - let end_time = rhai_timestamp_helpers::rhai_timestamp_to_datetime(end_time_ts) - .map_err(|e_str| Box::new(EvalAltResult::ErrorRuntime( - format!("Failed to convert end_time for Event: {}", e_str).into(), - context.position(), - )))?; - - Ok(Event::new(title_rhai.to_string(), start_time, end_time)) +// Helper to convert i64 from Rhai to u64 for timestamps or other large numbers +fn val_from_i64_to_u64(val_i64: i64) -> Result> { + u64::try_from(val_i64).map_err(|_| + Box::new(EvalAltResult::ErrorArithmetic( + format!("Failed to convert value '{}' to u64", val_i64).into(), + Position::NONE + )) + ) } -pub fn register_rhai_engine_functions(engine: &mut Engine, db: Arc) { - engine.register_fn("name", move |calendar: Calendar, name: String| Calendar::name(calendar, name)); - engine.register_fn("description", move |calendar: Calendar, description: String| Calendar::description(calendar, description)); - engine.register_fn("add_event", Calendar::add_event); - // Note: Event IDs are i64 in Calendar.events, but Event model's base_data.id is u32. - // This might require adjustment if events are fetched by ID from the DB via Calendar.events. +#[export_module] +mod rhai_calendar_module { + // --- Event Functions --- + #[rhai_fn(name = "new_event")] + pub fn new_event() -> RhaiEvent { + Event::new() + } - engine.register_fn("new_event", |context: NativeCallContext, title_rhai: ImmutableString, start_time_ts: i64, end_time_ts: i64| -> Result> { - new_event_rhai(context, title_rhai, start_time_ts, end_time_ts) - }); - engine.register_fn("title", move |event: Event, title: String| Event::title(event, title)); - engine.register_fn("description", move |event: Event, description: String| Event::description(event, description)); - engine.register_fn("add_attendee", move |event: Event, attendee: Attendee| Event::add_attendee(event, attendee)); - engine.register_fn("remove_attendee", adapt_rhai_i64_input_method!(Event, remove_attendee, u32)); - engine.register_fn("update_attendee_status", move |context: NativeCallContext, event: Event, contact_id_i64: i64, status: AttendanceStatus| -> Result> { - let contact_id_u32: u32 = contact_id_i64.try_into().map_err(|_e| { - Box::new(EvalAltResult::ErrorArithmetic( - format!("Conversion error for contact_id in Event::update_attendee_status from i64 to u32"), - context.position(), - )) - })?; - Ok(event.update_attendee_status(contact_id_u32, status)) - }); - - engine.register_fn("new_attendee", adapt_rhai_i64_input_fn!(Attendee::new, u32)); - - engine.register_fn("new_calendar", |name: String| -> Result> { new_calendar_rhai(name) }); - - // Register a function to get the database instance - engine.register_fn("get_db", move || db.clone()); + /// Sets the event title + #[rhai_fn(name = "title", return_raw, global, pure)] + pub fn event_title(event: &mut RhaiEvent, title: String) -> Result> { + let owned_event = mem::take(event); + *event = owned_event.title(title); + Ok(event.clone()) + } - // Register getters for Calendar - engine.register_get("id", |c: &mut Calendar| -> Result> { Ok(c.base_data.id as i64) }); - engine.register_get("name", |c: &mut Calendar| -> Result> { - // println!("Rhai attempting to get Calendar.name: {}", c.name); // Debug print - Ok(c.name.clone()) - }); - engine.register_get("description", |c: &mut Calendar| -> Result, Box> { Ok(c.description.clone()) }); + /// Sets the event description + #[rhai_fn(name = "description", return_raw, global, pure)] + pub fn event_description(event: &mut RhaiEvent, description: String) -> Result> { + let owned_event = mem::take(event); + *event = owned_event.description(description); + Ok(event.clone()) + } + + /// Sets the event location + #[rhai_fn(name = "location", return_raw, global, pure)] + pub fn event_location(event: &mut RhaiEvent, location: String) -> Result> { + let owned_event = mem::take(event); + *event = owned_event.location(location); + Ok(event.clone()) + } - // Register getter for Calendar.base_data - engine.register_get("base_data", |c: &mut Calendar| -> Result> { Ok(c.base_data.clone()) }); + /// Adds an attendee to the event + #[rhai_fn(name = "add_attendee", return_raw, global, pure)] + pub fn event_add_attendee(event: &mut RhaiEvent, attendee: RhaiAttendee) -> Result> { + // Use take to get ownership of the event + let owned_event = mem::take(event); + *event = owned_event.add_attendee(attendee); + Ok(event.clone()) + } - // Register getters for BaseModelData - engine.register_get("id", |bmd: &mut BaseModelData| -> Result> { Ok(bmd.id.into()) }); + /// Reschedules the event with new start and end times + #[rhai_fn(name = "reschedule", return_raw, global, pure)] + pub fn event_reschedule(event: &mut RhaiEvent, new_start_time: i64, new_end_time: i64) -> Result> { + // Validate timestamps + if new_end_time <= new_start_time { + return Err(Box::new(EvalAltResult::ErrorRuntime( + "End time must be after start time".into(), + Position::NONE + ))); + } + + // Use take to get ownership of the event + let owned_event = mem::take(event); + *event = owned_event.reschedule(new_start_time, new_end_time); + Ok(event.clone()) + } + + /// Updates an attendee's status in the event + #[rhai_fn(name = "update_attendee_status", return_raw, global, pure)] + pub fn event_update_attendee_status(event: &mut RhaiEvent, contact_id: i64, status_str: String) -> Result> { + let status_enum = AttendanceStatus::from_string(&status_str) + .map_err(|_| Box::new(EvalAltResult::ErrorRuntime( + format!("Invalid attendance status: '{}'. Expected one of: Pending, Accepted, Declined, Tentative", status_str).into(), + Position::NONE + )))?; + + // Use take to get ownership of the event + let owned_event = mem::take(event); + *event = owned_event.update_attendee_status(id_from_i64_to_u32(contact_id)?, status_enum); + Ok(event.clone()) + } - // Database interaction functions for Calendar are expected to be provided by #[rhai_model_export(..)] on the Calendar struct. - // Ensure that `db.rs` or similar correctly wires up the `OurDB` methods for these. + // Event Getters + #[rhai_fn(get = "id", pure)] + pub fn get_event_id(event: &mut RhaiEvent) -> i64 { event.base_data.id as i64 } + #[rhai_fn(get = "created_at", pure)] + pub fn get_event_created_at(event: &mut RhaiEvent) -> i64 { event.base_data.created_at } + #[rhai_fn(get = "modified_at", pure)] + pub fn get_event_modified_at(event: &mut RhaiEvent) -> i64 { event.base_data.modified_at } + + #[rhai_fn(get = "title", pure)] + pub fn get_event_title(event: &mut RhaiEvent) -> String { event.title.clone() } + #[rhai_fn(get = "description", pure)] + pub fn get_event_description(event: &mut RhaiEvent) -> Option { event.description.clone() } + #[rhai_fn(get = "start_time", pure)] + pub fn get_event_start_time(event: &mut RhaiEvent) -> i64 { event.start_time } + #[rhai_fn(get = "end_time", pure)] + pub fn get_event_end_time(event: &mut RhaiEvent) -> i64 { event.end_time } + #[rhai_fn(get = "location", pure)] + pub fn get_event_location(event: &mut RhaiEvent) -> Option { event.location.clone() } + #[rhai_fn(get = "attendees", pure)] + pub fn get_event_attendees(event: &mut RhaiEvent) -> Vec { event.attendees.clone() } + + // --- Attendee Functions --- + #[rhai_fn(name = "new_attendee")] + pub fn new_attendee() -> RhaiAttendee { + Attendee::new(0) // Default contact_id, will be set via builder + } + + /// Sets the contact ID for an attendee + #[rhai_fn(name = "with_contact_id", return_raw, global, pure)] + pub fn attendee_with_contact_id(attendee: &mut RhaiAttendee, contact_id: i64) -> Result> { + let new_contact_id = id_from_i64_to_u32(contact_id).unwrap_or(0); + let owned_attendee = mem::replace(attendee, Attendee::new(0)); + *attendee = Attendee::new(new_contact_id); + attendee.status = owned_attendee.status; + Ok(attendee.clone()) + } + + /// Sets the status for an attendee + #[rhai_fn(name = "with_status", return_raw, global, pure)] + pub fn attendee_with_status(attendee: &mut RhaiAttendee, status_str: String) -> Result> { + let status_enum = AttendanceStatus::from_string(&status_str) + .map_err(|_| Box::new(EvalAltResult::ErrorRuntime( + format!("Invalid attendance status: '{}'. Expected one of: Accepted, Declined, Tentative, NoResponse", status_str).into(), + Position::NONE + )))?; + + let owned_attendee = mem::replace(attendee, Attendee::new(0)); + *attendee = owned_attendee.status(status_enum); + Ok(attendee.clone()) + } + + // We now use with_status instead of update_attendee_status for consistency + + // Attendee Getters + #[rhai_fn(get = "contact_id", pure)] + pub fn get_attendee_contact_id(attendee: &mut RhaiAttendee) -> i64 { attendee.contact_id as i64 } + #[rhai_fn(get = "status", pure)] + pub fn get_attendee_status(attendee: &mut RhaiAttendee) -> String { attendee.status.to_string() } + + // --- Calendar Functions --- + #[rhai_fn(name = "new_calendar", return_raw)] + pub fn new_calendar() -> Result> { + let calendar = Calendar::new(None, ""); + Ok(calendar) + } + + /// Sets the calendar name + #[rhai_fn(name = "name", return_raw, global, pure)] + pub fn calendar_name(calendar: &mut RhaiCalendar, name: String) -> Result> { + // Create a default Calendar to replace the taken one + let default_calendar = Calendar::new(None, ""); + + // Take ownership of the calendar, apply the builder method, then put it back + let owned_calendar = std::mem::replace(calendar, default_calendar); + *calendar = Calendar::new(Some(owned_calendar.base_data.id), name); + Ok(calendar.clone()) + } + + /// Sets the calendar description + #[rhai_fn(name = "description", return_raw, global, pure)] + pub fn calendar_description(calendar: &mut RhaiCalendar, description: String) -> Result> { + // Create a default Calendar to replace the taken one + let default_calendar = Calendar::new(None, ""); + + // Take ownership of the calendar, apply the builder method, then put it back + let owned_calendar = std::mem::replace(calendar, default_calendar); + let updated_calendar = owned_calendar.description(description); + *calendar = updated_calendar.clone(); + Ok(updated_calendar) + } + + #[rhai_fn(name = "add_event_to_calendar", return_raw, global, pure)] + pub fn calendar_add_event(calendar: &mut RhaiCalendar, event: RhaiEvent) -> Result> { + // Create a default Calendar to replace the taken one + let default_calendar = Calendar::new(None, ""); + + // Take ownership of the calendar, apply the builder method, then put it back + let owned_calendar = std::mem::replace(calendar, default_calendar); + *calendar = owned_calendar.add_event(event.base_data.id as i64); + Ok(calendar.clone()) + } + + #[rhai_fn(name = "remove_event_from_calendar", return_raw)] + pub fn calendar_remove_event(calendar: &mut RhaiCalendar, event_id: i64) -> Result<(), Box> { + // Create a default Calendar to replace the taken one + let default_calendar = Calendar::new(None, ""); + + // Take ownership of the calendar, apply the builder method, then put it back + let owned_calendar = std::mem::replace(calendar, default_calendar); + *calendar = owned_calendar.remove_event(id_from_i64_to_u32(event_id)? as i64); + Ok(()) + } + + // Calendar Getters + #[rhai_fn(get = "id", pure)] + pub fn get_calendar_id(calendar: &mut RhaiCalendar) -> i64 { calendar.base_data.id as i64 } + + #[rhai_fn(get = "name", pure)] + pub fn get_calendar_name(calendar: &mut RhaiCalendar) -> String { calendar.name.clone() } + + #[rhai_fn(get = "created_at", pure)] + pub fn get_calendar_created_at(calendar: &mut RhaiCalendar) -> i64 { calendar.base_data.created_at } + + #[rhai_fn(get = "modified_at", pure)] + pub fn get_calendar_modified_at(calendar: &mut RhaiCalendar) -> i64 { calendar.base_data.modified_at } + + #[rhai_fn(get = "events", pure)] + pub fn get_calendar_events(calendar: &mut RhaiCalendar) -> Vec { calendar.events.clone() } + + #[rhai_fn(get = "description", pure)] + pub fn get_calendar_description(calendar: &mut RhaiCalendar) -> Option { calendar.description.clone() } + + // Calendar doesn't have an owner_id field in the current implementation + // pub fn get_calendar_owner_id(calendar: &mut RhaiCalendar) -> i64 { calendar.owner_id as i64 } - // Getters for Event - engine.register_get("id", |e: &mut Event| -> Result> { Ok(e.base_data.id as i64) }); - engine.register_get("title", |e: &mut Event| -> Result> { Ok(e.title.clone()) }); - // Add more getters for Event fields as needed +} + +pub fn register_calendar_rhai_module(engine: &mut Engine, db: Arc) { + // Register the exported module globally + let module = exported_module!(rhai_calendar_module); + engine.register_global_module(module.into()); + + // Create a module for database functions + let mut db_module = Module::new(); + + // Manually register database functions as they need to capture 'db' + let db_clone_set_event = db.clone(); + db_module.set_native_fn("save_event", move |event: Event| -> Result> { + // Use the Collection trait method directly + let result = db_clone_set_event.set(&event) + .map_err(|e| Box::new(EvalAltResult::ErrorRuntime(format!("DB Error set_event: {}", e).into(), Position::NONE)))?; + + // Return the updated event with the correct ID + Ok(result.1) + }); + + // Manually register database functions as they need to capture 'db' + let db_clone_delete_event = db.clone(); + db_module.set_native_fn("delete_event", move |event: Event| -> Result<(), Box> { + // Use the Collection trait method directly + let result = db_clone_delete_event.collection::() + .expect("can open event collection") + .delete_by_id(event.base_data.id) + .expect("can delete event"); + + // Return the updated event with the correct ID + Ok(result) + }); + + let db_clone_get_event = db.clone(); + db_module.set_native_fn("get_event_by_id", move |id_i64: INT| -> Result> { + let id_u32 = id_from_i64_to_u32(id_i64)?; + // Use the Collection trait method directly + db_clone_get_event.get_by_id(id_u32) + .map_err(|e| Box::new(EvalAltResult::ErrorRuntime(format!("DB Error get_event_by_id: {}", e).into(), Position::NONE)))? + .ok_or_else(|| Box::new(EvalAltResult::ErrorRuntime(format!("Event with ID {} not found", id_u32).into(), Position::NONE))) + }); + + let db_clone_set_calendar = db.clone(); + db_module.set_native_fn("save_calendar", move |calendar: Calendar| -> Result> { + // Use the Collection trait method directly + let result = db_clone_set_calendar.set(&calendar) + .map_err(|e| Box::new(EvalAltResult::ErrorRuntime(format!("DB Error set_calendar: {}", e).into(), Position::NONE)))?; + + // Return the updated calendar with the correct ID + Ok(result.1) + }); + + // Manually register database functions as they need to capture 'db' + let db_clone_delete_calendar = db.clone(); + db_module.set_native_fn("delete_calendar", move |calendar: Calendar| -> Result<(), Box> { + // Use the Collection trait method directly + let result = db_clone_delete_calendar.collection::() + .expect("can open calendar collection") + .delete_by_id(calendar.base_data.id) + .expect("can delete event"); + + // Return the updated event with the correct ID + Ok(result) + }); + + let db_clone_get_calendar = db.clone(); + db_module.set_native_fn("get_calendar_by_id", move |id_i64: INT| -> Result> { + let id_u32 = id_from_i64_to_u32(id_i64)?; + // Use the Collection trait method directly + db_clone_get_calendar.get_by_id(id_u32) + .map_err(|e| Box::new(EvalAltResult::ErrorRuntime(format!("DB Error get_calendar_by_id: {}", e).into(), Position::NONE)))? + .ok_or_else(|| Box::new(EvalAltResult::ErrorRuntime(format!("Calendar with ID {} not found", id_u32).into(), Position::NONE))) + }); + + // Add list_calendars function to get all calendars + let db_clone_list_calendars = db.clone(); + db_module.set_native_fn("list_calendars", move || -> Result> { + let collection = db_clone_list_calendars.collection::() + .map_err(|e| Box::new(EvalAltResult::ErrorRuntime( + format!("Failed to get calendar collection: {:?}", e).into(), + Position::NONE + )))?; + let calendars = collection.get_all() + .map_err(|e| Box::new(EvalAltResult::ErrorRuntime( + format!("Failed to get all calendars: {:?}", e).into(), + Position::NONE + )))?; + let mut array = Array::new(); + for calendar in calendars { + array.push(Dynamic::from(calendar)); + } + Ok(Dynamic::from(array)) + }); + + // Add list_events function to get all events + let db_clone_list_events = db.clone(); + db_module.set_native_fn("list_events", move || -> Result> { + let collection = db_clone_list_events.collection::() + .map_err(|e| Box::new(EvalAltResult::ErrorRuntime( + format!("Failed to get event collection: {:?}", e).into(), + Position::NONE + )))?; + let events = collection.get_all() + .map_err(|e| Box::new(EvalAltResult::ErrorRuntime( + format!("Failed to get all events: {:?}", e).into(), + Position::NONE + )))?; + let mut array = Array::new(); + for event in events { + array.push(Dynamic::from(event)); + } + Ok(Dynamic::from(array)) + }); + + // Register the database module globally + engine.register_global_module(db_module.into()); + + println!("Successfully registered calendar Rhai module using export_module approach."); } diff --git a/heromodels/src/models/core/comment.rs b/heromodels/src/models/core/comment.rs index 28bcefd..0122b02 100644 --- a/heromodels/src/models/core/comment.rs +++ b/heromodels/src/models/core/comment.rs @@ -1,36 +1,43 @@ +// heromodels/src/models/core/comment.rs use heromodels_core::BaseModelData; use heromodels_derive::model; use serde::{Deserialize, Serialize}; -/// Represents a comment on a model -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)] // Added PartialEq #[model] pub struct Comment { - pub base_data: BaseModelData, + pub base_data: BaseModelData, // Provides id, created_at, updated_at #[index] - pub user_id: u32, - pub content: String, + pub user_id: u32, // Maps to commenter_id + pub content: String, // Maps to text + pub parent_comment_id: Option, // For threaded comments } impl Comment { - /// Create a new comment with auto-generated ID pub fn new() -> Self { Self { base_data: BaseModelData::new(), - user_id: 0, + user_id: 0, // Default, should be set via builder or method content: String::new(), + parent_comment_id: None, } } - /// Set the user ID + // Builder method for user_id pub fn user_id(mut self, id: u32) -> Self { self.user_id = id; self } - /// Set the content + // Builder method for content pub fn content(mut self, content: impl ToString) -> Self { self.content = content.to_string(); self } + + // Builder method for parent_comment_id + pub fn parent_comment_id(mut self, parent_id: Option) -> Self { + self.parent_comment_id = parent_id; + self + } } diff --git a/heromodels/src/models/finance/account.rs b/heromodels/src/models/finance/account.rs index d6f9648..f225fc4 100644 --- a/heromodels/src/models/finance/account.rs +++ b/heromodels/src/models/finance/account.rs @@ -8,7 +8,7 @@ use heromodels_core::BaseModelData; use super::asset::Asset; /// Account represents a financial account owned by a user -#[derive(Debug, Clone, Serialize, Deserialize, CustomType)] +#[derive(Debug, Clone, Serialize, Deserialize, CustomType, Default)] #[model] // Has base.Base in V spec pub struct Account { pub base_data: BaseModelData, @@ -18,82 +18,75 @@ pub struct Account { pub ledger: String, // describes the ledger/blockchain where the account is located pub address: String, // address of the account on the blockchain pub pubkey: String, // public key - pub assets: Vec, // list of assets in this account + pub assets: Vec, // list of assets in this account } impl 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: Option, - 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); - } - + /// Create a new account with default values + pub fn new() -> Self { Self { - base_data, - name: name.to_string(), - user_id, - description: description.to_string(), - ledger: ledger.to_string(), - address: address.to_string(), - pubkey: pubkey.to_string(), + base_data: BaseModelData::new(), + name: String::new(), + user_id: 0, + description: String::new(), + ledger: String::new(), + address: String::new(), + pubkey: String::new(), assets: Vec::new(), } } + /// Set the name of the account + pub fn name(mut self, name: impl ToString) -> Self { + self.name = name.to_string(); + self + } + + /// Set the user ID of the account owner + pub fn user_id(mut self, user_id: u32) -> Self { + self.user_id = user_id; + self + } + + /// Set the description of the account + pub fn description(mut self, description: impl ToString) -> Self { + self.description = description.to_string(); + self + } + + /// Set the ledger/blockchain where the account is located + pub fn ledger(mut self, ledger: impl ToString) -> Self { + self.ledger = ledger.to_string(); + self + } + + /// Set the address of the account on the blockchain + pub fn address(mut self, address: impl ToString) -> Self { + self.address = address.to_string(); + self + } + + /// Set the public key of the account + pub fn pubkey(mut self, pubkey: impl ToString) -> Self { + self.pubkey = pubkey.to_string(); + self + } + /// Add an asset to the account - pub fn add_asset(mut self, asset: Asset) -> Self { - self.assets.push(asset); + pub fn add_asset(mut self, asset_id: u32) -> Self { + self.assets.push(asset_id); self } /// Get the total value of all assets in the account pub fn total_value(&self) -> f64 { - self.assets.iter().map(|asset| asset.amount).sum() + /// TODO: implement + 0.0 } /// Find an asset by name pub fn find_asset_by_name(&self, name: &str) -> Option<&Asset> { - self.assets.iter().find(|asset| asset.name == name) - } - - /// Update the account details - pub fn update_details( - mut self, - name: Option, - description: Option, - address: Option, - pubkey: Option, - ) -> Self { - if let Some(name) = name { - self.name = name.to_string(); - } - if let Some(description) = description { - self.description = description.to_string(); - } - if let Some(address) = address { - self.address = address.to_string(); - } - if let Some(pubkey) = pubkey { - self.pubkey = pubkey.to_string(); - } - self + /// TODO: implement + return None } } diff --git a/heromodels/src/models/finance/asset.rs b/heromodels/src/models/finance/asset.rs index 40a5acf..20dbff1 100644 --- a/heromodels/src/models/finance/asset.rs +++ b/heromodels/src/models/finance/asset.rs @@ -21,7 +21,7 @@ impl Default for AssetType { } /// Asset represents a digital asset or token -#[derive(Debug, Clone, Serialize, Deserialize, CustomType)] +#[derive(Debug, Clone, Serialize, Deserialize, CustomType, Default)] #[model] // Has base.Base in V spec pub struct Asset { pub base_data: BaseModelData, @@ -34,47 +34,55 @@ pub struct Asset { } impl 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: Option, - name: impl ToString, - description: impl ToString, - amount: f64, - address: impl ToString, - asset_type: AssetType, - decimals: u8, - ) -> Self { - let mut base_data = BaseModelData::new(); - if let Some(id) = id { - base_data.update_id(id); - } - + /// Create a new asset with default values + pub fn new() -> Self { Self { - base_data, - name: name.to_string(), - description: description.to_string(), - amount, - address: address.to_string(), - asset_type, - decimals, + base_data: BaseModelData::new(), + name: String::new(), + description: String::new(), + amount: 0.0, + address: String::new(), + asset_type: AssetType::default(), + decimals: 18, // Default for most tokens } } - /// Update the asset amount - pub fn update_amount(mut self, amount: f64) -> Self { + /// Set the name of the asset + pub fn name(mut self, name: impl ToString) -> Self { + self.name = name.to_string(); + self + } + + /// Set the description of the asset + pub fn description(mut self, description: impl ToString) -> Self { + self.description = description.to_string(); + self + } + + /// Set the amount of the asset + pub fn amount(mut self, amount: f64) -> Self { self.amount = amount; self } + /// Set the address of the asset on the blockchain + pub fn address(mut self, address: impl ToString) -> Self { + self.address = address.to_string(); + self + } + + /// Set the type of the asset + pub fn asset_type(mut self, asset_type: AssetType) -> Self { + self.asset_type = asset_type; + self + } + + /// Set the number of decimals of the asset + pub fn decimals(mut self, decimals: u8) -> Self { + self.decimals = decimals; + self + } + /// Get the formatted amount with proper decimal places pub fn formatted_amount(&self) -> String { let factor = 10_f64.powi(self.decimals as i32); diff --git a/heromodels/src/models/finance/marketplace.rs b/heromodels/src/models/finance/marketplace.rs index 701e1cd..f82bd90 100644 --- a/heromodels/src/models/finance/marketplace.rs +++ b/heromodels/src/models/finance/marketplace.rs @@ -54,6 +54,7 @@ impl Default for BidStatus { /// Bid represents a bid on an auction listing #[derive(Debug, Clone, Serialize, Deserialize, CustomType)] +#[derive(Default)] 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 @@ -64,32 +65,44 @@ pub struct Bid { } impl Bid { - /// Create a new bid - pub fn new( - listing_id: impl ToString, - bidder_id: u32, - amount: f64, - currency: impl ToString, - ) -> Self { - Self { - listing_id: listing_id.to_string(), - bidder_id, - amount, - currency: currency.to_string(), - status: BidStatus::default(), - created_at: Utc::now(), - } + /// Create a new bid with default values + pub fn new() -> Self { + Self::default() } - /// Update the status of the bid - pub fn update_status(mut self, status: BidStatus) -> Self { + /// Set the listing ID for the bid + pub fn listing_id(mut self, listing_id: impl ToString) -> Self { + self.listing_id = listing_id.to_string(); + self + } + + /// Set the bidder ID for the bid + pub fn bidder_id(mut self, bidder_id: u32) -> Self { + self.bidder_id = bidder_id; + self + } + + /// Set the amount for the bid + pub fn amount(mut self, amount: f64) -> Self { + self.amount = amount; + self + } + + /// Set the currency for the bid + pub fn currency(mut self, currency: impl ToString) -> Self { + self.currency = currency.to_string(); + self + } + + /// Set the status of the bid + pub fn status(mut self, status: BidStatus) -> Self { self.status = status; self } } /// Listing represents a marketplace listing for an asset -#[derive(Debug, Clone, Serialize, Deserialize, CustomType)] +#[derive(Debug, Clone, Serialize, Deserialize, CustomType, Default)] #[model] // Has base.Base in V spec pub struct Listing { pub base_data: BaseModelData, @@ -112,66 +125,82 @@ pub struct Listing { } impl 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: Option, - title: impl ToString, - description: impl ToString, - asset_id: impl ToString, - asset_type: AssetType, - seller_id: impl ToString, - price: f64, - currency: impl ToString, - listing_type: ListingType, - expires_at: Option>, - tags: Vec, - image_url: Option, - ) -> Self { - let mut base_data = BaseModelData::new(); - if let Some(id) = id { - base_data.update_id(id); - } + /// Create a new listing with default values + pub fn new() -> Self { + Self::default() + } - Self { - base_data, - title: title.to_string(), - description: description.to_string(), - asset_id: asset_id.to_string(), - asset_type, - seller_id: seller_id.to_string(), - price, - currency: currency.to_string(), - listing_type, - status: ListingStatus::default(), - expires_at, - sold_at: None, - buyer_id: None, - sale_price: None, - bids: Vec::new(), - tags, - image_url: image_url.map(|url| url.to_string()), - } + /// Set the title of the listing + pub fn title(mut self, title: impl ToString) -> Self { + self.title = title.to_string(); + self + } + + /// Set the description of the listing + pub fn description(mut self, description: impl ToString) -> Self { + self.description = description.to_string(); + self + } + + /// Set the asset ID of the listing + pub fn asset_id(mut self, asset_id: impl ToString) -> Self { + self.asset_id = asset_id.to_string(); + self + } + + /// Set the asset type of the listing + pub fn asset_type(mut self, asset_type: AssetType) -> Self { + self.asset_type = asset_type; + self + } + + /// Set the seller ID of the listing + pub fn seller_id(mut self, seller_id: impl ToString) -> Self { + self.seller_id = seller_id.to_string(); + self + } + + /// Set the price of the listing + pub fn price(mut self, price: f64) -> Self { + self.price = price; + self + } + + /// Set the currency of the listing + pub fn currency(mut self, currency: impl ToString) -> Self { + self.currency = currency.to_string(); + self + } + + /// Set the listing type + pub fn listing_type(mut self, listing_type: ListingType) -> Self { + self.listing_type = listing_type; + self + } + + /// Set the status of the listing + pub fn status(mut self, status: ListingStatus) -> Self { + self.status = status; + self + } + + /// Set the expiration date of the listing + pub fn expires_at(mut self, expires_at: Option>) -> Self { + self.expires_at = expires_at; + self + } + + /// Set the image URL of the listing + pub fn image_url(mut self, image_url: Option) -> Self { + self.image_url = image_url.map(|url| url.to_string()); + self } /// Add a bid to an auction listing pub fn add_bid(mut self, bid: Bid) -> Result { // Check if listing is an auction if self.listing_type != ListingType::Auction { - return Err("Bids can only be placed on auction listings"); + return Err("Cannot add bid to non-auction listing"); } // Check if listing is active @@ -210,27 +239,51 @@ impl Listing { .max_by(|a, b| a.amount.partial_cmp(&b.amount).unwrap()) } + /// Set the buyer ID for completing a sale + pub fn buyer_id(mut self, buyer_id: impl ToString) -> Self { + self.buyer_id = Some(buyer_id.to_string()); + self + } + + /// Set the sale price for completing a sale + pub fn sale_price(mut self, sale_price: f64) -> Self { + self.sale_price = Some(sale_price); + self + } + + /// Set the sold date for completing a sale + pub fn sold_at(mut self, sold_at: Option>) -> Self { + self.sold_at = sold_at; + self + } + /// Complete a sale (fixed price or auction) - pub fn complete_sale( - mut self, - buyer_id: impl ToString, - sale_price: f64, - ) -> Result { + pub fn complete_sale(mut self) -> Result { if self.status != ListingStatus::Active { return Err("Cannot complete sale for inactive listing"); } + if self.buyer_id.is_none() { + return Err("Buyer ID must be set before completing sale"); + } + + if self.sale_price.is_none() { + return Err("Sale price must be set before completing sale"); + } + 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 self.sold_at.is_none() { + self.sold_at = Some(Utc::now()); + } // If this was an auction, accept the winning bid and reject others if self.listing_type == ListingType::Auction { + let buyer_id_str = self.buyer_id.as_ref().unwrap().to_string(); + let sale_price_val = self.sale_price.unwrap(); + 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() == buyer_id_str && bid.amount == sale_price_val { bid.status = BidStatus::Accepted; } else { bid.status = BidStatus::Rejected; @@ -279,34 +332,11 @@ impl Listing { self } - /// Add tags to the listing - pub fn add_tags(mut self, tags: Vec) -> Self { - for tag in tags { - self.tags.push(tag.to_string()); - } + /// Add a single tag to the listing + pub fn add_tag(mut self, tag: impl ToString) -> Self { + self.tags.push(tag.to_string()); self } - /// Update the listing details - pub fn update_details( - mut self, - title: Option, - description: Option, - price: Option, - image_url: Option, - ) -> Self { - if let Some(title) = title { - self.title = title.to_string(); - } - if let Some(description) = description { - self.description = description.to_string(); - } - if let Some(price) = price { - self.price = price; - } - if let Some(image_url) = image_url { - self.image_url = Some(image_url.to_string()); - } - self - } + // update_details method removed as we now have individual setter methods for each field } diff --git a/heromodels/src/models/finance/rhai.rs b/heromodels/src/models/finance/rhai.rs index 01cb4f1..d58db70 100644 --- a/heromodels/src/models/finance/rhai.rs +++ b/heromodels/src/models/finance/rhai.rs @@ -1,346 +1,722 @@ -use rhai::{Engine, Array, Dynamic, ImmutableString, INT, EvalAltResult, NativeCallContext}; -use std::sync::{Arc, Mutex}; +use rhai::plugin::*; +use rhai::{Engine, EvalAltResult, Position, Module, INT, Dynamic, Array}; +use std::sync::Arc; +use std::mem; +use chrono::{DateTime, Utc}; + +use super::account::Account; +use super::asset::{Asset, AssetType}; +use super::marketplace::{Listing, Bid, ListingStatus, ListingType, BidStatus}; + +use crate::db::hero::OurDB; +use crate::db::{Collection, Db}; +use heromodels_core::Model; + +type RhaiAccount = Account; +type RhaiAsset = Asset; +type RhaiListing = Listing; +type RhaiBid = Bid; + +// Helper to convert i64 from Rhai to u32 for IDs +fn id_from_i64_to_u32(id_i64: i64) -> Result> { + u32::try_from(id_i64).map_err(|_| + Box::new(EvalAltResult::ErrorArithmetic( + format!("Failed to convert ID '{}' to u32", id_i64).into(), + Position::NONE + )) + ) +} + +// Helper functions for enum conversions + +// AssetType conversions +fn asset_type_to_string(asset_type: &AssetType) -> String { + match asset_type { + AssetType::Native => "Native".to_string(), + AssetType::Erc20 => "Erc20".to_string(), + AssetType::Erc721 => "Erc721".to_string(), + AssetType::Erc1155 => "Erc1155".to_string(), + } +} + +fn string_to_asset_type(s: &str) -> Result> { + match s { + "Native" => Ok(AssetType::Native), + "Erc20" => Ok(AssetType::Erc20), + "Erc721" => Ok(AssetType::Erc721), + "Erc1155" => Ok(AssetType::Erc1155), + _ => Err(Box::new(EvalAltResult::ErrorRuntime( + format!("Invalid asset type: '{}'. Expected one of: Native, Erc20, Erc721, Erc1155", s).into(), + Position::NONE + ))), + } +} + +// ListingStatus conversions +fn listing_status_to_string(status: &ListingStatus) -> String { + match status { + ListingStatus::Active => "Active".to_string(), + ListingStatus::Sold => "Sold".to_string(), + ListingStatus::Cancelled => "Cancelled".to_string(), + ListingStatus::Expired => "Expired".to_string(), + } +} + +fn string_to_listing_status(s: &str) -> Result> { + match s { + "Active" => Ok(ListingStatus::Active), + "Sold" => Ok(ListingStatus::Sold), + "Cancelled" => Ok(ListingStatus::Cancelled), + "Expired" => Ok(ListingStatus::Expired), + _ => Err(Box::new(EvalAltResult::ErrorRuntime( + format!("Invalid listing status: '{}'. Expected one of: Active, Sold, Cancelled, Expired", s).into(), + Position::NONE + ))), + } +} + +// ListingType conversions +fn listing_type_to_string(lt: &ListingType) -> String { + match lt { + ListingType::FixedPrice => "FixedPrice".to_string(), + ListingType::Auction => "Auction".to_string(), + ListingType::Exchange => "Exchange".to_string(), + } +} + +fn string_to_listing_type(s: &str) -> Result> { + match s { + "FixedPrice" => Ok(ListingType::FixedPrice), + "Auction" => Ok(ListingType::Auction), + "Exchange" => Ok(ListingType::Exchange), + _ => Err(Box::new(EvalAltResult::ErrorRuntime( + format!("Invalid listing type: '{}'. Expected one of: FixedPrice, Auction, Exchange", s).into(), + Position::NONE + ))), + } +} + +// BidStatus conversions +fn bid_status_to_string(status: &BidStatus) -> String { + match status { + BidStatus::Active => "Active".to_string(), + BidStatus::Accepted => "Accepted".to_string(), + BidStatus::Rejected => "Rejected".to_string(), + BidStatus::Cancelled => "Cancelled".to_string(), + } +} + +fn string_to_bid_status(s: &str) -> Result> { + match s { + "Active" => Ok(BidStatus::Active), + "Accepted" => Ok(BidStatus::Accepted), + "Rejected" => Ok(BidStatus::Rejected), + "Cancelled" => Ok(BidStatus::Cancelled), + _ => Err(Box::new(EvalAltResult::ErrorRuntime( + format!("Invalid bid status: '{}'. Expected one of: Active, Accepted, Rejected, Cancelled", s).into(), + Position::NONE + ))), + } +} + +// Account module +#[export_module] +mod account_module { + use super::*; + + // Constructor + #[rhai_fn(return_raw, global)] + pub fn new_account() -> Result> { + Ok(Account::new()) + } + + // Getters + #[rhai_fn(global, pure)] + pub fn get_id(account: &mut RhaiAccount) -> INT { + account.base_data.id as INT + } + + #[rhai_fn(global, pure)] + pub fn get_created_at(account: &mut RhaiAccount) -> INT { + account.base_data.created_at + } + + #[rhai_fn(global, pure)] + pub fn get_name(account: &mut RhaiAccount) -> String { + account.name.clone() + } + + #[rhai_fn(global, pure)] + pub fn get_user_id(account: &mut RhaiAccount) -> INT { + account.user_id as INT + } + + #[rhai_fn(global, pure)] + pub fn get_description(account: &mut RhaiAccount) -> String { + account.description.clone() + } + + #[rhai_fn(global, pure)] + pub fn get_ledger(account: &mut RhaiAccount) -> String { + account.ledger.clone() + } + + #[rhai_fn(global, pure)] + pub fn get_address(account: &mut RhaiAccount) -> String { + account.address.clone() + } + + #[rhai_fn(global, pure)] + pub fn get_pubkey(account: &mut RhaiAccount) -> String { + account.pubkey.clone() + } + + #[rhai_fn(global, pure)] + pub fn get_assets(account: &mut RhaiAccount) -> Array { + let mut assets_array = Array::new(); + for asset_id in &account.assets { + assets_array.push(Dynamic::from(*asset_id as INT)); + } + assets_array + } + + // Setters using builder pattern with proper mutability handling + #[rhai_fn(return_raw, global)] + pub fn name(account: &mut RhaiAccount, name: String) -> Result> { + let mut acc = mem::take(account); + acc = acc.name(name); + *account = acc; + Ok(account.clone()) + } + + #[rhai_fn(return_raw, global)] + pub fn user_id(account: &mut RhaiAccount, user_id: INT) -> Result> { + let user_id = id_from_i64_to_u32(user_id)?; + let mut acc = mem::take(account); + acc = acc.user_id(user_id); + *account = acc; + Ok(account.clone()) + } + + #[rhai_fn(return_raw, global)] + pub fn description(account: &mut RhaiAccount, description: String) -> Result> { + let mut acc = mem::take(account); + acc = acc.description(description); + *account = acc; + Ok(account.clone()) + } + + #[rhai_fn(return_raw, global)] + pub fn ledger(account: &mut RhaiAccount, ledger: String) -> Result> { + let mut acc = mem::take(account); + acc = acc.ledger(ledger); + *account = acc; + Ok(account.clone()) + } + + #[rhai_fn(return_raw, global)] + pub fn address(account: &mut RhaiAccount, address: String) -> Result> { + let mut acc = mem::take(account); + acc = acc.address(address); + *account = acc; + Ok(account.clone()) + } + + #[rhai_fn(return_raw, global)] + pub fn pubkey(account: &mut RhaiAccount, pubkey: String) -> Result> { + let mut acc = mem::take(account); + acc = acc.pubkey(pubkey); + *account = acc; + Ok(account.clone()) + } + + #[rhai_fn(return_raw, global)] + pub fn add_asset(account: &mut RhaiAccount, asset_id: INT) -> Result> { + let asset_id = id_from_i64_to_u32(asset_id)?; + let mut acc = mem::take(account); + acc = acc.add_asset(asset_id); + *account = acc; + Ok(account.clone()) + } +} + +// Asset module +#[export_module] +mod asset_module { + use super::*; + + // Constructor + #[rhai_fn(return_raw, global)] + pub fn new_asset() -> Result> { + Ok(Asset::new()) + } + + // Getters + #[rhai_fn(global, pure)] + pub fn get_id(asset: &mut RhaiAsset) -> INT { + asset.base_data.id as INT + } + + #[rhai_fn(global, pure)] + pub fn get_created_at(asset: &mut RhaiAsset) -> INT { + asset.base_data.created_at + } + + #[rhai_fn(global, pure)] + pub fn get_name(asset: &mut RhaiAsset) -> String { + asset.name.clone() + } + + #[rhai_fn(global, pure)] + pub fn get_description(asset: &mut RhaiAsset) -> String { + asset.description.clone() + } + + #[rhai_fn(global, pure)] + pub fn get_amount(asset: &mut RhaiAsset) -> f64 { + asset.amount + } + + #[rhai_fn(global, pure)] + pub fn get_address(asset: &mut RhaiAsset) -> String { + asset.address.clone() + } + + #[rhai_fn(global, pure)] + pub fn get_asset_type(asset: &mut RhaiAsset) -> String { + asset_type_to_string(&asset.asset_type) + } + + #[rhai_fn(global, pure)] + pub fn get_decimals(asset: &mut RhaiAsset) -> INT { + asset.decimals as INT + } + + // Setters using builder pattern with proper mutability handling + #[rhai_fn(return_raw, global)] + pub fn name(asset: &mut RhaiAsset, name: String) -> Result> { + let mut ast = mem::take(asset); + ast = ast.name(name); + *asset = ast; + Ok(asset.clone()) + } + + #[rhai_fn(return_raw, global)] + pub fn description(asset: &mut RhaiAsset, description: String) -> Result> { + let mut ast = mem::take(asset); + ast = ast.description(description); + *asset = ast; + Ok(asset.clone()) + } + + #[rhai_fn(return_raw, global)] + pub fn amount(asset: &mut RhaiAsset, amount: f64) -> Result> { + let mut ast = mem::take(asset); + ast = ast.amount(amount); + *asset = ast; + Ok(asset.clone()) + } + + #[rhai_fn(return_raw, global)] + pub fn address(asset: &mut RhaiAsset, address: String) -> Result> { + let mut ast = mem::take(asset); + ast = ast.address(address); + *asset = ast; + Ok(asset.clone()) + } + + #[rhai_fn(return_raw, global)] + pub fn asset_type(asset: &mut RhaiAsset, asset_type_str: String) -> Result> { + let asset_type_enum = string_to_asset_type(&asset_type_str)?; + let mut ast = mem::take(asset); + ast = ast.asset_type(asset_type_enum); + *asset = ast; + Ok(asset.clone()) + } + + #[rhai_fn(return_raw, global)] + pub fn decimals(asset: &mut RhaiAsset, decimals: INT) -> Result> { + if decimals < 0 || decimals > 255 { + return Err(Box::new(EvalAltResult::ErrorArithmetic( + format!("Decimals value must be between 0 and 255, got {}", decimals).into(), + Position::NONE + ))); + } + let mut ast = mem::take(asset); + ast = ast.decimals(decimals as u8); + *asset = ast; + Ok(asset.clone()) + } +} +// Listing module +#[export_module] +mod listing_module { + use super::*; + + // Constructor + #[rhai_fn(return_raw, global)] + pub fn new_listing() -> Result> { + Ok(Listing::new()) + } + + // Getters + #[rhai_fn(global, pure)] + pub fn get_id(listing: &mut RhaiListing) -> INT { + listing.base_data.id as INT + } + + #[rhai_fn(global, pure)] + pub fn get_created_at(listing: &mut RhaiListing) -> INT { + listing.base_data.created_at + } + + #[rhai_fn(global, pure)] + pub fn get_seller_id(listing: &mut RhaiListing) -> INT { + // Parse the seller_id string to u32, then to INT + listing.seller_id.parse::().unwrap_or(0) as INT + } + + #[rhai_fn(global, pure)] + pub fn get_asset_id(listing: &mut RhaiListing) -> INT { + // Parse the asset_id string to u32, then to INT + listing.asset_id.parse::().unwrap_or(0) as INT + } + + #[rhai_fn(global, pure)] + pub fn get_price(listing: &mut RhaiListing) -> f64 { + listing.price + } + + #[rhai_fn(global, pure)] + pub fn get_currency(listing: &mut RhaiListing) -> String { + listing.currency.clone() + } + + #[rhai_fn(global, pure)] + pub fn get_listing_type(listing: &mut RhaiListing) -> String { + listing_type_to_string(&listing.listing_type) + } + + #[rhai_fn(global, pure)] + pub fn get_status(listing: &mut RhaiListing) -> String { + listing_status_to_string(&listing.status) + } + + #[rhai_fn(global, pure)] + pub fn get_title(listing: &mut RhaiListing) -> String { + listing.title.clone() + } + + #[rhai_fn(global, pure)] + pub fn get_description(listing: &mut RhaiListing) -> String { + listing.description.clone() + } + + #[rhai_fn(global, pure)] + pub fn get_image_url(listing: &mut RhaiListing) -> String { + listing.image_url.clone().unwrap_or_else(|| "".to_string()) + } + + #[rhai_fn(global, pure)] + pub fn get_end_date(listing: &mut RhaiListing) -> INT { + listing.expires_at.map_or(0, |d| d.timestamp()) + } + + #[rhai_fn(global, pure)] + pub fn get_tags(listing: &mut RhaiListing) -> Array { + let mut tags_array = Array::new(); + for tag in &listing.tags { + tags_array.push(Dynamic::from(tag.clone())); + } + tags_array + } + + #[rhai_fn(global, pure)] + pub fn get_bids(listing: &mut RhaiListing) -> Array { + let mut bids_array = Array::new(); + for bid in &listing.bids { + bids_array.push(Dynamic::from(bid.clone())); + } + bids_array + } + + // Setters using builder pattern with proper mutability handling + #[rhai_fn(return_raw, global)] + pub fn seller_id(listing: &mut RhaiListing, seller_id: INT) -> Result> { + let seller_id = id_from_i64_to_u32(seller_id)?; + let mut lst = mem::take(listing); + lst = lst.seller_id(seller_id); + *listing = lst; + Ok(listing.clone()) + } + + #[rhai_fn(return_raw, global)] + pub fn asset_id(listing: &mut RhaiListing, asset_id: INT) -> Result> { + let asset_id = id_from_i64_to_u32(asset_id)?; + let mut lst = mem::take(listing); + lst = lst.asset_id(asset_id); + *listing = lst; + Ok(listing.clone()) + } + + #[rhai_fn(return_raw, global)] + pub fn price(listing: &mut RhaiListing, price: f64) -> Result> { + let mut lst = mem::take(listing); + lst = lst.price(price); + *listing = lst; + Ok(listing.clone()) + } + + #[rhai_fn(return_raw, global)] + pub fn currency(listing: &mut RhaiListing, currency: String) -> Result> { + let mut lst = mem::take(listing); + lst = lst.currency(currency); + *listing = lst; + Ok(listing.clone()) + } + + #[rhai_fn(return_raw, global)] + pub fn listing_type(listing: &mut RhaiListing, listing_type_str: String) -> Result> { + let listing_type_enum = string_to_listing_type(&listing_type_str)?; + let mut lst = mem::take(listing); + lst = lst.listing_type(listing_type_enum); + *listing = lst; + Ok(listing.clone()) + } + + #[rhai_fn(return_raw, global)] + pub fn title(listing: &mut RhaiListing, title: String) -> Result> { + let mut lst = mem::take(listing); + lst = lst.title(title); + *listing = lst; + Ok(listing.clone()) + } + + #[rhai_fn(return_raw, global)] + pub fn description(listing: &mut RhaiListing, description: String) -> Result> { + let mut lst = mem::take(listing); + lst = lst.description(description); + *listing = lst; + Ok(listing.clone()) + } + + #[rhai_fn(return_raw, global)] + pub fn image_url(listing: &mut RhaiListing, image_url: String) -> Result> { + let mut lst = mem::take(listing); + lst = lst.image_url(Some(image_url)); + *listing = lst; + Ok(listing.clone()) + } + + #[rhai_fn(return_raw, global)] + pub fn expires_at(listing: &mut RhaiListing, end_date_ts: INT) -> Result> { + use chrono::TimeZone; + let end_date = chrono::Utc.timestamp_opt(end_date_ts, 0) + .single() + .ok_or_else(|| Box::new(EvalAltResult::ErrorRuntime( + format!("Invalid timestamp: {}", end_date_ts).into(), + Position::NONE + )))?; + + let mut lst = mem::take(listing); + lst = lst.expires_at(Some(end_date)); + *listing = lst; + Ok(listing.clone()) + } + + #[rhai_fn(return_raw, global)] + pub fn add_tag(listing: &mut RhaiListing, tag: String) -> Result> { + let mut lst = mem::take(listing); + lst = lst.add_tag(tag); + *listing = lst; + Ok(listing.clone()) + } + + #[rhai_fn(return_raw, global)] + pub fn add_bid(listing: &mut RhaiListing, bid: RhaiBid) -> Result> { + let lst = mem::take(listing); + match lst.clone().add_bid(bid) { + Ok(updated_lst) => { + *listing = updated_lst; + Ok(listing.clone()) + }, + Err(err) => { + // Put back the original listing since the add_bid failed + *listing = lst; + Err(Box::new(EvalAltResult::ErrorRuntime( + format!("Failed to add bid: {}", err).into(), + Position::NONE + ))) + } + } + } + + #[rhai_fn(return_raw, global)] + pub fn complete_sale(listing: &mut RhaiListing, buyer_id: INT) -> Result> { + let buyer_id = id_from_i64_to_u32(buyer_id)?; + let lst = mem::take(listing); + + // First set the buyer_id and sale_price + let lst_with_buyer = lst.clone().buyer_id(buyer_id); + + // Now complete the sale + match lst_with_buyer.complete_sale() { + Ok(updated_lst) => { + *listing = updated_lst; + Ok(listing.clone()) + }, + Err(err) => { + // Put back the original listing since the complete_sale failed + *listing = lst; + Err(Box::new(EvalAltResult::ErrorRuntime( + format!("Failed to complete sale: {}", err).into(), + Position::NONE + ))) + } + } + } + + #[rhai_fn(return_raw, global)] + pub fn cancel(listing: &mut RhaiListing) -> Result> { + let lst = mem::take(listing); + match lst.clone().cancel() { + Ok(updated_lst) => { + *listing = updated_lst; + Ok(listing.clone()) + }, + Err(err) => { + // Put back the original listing since the cancel failed + *listing = lst; + Err(Box::new(EvalAltResult::ErrorRuntime( + format!("Failed to cancel listing: {}", err).into(), + Position::NONE + ))) + } + } + } + + #[rhai_fn(return_raw, global)] + pub fn check_expiration(listing: &mut RhaiListing) -> Result> { + let mut lst = mem::take(listing); + lst = lst.check_expiration(); + *listing = lst; + Ok(listing.clone()) + } +} + +// Bid module +#[export_module] +mod bid_module { + use super::*; + + // Constructor + #[rhai_fn(return_raw, global)] + pub fn new_bid() -> Result> { + Ok(Bid::new()) + } + + // Getters + #[rhai_fn(global, pure)] + pub fn get_listing_id(bid: &mut RhaiBid) -> String { + bid.listing_id.clone() + } + + #[rhai_fn(global, pure)] + pub fn get_bidder_id(bid: &mut RhaiBid) -> INT { + bid.bidder_id as INT + } + + #[rhai_fn(global, pure)] + pub fn get_amount(bid: &mut RhaiBid) -> f64 { + bid.amount + } + + #[rhai_fn(global, pure)] + pub fn get_currency(bid: &mut RhaiBid) -> String { + bid.currency.clone() + } + + #[rhai_fn(global, pure)] + pub fn get_status(bid: &mut RhaiBid) -> String { + bid_status_to_string(&bid.status) + } + + #[rhai_fn(global, pure)] + pub fn get_created_at(bid: &mut RhaiBid) -> INT { + bid.created_at.timestamp() + } + + // Setters using builder pattern with proper mutability handling + #[rhai_fn(return_raw, global)] + pub fn listing_id(bid: &mut RhaiBid, listing_id: String) -> Result> { + let mut b = mem::take(bid); + b = b.listing_id(listing_id); + *bid = b; + Ok(bid.clone()) + } + + #[rhai_fn(return_raw, global)] + pub fn bidder_id(bid: &mut RhaiBid, bidder_id: INT) -> Result> { + let bidder_id = id_from_i64_to_u32(bidder_id)?; + let mut b = mem::take(bid); + b = b.bidder_id(bidder_id); + *bid = b; + Ok(bid.clone()) + } + + #[rhai_fn(return_raw, global)] + pub fn amount(bid: &mut RhaiBid, amount: f64) -> Result> { + let mut b = mem::take(bid); + b = b.amount(amount); + *bid = b; + Ok(bid.clone()) + } + + #[rhai_fn(return_raw, global)] + pub fn currency(bid: &mut RhaiBid, currency: String) -> Result> { + let mut b = mem::take(bid); + b = b.currency(currency); + *bid = b; + Ok(bid.clone()) + } + + #[rhai_fn(return_raw, global)] + pub fn update_status(bid: &mut RhaiBid, status_str: String) -> Result> { + let status = string_to_bid_status(&status_str)?; + let mut b = mem::take(bid); + b = b.status(status); + *bid = b; + Ok(bid.clone()) + } +} use std::collections::HashMap; -use std::error::Error as StdError; // For Box +use std::sync::Mutex; +use rhai::ImmutableString; -// Custom error type for Rhai that wraps a String -#[derive(Debug)] +// Custom error type for Rhai struct RhaiStringError(String); - -impl std::fmt::Display for RhaiStringError { +impl std::fmt::Debug for RhaiStringError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.0) } } -impl StdError for RhaiStringError {} +// Register all Rhai functions for the finance module +pub fn register_finance_rhai_module(engine: &mut Engine, db: Arc) { + // --- Register model-specific modules with the engine --- + let account_module = exported_module!(account_module); + engine.register_global_module(account_module.into()); + let asset_module = exported_module!(asset_module); + engine.register_global_module(asset_module.into()); -use chrono::{DateTime, Utc}; + let listing_module = exported_module!(listing_module); + engine.register_global_module(listing_module.into()); -use crate::models::finance::account::Account; -use crate::models::finance::asset::{Asset, AssetType}; -use crate::models::finance::marketplace::{Listing, Bid, ListingStatus, ListingType, BidStatus}; + let bid_module = exported_module!(bid_module); + engine.register_global_module(bid_module.into()); -// --- Enum to String & String to Enum Helper Functions (domain-specific) --- -// These remain here as they are specific to the finance models' enums. - -fn asset_type_to_string(asset_type: &AssetType) -> ImmutableString { - format!("{:?}", asset_type).into() -} -fn string_to_asset_type(s: &str) -> Result> { - match s { - "Erc20" => Ok(AssetType::Erc20), - "Erc721" => Ok(AssetType::Erc721), - "Erc1155" => Ok(AssetType::Erc1155), - "Native" => Ok(AssetType::Native), - _ => Err(format!("Invalid AssetType string: {}", s).into()), - } -} - -fn listing_status_to_string(status: &ListingStatus) -> ImmutableString { - format!("{:?}", status).into() -} -fn string_to_listing_status(s: &str) -> Result> { - match s.to_lowercase().as_str() { - "active" => Ok(ListingStatus::Active), - "sold" => Ok(ListingStatus::Sold), - "cancelled" => Ok(ListingStatus::Cancelled), - "expired" => Ok(ListingStatus::Expired), - _ => Err(format!("Invalid ListingStatus string: {}", s).into()), - } -} - -fn listing_type_to_string(lt: &ListingType) -> ImmutableString { - format!("{:?}", lt).into() -} -fn string_to_listing_type(s: &str) -> Result> { - match s.to_lowercase().as_str() { - "fixedprice" => Ok(ListingType::FixedPrice), - "auction" => Ok(ListingType::Auction), - "exchange" => Ok(ListingType::Exchange), - _ => Err(format!("Invalid ListingType string: {}", s).into()), - } -} - -fn bid_status_to_string(status: &BidStatus) -> ImmutableString { - format!("{:?}", status).into() -} -fn string_to_bid_status(s: &str) -> Result> { - match s.to_lowercase().as_str() { - "active" => Ok(BidStatus::Active), - "accepted" => Ok(BidStatus::Accepted), - "rejected" => Ok(BidStatus::Rejected), - "cancelled" => Ok(BidStatus::Cancelled), - _ => Err(format!("Invalid BidStatus string: {}", s).into()), - } -} - -pub fn register_rhai_engine_functions( - engine: &mut Engine, - db_accounts: Arc>>, - db_assets: Arc>>, - db_listings: Arc>>, -) { - // --- Account model --- - engine.register_type_with_name::("Account"); - engine.register_fn("new_account", || -> Account { - Account::new(None, "", 0, "", "", "", "") - }); - // Getters - engine.register_get("id", |acc: &mut Account| acc.base_data.id as INT); - engine.register_get("created_at_ts", |acc: &mut Account| acc.base_data.created_at); - engine.register_get("name", |acc: &mut Account| -> ImmutableString { acc.name.clone().into() }); - engine.register_get("user_id", |acc: &mut Account| acc.user_id as INT); - engine.register_get("description", |acc: &mut Account| -> ImmutableString { acc.description.clone().into() }); - engine.register_get("ledger", |acc: &mut Account| -> ImmutableString { acc.ledger.clone().into() }); - engine.register_get("address", |acc: &mut Account| -> ImmutableString { acc.address.clone().into() }); - engine.register_get("pubkey", |acc: &mut Account| -> ImmutableString { acc.pubkey.clone().into() }); - engine.register_get("assets_list", |acc: &mut Account| -> Result> { - Ok(acc.assets.iter().cloned().map(rhai::Dynamic::from).collect()) - }); - engine.register_get("modified_at_ts", |acc: &mut Account| acc.base_data.modified_at); - - // Setters (Builder Pattern) - engine.register_fn("set_name", |mut acc: Account, name: ImmutableString| -> Result> { - acc.name = name.to_string(); Ok(acc) - }); - engine.register_fn("set_user_id", |mut acc: Account, user_id: INT| -> Result> { - acc.user_id = user_id as u32; Ok(acc) - }); - engine.register_fn("set_description", |mut acc: Account, description: ImmutableString| -> Result> { - acc.description = description.to_string(); Ok(acc) - }); - engine.register_fn("set_ledger", |mut acc: Account, ledger: ImmutableString| -> Result> { - acc.ledger = ledger.to_string(); Ok(acc) - }); - engine.register_fn("set_address", |mut acc: Account, address: ImmutableString| -> Result> { - acc.address = address.to_string(); Ok(acc) - }); - engine.register_fn("set_pubkey", |mut acc: Account, pubkey: ImmutableString| -> Result> { - acc.pubkey = pubkey.to_string(); Ok(acc) - }); - // Action: Add an Asset object to the account's asset list - engine.register_fn("add_asset", |mut acc: Account, asset: Asset| -> Result> { - acc.assets.push(asset); - Ok(acc) - }); - - // --- Asset model --- - engine.register_type_with_name::("Asset"); - engine.register_fn("new_asset", || -> Asset { - Asset::new(None, "", "", 0.0, "", AssetType::Native, 0) - }); - // Getters - engine.register_get("id", |asset: &mut Asset| asset.base_data.id as INT); - engine.register_get("created_at_ts", |asset: &mut Asset| asset.base_data.created_at); - engine.register_get("name", |asset: &mut Asset| -> ImmutableString { asset.name.clone().into() }); - engine.register_get("description", |asset: &mut Asset| -> ImmutableString { asset.description.clone().into() }); - engine.register_get("amount", |asset: &mut Asset| asset.amount); - engine.register_get("address", |asset: &mut Asset| -> ImmutableString { asset.address.clone().into() }); - engine.register_get("asset_type_str", |asset: &mut Asset| -> ImmutableString { self::asset_type_to_string(&asset.asset_type) }); - engine.register_get("decimals", |asset: &mut Asset| asset.decimals as INT); - engine.register_get("modified_at_ts", |asset: &mut Asset| asset.base_data.modified_at); - - // Setters (Builder Pattern) - engine.register_fn("set_name", |mut asset: Asset, name: ImmutableString| -> Result> { - asset.name = name.to_string(); Ok(asset) - }); - engine.register_fn("set_description", |mut asset: Asset, description: ImmutableString| -> Result> { - asset.description = description.to_string(); Ok(asset) - }); - engine.register_fn("set_amount", |mut asset: Asset, amount: f64| -> Result> { - asset.amount = amount; Ok(asset) - }); - engine.register_fn("set_address", |mut asset: Asset, address: ImmutableString| -> Result> { - asset.address = address.to_string(); Ok(asset) - }); - engine.register_fn("set_asset_type", |mut asset: Asset, asset_type_str: ImmutableString| -> Result> { - asset.asset_type = self::string_to_asset_type(asset_type_str.as_str())?; - Ok(asset) - }); - engine.register_fn("set_decimals", |mut asset: Asset, decimals: INT| -> Result> { - asset.decimals = decimals as u8; Ok(asset) - }); - - // --- Listing model --- - engine.register_type_with_name::("Listing"); - engine.register_fn("new_listing", || -> Listing { - Listing::new(None, "", "", "", AssetType::Native, "", 0.0, "", ListingType::FixedPrice, None, Vec::new(), None::) - }); - // Getters - engine.register_get("id", |l: &mut Listing| l.base_data.id as INT); - engine.register_get("created_at_ts", |l: &mut Listing| l.base_data.created_at); - engine.register_get("modified_at_ts", |l: &mut Listing| l.base_data.modified_at); - engine.register_get("title", |l: &mut Listing| -> ImmutableString { l.title.clone().into() }); - engine.register_get("description", |l: &mut Listing| -> ImmutableString { l.description.clone().into() }); - engine.register_get("asset_id", |l: &mut Listing| -> ImmutableString { l.asset_id.clone().into() }); - engine.register_get("asset_type_str", |l: &mut Listing| -> ImmutableString { self::asset_type_to_string(&l.asset_type) }); - engine.register_get("seller_id", |l: &mut Listing| -> ImmutableString { l.seller_id.clone().into() }); - engine.register_get("price", |l: &mut Listing| l.price); - engine.register_get("currency", |l: &mut Listing| -> ImmutableString { l.currency.clone().into() }); - engine.register_get("listing_type", |l: &mut Listing| l.listing_type.clone()); - engine.register_get("status", |l: &mut Listing| l.status.clone()); - engine.register_get("expires_at_ts", |l: &mut Listing| l.expires_at); - engine.register_get("expires_at_ts_opt", |l: &mut Listing| l.expires_at.map(|dt| dt.timestamp())); - engine.register_get("tags", |l: &mut Listing| -> Result> { - Ok(l.tags.iter().map(|s| Dynamic::from(s.clone())).collect()) - }); - engine.register_get("image_url", |l: &mut Listing| -> Option { l.image_url.as_ref().map(|s| s.clone().into()) }); - engine.register_get("bids_list", |l: &mut Listing| -> Result> { - Ok(l.bids.iter().cloned().map(rhai::Dynamic::from).collect()) - }); - // Setters (Builder Pattern) - engine.register_fn("set_title", |mut l: Listing, title: ImmutableString| -> Result> { - l.title = title.to_string(); Ok(l) - }); - engine.register_fn("set_description", |mut l: Listing, description: ImmutableString| -> Result> { - l.description = description.to_string(); Ok(l) - }); - engine.register_fn("set_asset_id", |mut l: Listing, asset_id: ImmutableString| -> Result> { - l.asset_id = asset_id.to_string(); Ok(l) - }); - engine.register_fn("set_asset_type", |mut l: Listing, asset_type_str: ImmutableString| -> Result> { - l.asset_type = self::string_to_asset_type(asset_type_str.as_str())?; - Ok(l) - }); - engine.register_fn("set_seller_id", |mut l: Listing, seller_id: ImmutableString| -> Result> { - l.seller_id = seller_id.to_string(); Ok(l) - }); - engine.register_fn("set_price", |mut l: Listing, price: f64| -> Result> { - l.price = price; Ok(l) - }); - engine.register_fn("set_currency", |mut l: Listing, currency: ImmutableString| -> Result> { - l.currency = currency.to_string(); Ok(l) - }); - engine.register_fn("set_listing_type", |mut l: Listing, listing_type: ImmutableString| -> Result> { - l.listing_type = self::string_to_listing_type(listing_type.as_str())?; - Ok(l) - }); - engine.register_fn("set_status_str", |mut l: Listing, status_str: ImmutableString| -> Result> { - l.status = self::string_to_listing_status(status_str.as_str())?; - Ok(l) - }); - engine.register_fn("set_expires_at_ts", |mut l: Listing, expires_at_ts: Option| -> Result> { - l.expires_at = expires_at_ts.map(|ts| DateTime::from_timestamp(ts, 0).unwrap_or_else(|| Utc::now())); - Ok(l) - }); - engine.register_fn("set_tags", |mut l: Listing, tags_array: Array| -> Result> { - l.tags = tags_array.into_iter().map(|d| d.into_string().unwrap_or_default()).collect(); - Ok(l) - }); - engine.register_fn("add_tag", |mut l: Listing, tag: ImmutableString| -> Result> { - l.tags.push(tag.to_string()); - Ok(l) - }); - engine.register_fn("set_image_url", |mut l: Listing, image_url: Option| -> Result> { - l.image_url = image_url.map(|s| s.to_string()); - Ok(l) - }); - // Listing Action Methods (preserved) - engine.register_fn("add_listing_bid", |listing: Listing, bid: Bid| -> Result> { - listing.add_bid(bid).map_err(|e_str| { - Box::new(EvalAltResult::ErrorSystem( - "Failed to add bid".to_string(), - Box::new(RhaiStringError(e_str.to_string())), - )) - }) - }); - engine.register_fn("accept_listing_bid", |listing: Listing, bid_index_rhai: i64| -> Result> { - let bid_index = bid_index_rhai as usize; - if bid_index >= listing.bids.len() { - return Err(Box::new(EvalAltResult::ErrorSystem( - "Invalid bid index".to_string(), - Box::new(RhaiStringError(format!("Bid index {} out of bounds for {} bids", bid_index, listing.bids.len()))), - ))); - } - - let bid_to_accept = listing.bids[bid_index].clone(); - - if bid_to_accept.status != BidStatus::Active { - return Err(Box::new(EvalAltResult::ErrorSystem( - "Bid not active".to_string(), - Box::new(RhaiStringError(format!("Cannot accept bid at index {} as it is not active (status: {:?})", bid_index, bid_to_accept.status))), - ))); - } - - let mut listing_after_sale = listing.complete_sale(bid_to_accept.bidder_id.to_string(), bid_to_accept.amount) - .map_err(|e_str| Box::new(EvalAltResult::ErrorSystem( - "Failed to complete sale".to_string(), - Box::new(RhaiStringError(e_str.to_string())), - )))?; - - // Update bid statuses on the new listing state - for (idx, bid_in_list) in listing_after_sale.bids.iter_mut().enumerate() { - if idx == bid_index { - *bid_in_list = bid_in_list.clone().update_status(BidStatus::Accepted); - } else { - if bid_in_list.status == BidStatus::Active { // Only reject other active bids - *bid_in_list = bid_in_list.clone().update_status(BidStatus::Rejected); - } - } - } - Ok(listing_after_sale) - }); - engine.register_fn("cancel_listing", |_ctx: NativeCallContext, listing: Listing| -> Result> { - listing.cancel().map_err(|e_str| { - Box::new(EvalAltResult::ErrorSystem( - "Failed to cancel listing".to_string(), - Box::new(RhaiStringError(e_str.to_string())) - )) - }) - }); - // --- Bid model (preserved as is) --- - engine.register_type_with_name::("Bid") - .register_fn("new_bid", - |listing_id_rhai: ImmutableString, bidder_id_rhai: INT, amount_rhai: f64, currency_rhai: ImmutableString| -> Bid { - Bid::new(listing_id_rhai, bidder_id_rhai as u32, amount_rhai, currency_rhai) - } - ) - .register_get_set("listing_id", - |bid: &mut Bid| -> ImmutableString { bid.listing_id.clone().into() }, - |bid: &mut Bid, val: ImmutableString| bid.listing_id = val.to_string() - ) - .register_get_set("bidder_id", |bid: &mut Bid| bid.bidder_id as INT, |bid: &mut Bid, val: INT| bid.bidder_id = val as u32) - .register_get_set("amount", |bid: &mut Bid| bid.amount, |bid: &mut Bid, val: f64| bid.amount = val) - .register_get_set("currency", - |bid: &mut Bid| -> ImmutableString { bid.currency.clone().into() }, - |bid: &mut Bid, val: ImmutableString| bid.currency = val.to_string() - ) - .register_get("status_str", |bid: &mut Bid| -> ImmutableString { self::bid_status_to_string(&bid.status) }); - - engine.register_fn("accept_bid_script", |bid: Bid| -> Result> { - // Ensure the bid is active before accepting - if bid.status != BidStatus::Active { - return Err(Box::new(EvalAltResult::ErrorSystem( - "Bid not active".to_string(), - Box::new(RhaiStringError(format!("Cannot accept bid as it is not active (status: {:?})", bid.status))) - ))); - } - Ok(bid.update_status(BidStatus::Accepted)) - }); - engine.register_fn("reject_bid_script", |bid: Bid| -> Result> { - // Ensure the bid is active before rejecting - if bid.status != BidStatus::Active { - return Err(Box::new(EvalAltResult::ErrorSystem( - "Bid not active".to_string(), - Box::new(RhaiStringError(format!("Cannot reject bid as it is not active (status: {:?})", bid.status))) - ))); - } - Ok(bid.update_status(BidStatus::Rejected)) - }); - engine.register_fn("cancel_bid_script", |bid: Bid| -> Result> { - // Ensure the bid is active before cancelling - if bid.status != BidStatus::Active { - return Err(Box::new(EvalAltResult::ErrorSystem( - "Bid not active".to_string(), - Box::new(RhaiStringError(format!("Cannot cancel bid as it is not active (status: {:?})", bid.status))) - ))); - } - Ok(bid.update_status(BidStatus::Cancelled)) - }); - - // --- Global Helper Functions (Enum conversions, potentially already covered by macros but good for direct script use) --- + // --- Global Helper Functions (Enum conversions) --- engine.register_fn("str_to_asset_type", |s: ImmutableString| self::string_to_asset_type(s.as_str())); engine.register_fn("asset_type_to_str", self::asset_type_to_string); engine.register_fn("str_to_listing_status", |s: ImmutableString| self::string_to_listing_status(s.as_str())); @@ -350,67 +726,73 @@ pub fn register_rhai_engine_functions( engine.register_fn("str_to_bid_status", |s: ImmutableString| self::string_to_bid_status(s.as_str())); engine.register_fn("bid_status_to_str", self::bid_status_to_string); - // --- Mock DB functions (preserved) --- - let accounts_db_clone = Arc::clone(&db_accounts); - engine.register_fn("set_account", move |mut account: Account| -> Account { - let mut db = accounts_db_clone.lock().unwrap(); - if account.base_data.id == 0 { - let next_id = db.keys().max().cloned().unwrap_or(0) + 1; - account.base_data.update_id(next_id); - } - db.insert(account.base_data.id, account.clone()); - account + // --- Database interaction functions (registered in a separate db_module) --- + let mut db_module = Module::new(); + + // Account DB functions + let db_set_account = Arc::clone(&db); + db_module.set_native_fn("set_account", move |account: Account| -> Result> { + let collection = db_set_account.collection::().map_err(|e| + Box::new(EvalAltResult::ErrorRuntime(format!("Failed to get Account collection: {:?}", e).into(), Position::NONE)) )?; + collection.set(&account) + .map(|(id, _)| id as INT) + .map_err(|e| Box::new(EvalAltResult::ErrorRuntime(format!("Failed to save Account: {:?}", e).into(), Position::NONE))) }); - let accounts_db_clone_get = Arc::clone(&db_accounts); - engine.register_fn("get_account_by_id", move |id_rhai: INT| -> Result> { - let db = accounts_db_clone_get.lock().unwrap(); - match db.get(&(id_rhai as u32)) { - Some(account) => Ok(account.clone()), - None => Err(format!("Account not found with ID: {}", id_rhai).into()), - } + let db_get_account = Arc::clone(&db); + db_module.set_native_fn("get_account_by_id", move |id_rhai: INT| -> Result> { + let id_u32 = id_from_i64_to_u32(id_rhai)?; + let collection = db_get_account.collection::().map_err(|e| + Box::new(EvalAltResult::ErrorRuntime(format!("Failed to get Account collection: {:?}", e).into(), Position::NONE)) )?; + collection.get_by_id(id_u32) + .map(|opt_account| opt_account.map(Dynamic::from).unwrap_or_else(|| Dynamic::UNIT)) + .map_err(|e| Box::new(EvalAltResult::ErrorRuntime(format!("Failed to get Account with ID {}: {:?}", id_rhai, e).into(), Position::NONE))) }); - let assets_db_clone = Arc::clone(&db_assets); - engine.register_fn("set_asset", move |mut asset: Asset| -> Asset { - let mut db = assets_db_clone.lock().unwrap(); - if asset.base_data.id == 0 { - let next_id = db.keys().max().cloned().unwrap_or(0) + 1; - asset.base_data.update_id(next_id); - } - db.insert(asset.base_data.id, asset.clone()); - asset + // Asset DB functions + let db_set_asset = Arc::clone(&db); + db_module.set_native_fn("set_asset", move |asset: Asset| -> Result> { + let collection = db_set_asset.collection::().map_err(|e| + Box::new(EvalAltResult::ErrorRuntime(format!("Failed to get Asset collection: {:?}", e).into(), Position::NONE)) )?; + collection.set(&asset) + .map(|(id, _)| id as INT) + .map_err(|e| Box::new(EvalAltResult::ErrorRuntime(format!("Failed to save Asset: {:?}", e).into(), Position::NONE))) }); - let assets_db_clone_get = Arc::clone(&db_assets); - engine.register_fn("get_asset_by_id", move |id_rhai: INT| -> Result> { - let db = assets_db_clone_get.lock().unwrap(); - match db.get(&(id_rhai as u32)) { - Some(asset) => Ok(asset.clone()), - None => Err(format!("Asset not found with ID: {}", id_rhai).into()), - } + let db_get_asset = Arc::clone(&db); + db_module.set_native_fn("get_asset_by_id", move |id_rhai: INT| -> Result> { + let id_u32 = id_from_i64_to_u32(id_rhai)?; + let collection = db_get_asset.collection::().map_err(|e| + Box::new(EvalAltResult::ErrorRuntime(format!("Failed to get Asset collection: {:?}", e).into(), Position::NONE)) )?; + collection.get_by_id(id_u32) + .map(|opt_asset| opt_asset.map(Dynamic::from).unwrap_or_else(|| Dynamic::UNIT)) + .map_err(|e| Box::new(EvalAltResult::ErrorRuntime(format!("Failed to get Asset with ID {}: {:?}", id_rhai, e).into(), Position::NONE))) }); - let listings_db_clone = Arc::clone(&db_listings); - engine.register_fn("set_listing", move |mut listing: Listing| -> Listing { - let mut db = listings_db_clone.lock().unwrap(); - if listing.base_data.id == 0 { - let next_id = db.keys().max().cloned().unwrap_or(0) + 1; - listing.base_data.update_id(next_id); - } - db.insert(listing.base_data.id, listing.clone()); - listing + // Listing DB functions + let db_set_listing = Arc::clone(&db); + db_module.set_native_fn("set_listing", move |listing: Listing| -> Result> { + let collection = db_set_listing.collection::().map_err(|e| + Box::new(EvalAltResult::ErrorRuntime(format!("Failed to get Listing collection: {:?}", e).into(), Position::NONE)) )?; + collection.set(&listing) + .map(|(id, _)| id as INT) + .map_err(|e| Box::new(EvalAltResult::ErrorRuntime(format!("Failed to save Listing: {:?}", e).into(), Position::NONE))) }); - let listings_db_clone_get = Arc::clone(&db_listings); - engine.register_fn("get_listing_by_id", move |id_rhai: INT| -> Result> { - let db = listings_db_clone_get.lock().unwrap(); - match db.get(&(id_rhai as u32)) { - Some(listing) => Ok(listing.clone()), - None => Err(format!("Listing not found with ID: {}", id_rhai).into()), - } + let db_get_listing = Arc::clone(&db); + db_module.set_native_fn("get_listing_by_id", move |id_rhai: INT| -> Result> { + let id_u32 = id_from_i64_to_u32(id_rhai)?; + let collection = db_get_listing.collection::().map_err(|e| + Box::new(EvalAltResult::ErrorRuntime(format!("Failed to get Listing collection: {:?}", e).into(), Position::NONE)) )?; + collection.get_by_id(id_u32) + .map(|opt_listing| opt_listing.map(Dynamic::from).unwrap_or_else(|| Dynamic::UNIT)) + .map_err(|e| Box::new(EvalAltResult::ErrorRuntime(format!("Failed to get Listing with ID {}: {:?}", id_rhai, e).into(), Position::NONE))) }); + engine.register_global_module(db_module.into()); + // Global timestamp function for scripts to get current time engine.register_fn("timestamp", || Utc::now().timestamp()); + + println!("Successfully registered finance Rhai module."); } diff --git a/heromodels/src/models/flow/flow.rs b/heromodels/src/models/flow/flow.rs index 71a0e46..c586df4 100644 --- a/heromodels/src/models/flow/flow.rs +++ b/heromodels/src/models/flow/flow.rs @@ -4,7 +4,7 @@ use serde::{Deserialize, Serialize}; use super::flow_step::FlowStep; /// Represents a signing flow. -#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] +#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, Default)] #[model] pub struct Flow { /// Base model data (id, created_at, updated_at). @@ -27,9 +27,9 @@ pub struct Flow { impl Flow { /// Create a new flow. - /// The `id` is the database primary key. /// The `flow_uuid` should be a Uuid::new_v4().to_string(). - pub fn new(_id: u32, flow_uuid: impl ToString) -> Self { + /// The ID is managed by `BaseModelData::new()` and the database. + pub fn new(flow_uuid: impl ToString) -> Self { Self { base_data: BaseModelData::new(), flow_uuid: flow_uuid.to_string(), diff --git a/heromodels/src/models/flow/flow_step.rs b/heromodels/src/models/flow/flow_step.rs index 432279f..ce554f6 100644 --- a/heromodels/src/models/flow/flow_step.rs +++ b/heromodels/src/models/flow/flow_step.rs @@ -1,6 +1,7 @@ use heromodels_core::BaseModelData; use heromodels_derive::model; use serde::{Deserialize, Serialize}; +use std::default::Default; /// Represents a step within a signing flow. #[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] @@ -20,6 +21,17 @@ pub struct FlowStep { pub status: String, } +impl Default for FlowStep { + fn default() -> Self { + Self { + base_data: BaseModelData::new(), + description: None, + step_order: 0, + status: String::from("Pending"), // Default status + } + } +} + impl FlowStep { /// Create a new flow step. pub fn new(_id: u32, step_order: u32) -> Self { diff --git a/heromodels/src/models/flow/rhai.rs b/heromodels/src/models/flow/rhai.rs index dff653d..b564850 100644 --- a/heromodels/src/models/flow/rhai.rs +++ b/heromodels/src/models/flow/rhai.rs @@ -1,140 +1,384 @@ -use rhai::{Dynamic, Engine, EvalAltResult, NativeCallContext, Position}; +use rhai::plugin::*; +use rhai::{Engine, EvalAltResult, Position, Module, INT, Dynamic, Array}; use std::sync::Arc; +use std::mem; -use heromodels_core::BaseModelData; -use crate::db::hero::OurDB; // Import OurDB for actual DB operations -use crate::db::Collection; // Collection might be needed if we add more specific DB functions -use super::{ - flow::Flow, - flow_step::FlowStep, - signature_requirement::SignatureRequirement, -}; -// use rhai_wrapper::wrap_vec_return; // Not currently used for flow, but keep for potential future use. +use super::flow::Flow; +use super::flow_step::FlowStep; +use super::signature_requirement::SignatureRequirement; +type RhaiFlow = Flow; +type RhaiFlowStep = FlowStep; +type RhaiSignatureRequirement = SignatureRequirement; +use crate::db::hero::OurDB; +use crate::db::Collection; +use crate::db::Db; +use heromodels_core::Model; -// Helper function to convert Rhai's i64 to u32 for IDs -fn i64_to_u32(val: i64, context_pos: Position, field_name: &str, object_name: &str) -> Result> { - val.try_into().map_err(|_e| { +// Helper to convert i64 from Rhai to u32 for IDs +fn id_from_i64_to_u32(id_i64: i64) -> Result> { + u32::try_from(id_i64).map_err(|_| Box::new(EvalAltResult::ErrorArithmetic( - format!("Conversion error for {} in {} from i64 to u32", field_name, object_name), - context_pos, + format!("Failed to convert ID '{}' to u32", id_i64).into(), + Position::NONE )) - }) + ) } +// Helper to convert i64 from Rhai to u64 for timestamps or other large numbers +fn val_from_i64_to_u64(val_i64: i64) -> Result> { + u64::try_from(val_i64).map_err(|_| + Box::new(EvalAltResult::ErrorArithmetic( + format!("Failed to convert value '{}' to u64", val_i64).into(), + Position::NONE + )) + ) +} + +#[export_module] +mod rhai_flow_module { + // --- Flow Functions --- + #[rhai_fn(name = "new_flow")] + pub fn new_flow(flow_uuid: String) -> RhaiFlow { + Flow::new(flow_uuid) + } + + /// Sets the flow name + #[rhai_fn(name = "name", return_raw, global, pure)] + pub fn flow_name(flow: &mut RhaiFlow, name: String) -> Result> { + let owned_flow = mem::replace(flow, Flow::new("")); // Dummy for replacement + *flow = owned_flow.name(name); + Ok(flow.clone()) + } + + /// Sets the flow status + #[rhai_fn(name = "status", return_raw, global, pure)] + pub fn flow_status(flow: &mut RhaiFlow, status: String) -> Result> { + let owned_flow = mem::replace(flow, Flow::new("")); // Dummy for replacement + *flow = owned_flow.status(status); + Ok(flow.clone()) + } + + /// Adds a step to the flow + #[rhai_fn(name = "add_step", return_raw, global, pure)] + pub fn flow_add_step(flow: &mut RhaiFlow, step: RhaiFlowStep) -> Result> { + let owned_flow = mem::replace(flow, Flow::new("")); // Dummy for replacement + *flow = owned_flow.add_step(step); + Ok(flow.clone()) + } + + // Flow Getters + #[rhai_fn(get = "id", pure)] + pub fn get_id(flow: &mut RhaiFlow) -> i64 { flow.base_data.id as i64 } + #[rhai_fn(get = "created_at", pure)] + pub fn get_created_at(flow: &mut RhaiFlow) -> i64 { flow.base_data.created_at } + #[rhai_fn(get = "modified_at", pure)] + pub fn get_modified_at(flow: &mut RhaiFlow) -> i64 { flow.base_data.modified_at } + #[rhai_fn(get = "flow_uuid", pure)] + pub fn get_flow_uuid(flow: &mut RhaiFlow) -> String { flow.flow_uuid.clone() } + #[rhai_fn(get = "name", pure)] + pub fn get_name(flow: &mut RhaiFlow) -> String { flow.name.clone() } + #[rhai_fn(get = "status", pure)] + pub fn get_status(flow: &mut RhaiFlow) -> String { flow.status.clone() } + #[rhai_fn(get = "steps", pure)] + pub fn get_steps(flow: &mut RhaiFlow) -> Array { + flow.steps.iter().cloned().map(Dynamic::from).collect::() + } + + // --- FlowStep Functions --- + #[rhai_fn(global)] + pub fn new_flow_step(step_order_i64: i64) -> Dynamic { + match id_from_i64_to_u32(step_order_i64) { + Ok(step_order) => { + let mut flow_step = FlowStep::default(); + flow_step.step_order = step_order; + Dynamic::from(flow_step) + }, + Err(err) => Dynamic::from(err.to_string()) + } + } + + /// Sets the flow step description + #[rhai_fn(name = "description", return_raw, global, pure)] + pub fn flow_step_description(step: &mut RhaiFlowStep, description: String) -> Result> { + let owned_step = mem::replace(step, FlowStep::default()); // Use Default trait + *step = owned_step.description(description); + Ok(step.clone()) + } + + /// Sets the flow step status + #[rhai_fn(name = "status", return_raw, global, pure)] + pub fn flow_step_status(step: &mut RhaiFlowStep, status: String) -> Result> { + let owned_step = mem::replace(step, FlowStep::default()); // Use Default trait + *step = owned_step.status(status); + Ok(step.clone()) + } + + // FlowStep Getters + #[rhai_fn(get = "id", pure)] + pub fn get_step_id(step: &mut RhaiFlowStep) -> i64 { step.base_data.id as i64 } + #[rhai_fn(get = "created_at", pure)] + pub fn get_step_created_at(step: &mut RhaiFlowStep) -> i64 { step.base_data.created_at } + #[rhai_fn(get = "modified_at", pure)] + pub fn get_step_modified_at(step: &mut RhaiFlowStep) -> i64 { step.base_data.modified_at } + #[rhai_fn(get = "description", pure)] + pub fn get_step_description(step: &mut RhaiFlowStep) -> Dynamic { + match &step.description { + Some(desc) => Dynamic::from(desc.clone()), + None => Dynamic::UNIT, + } + } + #[rhai_fn(get = "step_order", pure)] + pub fn get_step_order(step: &mut RhaiFlowStep) -> i64 { step.step_order as i64 } + #[rhai_fn(get = "status", pure)] + pub fn get_step_status(step: &mut RhaiFlowStep) -> String { step.status.clone() } + + // --- SignatureRequirement Functions --- + /// Create a new signature requirement + #[rhai_fn(global)] + pub fn new_signature_requirement(flow_step_id_i64: i64, public_key: String, message: String) -> Dynamic { + match id_from_i64_to_u32(flow_step_id_i64) { + Ok(flow_step_id) => { + let mut signature_requirement = SignatureRequirement::default(); + signature_requirement.flow_step_id = flow_step_id; + signature_requirement.public_key = public_key; + signature_requirement.message = message; + Dynamic::from(signature_requirement) + }, + Err(err) => Dynamic::from(err.to_string()) + } + } + + /// Sets the signed_by field + #[rhai_fn(name = "signed_by", return_raw, global, pure)] + pub fn signature_requirement_signed_by(sr: &mut RhaiSignatureRequirement, signed_by: String) -> Result> { + let owned_sr = mem::replace(sr, SignatureRequirement::default()); // Use Default trait + *sr = owned_sr.signed_by(signed_by); + Ok(sr.clone()) + } + + /// Sets the signature field + #[rhai_fn(name = "signature", return_raw, global, pure)] + pub fn signature_requirement_signature(sr: &mut RhaiSignatureRequirement, signature: String) -> Result> { + let owned_sr = mem::replace(sr, SignatureRequirement::default()); // Use Default trait + *sr = owned_sr.signature(signature); + Ok(sr.clone()) + } + + /// Sets the status field + #[rhai_fn(name = "status", return_raw, global, pure)] + pub fn signature_requirement_status(sr: &mut RhaiSignatureRequirement, status: String) -> Result> { + let owned_sr = mem::replace(sr, SignatureRequirement::default()); // Use Default trait + *sr = owned_sr.status(status); + Ok(sr.clone()) + } + + // SignatureRequirement Getters + #[rhai_fn(get = "id", pure)] + pub fn get_sr_id(sr: &mut RhaiSignatureRequirement) -> i64 { sr.base_data.id as i64 } + #[rhai_fn(get = "created_at", pure)] + pub fn get_sr_created_at(sr: &mut RhaiSignatureRequirement) -> i64 { sr.base_data.created_at } + #[rhai_fn(get = "modified_at", pure)] + pub fn get_sr_modified_at(sr: &mut RhaiSignatureRequirement) -> i64 { sr.base_data.modified_at } + #[rhai_fn(get = "flow_step_id", pure)] + pub fn get_sr_flow_step_id(sr: &mut RhaiSignatureRequirement) -> i64 { sr.flow_step_id as i64 } + #[rhai_fn(get = "public_key", pure)] + pub fn get_sr_public_key(sr: &mut RhaiSignatureRequirement) -> String { sr.public_key.clone() } + #[rhai_fn(get = "message", pure)] + pub fn get_sr_message(sr: &mut RhaiSignatureRequirement) -> String { sr.message.clone() } + #[rhai_fn(get = "signed_by", pure)] + pub fn get_sr_signed_by(sr: &mut RhaiSignatureRequirement) -> Dynamic { + match &sr.signed_by { + Some(signed_by) => Dynamic::from(signed_by.clone()), + None => Dynamic::UNIT, + } + } + #[rhai_fn(get = "signature", pure)] + pub fn get_sr_signature(sr: &mut RhaiSignatureRequirement) -> Dynamic { + match &sr.signature { + Some(signature) => Dynamic::from(signature.clone()), + None => Dynamic::UNIT, + } + } + #[rhai_fn(get = "status", pure)] + pub fn get_sr_status(sr: &mut RhaiSignatureRequirement) -> String { sr.status.clone() } +} + +/// Register the flow module with the Rhai engine pub fn register_flow_rhai_module(engine: &mut Engine, db: Arc) { - // --- Flow Model --- - - // Constructor: new_flow(id: u32, flow_uuid: String) - engine.register_fn("new_flow", move |context: NativeCallContext, id_i64: i64, flow_uuid: String| -> Result> { - let id_u32 = i64_to_u32(id_i64, context.position(), "id", "new_flow")?; - Ok(Flow::new(id_u32, flow_uuid)) + // Create a module for database functions + let mut db_module = Module::new(); + + // Flow database functions + let db_clone = Arc::clone(&db); + db_module.set_native_fn("save_flow", move |flow: Flow| -> Result> { + // Use the Collection trait method directly + let result = db_clone.set(&flow) + .map_err(|e| Box::new(EvalAltResult::ErrorRuntime(format!("DB Error save_flow: {:?}", e).into(), Position::NONE)))?; + + // Return the updated flow with the correct ID + Ok(result.1) }); - - // Builder methods for Flow - engine.register_fn("name", |flow: Flow, name_val: String| -> Flow { flow.name(name_val) }); - engine.register_fn("status", |flow: Flow, status_val: String| -> Flow { flow.status(status_val) }); - engine.register_fn("add_step", |flow: Flow, step: FlowStep| -> Flow { flow.add_step(step) }); - - // Getters for Flow fields - engine.register_get("id", |flow: &mut Flow| -> Result> { Ok(flow.base_data.id as i64) }); - engine.register_get("base_data", |flow: &mut Flow| -> Result> { Ok(flow.base_data.clone()) }); - engine.register_get("flow_uuid", |flow: &mut Flow| -> Result> { Ok(flow.flow_uuid.clone()) }); - engine.register_get("name", |flow: &mut Flow| -> Result> { Ok(flow.name.clone()) }); - engine.register_get("status", |flow: &mut Flow| -> Result> { Ok(flow.status.clone()) }); - engine.register_get("steps", |flow: &mut Flow| -> Result> { - let rhai_array = flow.steps.iter().cloned().map(Dynamic::from).collect::(); - Ok(rhai_array) - }); - - // --- FlowStep Model --- - - // Constructor: new_flow_step(id: u32, step_order: u32) - engine.register_fn("new_flow_step", move |context: NativeCallContext, id_i64: i64, step_order_i64: i64| -> Result> { - let id_u32 = i64_to_u32(id_i64, context.position(), "id", "new_flow_step")?; - let step_order_u32 = i64_to_u32(step_order_i64, context.position(), "step_order", "new_flow_step")?; - Ok(FlowStep::new(id_u32, step_order_u32)) - }); - - // Builder methods for FlowStep - engine.register_fn("description", |fs: FlowStep, desc_val: String| -> FlowStep { fs.description(desc_val) }); // Assuming FlowStep::description takes impl ToString - engine.register_fn("status", |fs: FlowStep, status_val: String| -> FlowStep { fs.status(status_val) }); - - // Getters for FlowStep fields - engine.register_get("id", |step: &mut FlowStep| -> Result> { Ok(step.base_data.id as i64) }); - engine.register_get("base_data", |step: &mut FlowStep| -> Result> { Ok(step.base_data.clone()) }); - engine.register_get("description", |step: &mut FlowStep| -> Result> { Ok(match step.description.clone() { Some(s) => Dynamic::from(s), None => Dynamic::from(()) }) }); - engine.register_get("step_order", |step: &mut FlowStep| -> Result> { Ok(step.step_order as i64) }); - engine.register_get("status", |step: &mut FlowStep| -> Result> { Ok(step.status.clone()) }); - - // --- SignatureRequirement Model --- - - // Constructor: new_signature_requirement(id: u32, flow_step_id: u32, public_key: String, message: String) - engine.register_fn("new_signature_requirement", - move |context: NativeCallContext, id_i64: i64, flow_step_id_i64: i64, public_key: String, message: String| - -> Result> { - let id_u32 = i64_to_u32(id_i64, context.position(), "id", "new_signature_requirement")?; - let flow_step_id_u32 = i64_to_u32(flow_step_id_i64, context.position(), "flow_step_id", "new_signature_requirement")?; - Ok(SignatureRequirement::new(id_u32, flow_step_id_u32, public_key, message)) - }); - - // Builder methods for SignatureRequirement - engine.register_fn("signed_by", |sr: SignatureRequirement, signed_by_val: String| -> SignatureRequirement { sr.signed_by(signed_by_val) }); // Assuming SR::signed_by takes impl ToString - engine.register_fn("signature", |sr: SignatureRequirement, sig_val: String| -> SignatureRequirement { sr.signature(sig_val) }); // Assuming SR::signature takes impl ToString - engine.register_fn("status", |sr: SignatureRequirement, status_val: String| -> SignatureRequirement { sr.status(status_val) }); - - // Getters for SignatureRequirement fields - engine.register_get("id", |sr: &mut SignatureRequirement| -> Result> { Ok(sr.base_data.id as i64) }); - engine.register_get("base_data", |sr: &mut SignatureRequirement| -> Result> { Ok(sr.base_data.clone()) }); - engine.register_get("flow_step_id", |sr: &mut SignatureRequirement| -> Result> { Ok(sr.flow_step_id as i64) }); - engine.register_get("public_key", |sr: &mut SignatureRequirement| -> Result> { Ok(sr.public_key.clone()) }); - engine.register_get("message", |sr: &mut SignatureRequirement| -> Result> { Ok(sr.message.clone()) }); - engine.register_get("signed_by", |sr: &mut SignatureRequirement| -> Result> { Ok(match sr.signed_by.clone() { Some(s) => Dynamic::from(s), None => Dynamic::from(()) }) }); - engine.register_get("signature", |sr: &mut SignatureRequirement| -> Result> { Ok(match sr.signature.clone() { Some(s) => Dynamic::from(s), None => Dynamic::from(()) }) }); - engine.register_get("status", |sr: &mut SignatureRequirement| -> Result> { Ok(sr.status.clone()) }); - - // --- BaseModelData Getters (if not already globally registered) --- - // Assuming these might be specific to the context or shadowed, explicit registration is safer. - engine.register_get("id", |bmd: &mut BaseModelData| -> Result> { Ok(bmd.id as i64) }); - engine.register_get("created_at", |bmd: &mut BaseModelData| -> Result> { Ok(bmd.created_at) }); - engine.register_get("modified_at", |bmd: &mut BaseModelData| -> Result> { Ok(bmd.modified_at) }); - // engine.register_get("comments", |bmd: &mut BaseModelData| Ok(bmd.comments.clone())); // Rhai might need specific handling for Vec - - // --- Database Interaction Functions --- - - let captured_db_for_set_flow = Arc::clone(&db); - engine.register_fn("set_flow", move |flow: Flow| -> Result<(), Box> { - captured_db_for_set_flow.set(&flow).map(|_| ()).map_err(|e| { - Box::new(EvalAltResult::ErrorRuntime(format!("Failed to set Flow (ID: {}): {}", flow.base_data.id, e).into(), Position::NONE)) - }) - }); - - let captured_db_for_get_flow = Arc::clone(&db); - engine.register_fn("get_flow_by_id", move |context: NativeCallContext, id_i64: i64| -> Result> { - let id_u32 = i64_to_u32(id_i64, context.position(), "id", "get_flow_by_id")?; - captured_db_for_get_flow.get_by_id(id_u32) - .map_err(|e| Box::new(EvalAltResult::ErrorRuntime(format!("Error getting Flow (ID: {}): {}", id_u32, e).into(), Position::NONE)))? + + let db_clone = Arc::clone(&db); + db_module.set_native_fn("get_flow_by_id", move |id_i64: INT| -> Result> { + let id_u32 = id_from_i64_to_u32(id_i64)?; + // Use the Collection trait method directly + db_clone.get_by_id(id_u32) + .map_err(|e| Box::new(EvalAltResult::ErrorRuntime(format!("DB Error get_flow_by_id: {:?}", e).into(), Position::NONE)))? .ok_or_else(|| Box::new(EvalAltResult::ErrorRuntime(format!("Flow with ID {} not found", id_u32).into(), Position::NONE))) }); - // Add get_flows_by_uuid, flow_exists etc. as needed, using wrap_vec_return for Vec results. - - // FlowStep DB functions are removed as FlowSteps are now part of Flow. - - let captured_db_for_set_sig_req = Arc::clone(&db); - engine.register_fn("set_signature_requirement", move |sr: SignatureRequirement| -> Result<(), Box> { - captured_db_for_set_sig_req.set(&sr).map(|_| ()).map_err(|e| { - Box::new(EvalAltResult::ErrorRuntime(format!("Failed to set SignatureRequirement (ID: {}): {}", sr.base_data.id, e).into(), Position::NONE)) - }) + + let db_clone = Arc::clone(&db); + db_module.set_native_fn("delete_flow", move |id_i64: INT| -> Result<(), Box> { + let id_u32 = id_from_i64_to_u32(id_i64)?; + // Use the Collection trait method directly + let collection = db_clone.collection::() + .map_err(|e| Box::new(EvalAltResult::ErrorRuntime( + format!("Failed to get flow collection: {:?}", e).into(), + Position::NONE + )))?; + collection.delete_by_id(id_u32) + .map_err(|e| Box::new(EvalAltResult::ErrorRuntime( + format!("Failed to delete Flow (ID: {}): {:?}", id_u32, e).into(), + Position::NONE + ))) }); - - let captured_db_for_get_sig_req = Arc::clone(&db); - engine.register_fn("get_signature_requirement_by_id", - move |context: NativeCallContext, id_i64: i64| - -> Result> { - let id_u32 = i64_to_u32(id_i64, context.position(), "id", "get_signature_requirement_by_id")?; - captured_db_for_get_sig_req.get_by_id(id_u32) - .map_err(|e| Box::new(EvalAltResult::ErrorRuntime(format!("Error getting SignatureRequirement (ID: {}): {}", id_u32, e).into(), Position::NONE)))? - .ok_or_else(|| Box::new(EvalAltResult::ErrorRuntime(format!("SignatureRequirement with ID {} not found", id_u32).into(), Position::NONE))) + + let db_clone = Arc::clone(&db); + db_module.set_native_fn("list_flows", move || -> Result> { + let collection = db_clone.collection::() + .map_err(|e| Box::new(EvalAltResult::ErrorRuntime( + format!("Failed to get flow collection: {:?}", e).into(), + Position::NONE + )))?; + let flows = collection.get_all() + .map_err(|e| Box::new(EvalAltResult::ErrorRuntime( + format!("Failed to get all flows: {:?}", e).into(), + Position::NONE + )))?; + let mut array = Array::new(); + for flow in flows { + array.push(Dynamic::from(flow)); + } + Ok(Dynamic::from(array)) }); - + + // FlowStep database functions + let db_clone = Arc::clone(&db); + db_module.set_native_fn("save_flow_step", move |step: FlowStep| -> Result> { + // Use the Collection trait method directly + let result = db_clone.set(&step) + .map_err(|e| Box::new(EvalAltResult::ErrorRuntime(format!("DB Error save_flow_step: {:?}", e).into(), Position::NONE)))?; + + // Return the updated flow step with the correct ID + Ok(result.1) + }); + + let db_clone = Arc::clone(&db); + db_module.set_native_fn("get_flow_step_by_id", move |id_i64: INT| -> Result> { + let id_u32 = id_from_i64_to_u32(id_i64)?; + // Use the Collection trait method directly + db_clone.get_by_id(id_u32) + .map_err(|e| Box::new(EvalAltResult::ErrorRuntime(format!("DB Error get_flow_step_by_id: {:?}", e).into(), Position::NONE)))? + .ok_or_else(|| Box::new(EvalAltResult::ErrorRuntime(format!("FlowStep with ID {} not found", id_u32).into(), Position::NONE))) + }); + + let db_clone = Arc::clone(&db); + db_module.set_native_fn("delete_flow_step", move |id_i64: INT| -> Result<(), Box> { + let id_u32 = id_from_i64_to_u32(id_i64)?; + // Use the Collection trait method directly + let collection = db_clone.collection::() + .map_err(|e| Box::new(EvalAltResult::ErrorRuntime( + format!("Failed to get flow step collection: {:?}", e).into(), + Position::NONE + )))?; + collection.delete_by_id(id_u32) + .map_err(|e| Box::new(EvalAltResult::ErrorRuntime( + format!("Failed to delete FlowStep (ID: {}): {:?}", id_u32, e).into(), + Position::NONE + ))) + }); + + let db_clone = Arc::clone(&db); + db_module.set_native_fn("list_flow_steps", move || -> Result> { + let collection = db_clone.collection::() + .map_err(|e| Box::new(EvalAltResult::ErrorRuntime( + format!("Failed to get flow step collection: {:?}", e).into(), + Position::NONE + )))?; + let steps = collection.get_all() + .map_err(|e| Box::new(EvalAltResult::ErrorRuntime( + format!("Failed to get all flow steps: {:?}", e).into(), + Position::NONE + )))?; + let mut array = Array::new(); + for step in steps { + array.push(Dynamic::from(step)); + } + Ok(Dynamic::from(array)) + }); + + // SignatureRequirement database functions + let db_clone = Arc::clone(&db); + db_module.set_native_fn("save_signature_requirement", move |sr: SignatureRequirement| -> Result> { + // Use the Collection trait method directly + let result = db_clone.set(&sr) + .map_err(|e| Box::new(EvalAltResult::ErrorRuntime(format!("DB Error save_signature_requirement: {:?}", e).into(), Position::NONE)))?; + + // Return the updated signature requirement with the correct ID + Ok(result.1) + }); + + let db_clone = Arc::clone(&db); + db_module.set_native_fn("get_signature_requirement_by_id", move |id_i64: INT| -> Result> { + let id_u32 = id_from_i64_to_u32(id_i64)?; + // Use the Collection trait method directly + db_clone.get_by_id(id_u32) + .map_err(|e| Box::new(EvalAltResult::ErrorRuntime(format!("DB Error get_signature_requirement_by_id: {:?}", e).into(), Position::NONE)))? + .ok_or_else(|| Box::new(EvalAltResult::ErrorRuntime(format!("SignatureRequirement with ID {} not found", id_u32).into(), Position::NONE))) + }); + + let db_clone = Arc::clone(&db); + db_module.set_native_fn("delete_signature_requirement", move |id_i64: INT| -> Result<(), Box> { + let id_u32 = id_from_i64_to_u32(id_i64)?; + // Use the Collection trait method directly + let collection = db_clone.collection::() + .map_err(|e| Box::new(EvalAltResult::ErrorRuntime( + format!("Failed to get signature requirement collection: {:?}", e).into(), + Position::NONE + )))?; + collection.delete_by_id(id_u32) + .map_err(|e| Box::new(EvalAltResult::ErrorRuntime( + format!("Failed to delete SignatureRequirement (ID: {}): {:?}", id_u32, e).into(), + Position::NONE + ))) + }); + + let db_clone = Arc::clone(&db); + db_module.set_native_fn("list_signature_requirements", move || -> Result> { + let collection = db_clone.collection::() + .map_err(|e| Box::new(EvalAltResult::ErrorRuntime( + format!("Failed to get signature requirement collection: {:?}", e).into(), + Position::NONE + )))?; + let srs = collection.get_all() + .map_err(|e| Box::new(EvalAltResult::ErrorRuntime( + format!("Failed to get all signature requirements: {:?}", e).into(), + Position::NONE + )))?; + let mut array = Array::new(); + for sr in srs { + array.push(Dynamic::from(sr)); + } + Ok(Dynamic::from(array)) + }); + + // Register the database module globally + engine.register_static_module("db", db_module.into()); + + // Register the flow module using exported_module! macro + let module = exported_module!(rhai_flow_module); + engine.register_global_module(module.into()); + println!("Flow Rhai module registered."); } diff --git a/heromodels/src/models/flow/signature_requirement.rs b/heromodels/src/models/flow/signature_requirement.rs index 44007cb..05093f6 100644 --- a/heromodels/src/models/flow/signature_requirement.rs +++ b/heromodels/src/models/flow/signature_requirement.rs @@ -1,9 +1,10 @@ use heromodels_core::BaseModelData; use heromodels_derive::model; use serde::{Deserialize, Serialize}; +use std::default::Default; /// Represents a signature requirement for a flow step. -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, Default)] #[model] pub struct SignatureRequirement { /// Base model data. diff --git a/heromodels/src/models/governance/attached_file.rs b/heromodels/src/models/governance/attached_file.rs new file mode 100644 index 0000000..8885924 --- /dev/null +++ b/heromodels/src/models/governance/attached_file.rs @@ -0,0 +1,27 @@ +// heromodels/src/models/governance/attached_file.rs +use heromodels_core::BaseModelData; +use heromodels_derive::model; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)] +#[model] +pub struct AttachedFile { + pub base_data: BaseModelData, // Provides id, created_at, updated_at + pub name: String, + pub url: String, + pub file_type: String, // e.g., "pdf", "image/jpeg", "application/msword" + pub size_bytes: u64, + // Optional: could add uploader_id: u32 if needed +} + +impl AttachedFile { + pub fn new(name: String, url: String, file_type: String, size_bytes: u64) -> Self { + Self { + base_data: BaseModelData::new(), + name, + url, + file_type, + size_bytes, + } + } +} diff --git a/heromodels/src/models/governance/mod.rs b/heromodels/src/models/governance/mod.rs index b3df030..5570233 100644 --- a/heromodels/src/models/governance/mod.rs +++ b/heromodels/src/models/governance/mod.rs @@ -2,4 +2,7 @@ // This module will contain the Proposal model and related types. pub mod proposal; -pub use self::proposal::{Proposal, Ballot, VoteOption, ProposalStatus, VoteEventStatus}; \ No newline at end of file +pub mod attached_file; + +pub use self::proposal::{Proposal, Ballot, VoteOption, ProposalStatus, VoteEventStatus}; +pub use self::attached_file::AttachedFile; \ No newline at end of file diff --git a/heromodels/src/models/governance/proposal.rs b/heromodels/src/models/governance/proposal.rs index 0440f89..8e9a1a3 100644 --- a/heromodels/src/models/governance/proposal.rs +++ b/heromodels/src/models/governance/proposal.rs @@ -3,10 +3,11 @@ use chrono::{DateTime, Utc}; use heromodels_derive::model; // For #[model] use rhai::{CustomType, TypeBuilder}; -use rhai_autobind_macros::rhai_model_export; use serde::{Deserialize, Serialize}; use heromodels_core::BaseModelData; +use crate::models::core::Comment; +use super::AttachedFile; // --- Enums --- @@ -29,6 +30,7 @@ impl Default for ProposalStatus { /// VoteEventStatus represents the status of the voting process for a proposal #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] pub enum VoteEventStatus { + Upcoming, // Voting is scheduled but not yet open Open, // Voting is currently open Closed, // Voting has finished Cancelled, // The voting event was cancelled @@ -36,14 +38,14 @@ pub enum VoteEventStatus { impl Default for VoteEventStatus { fn default() -> Self { - VoteEventStatus::Open + VoteEventStatus::Upcoming } } // --- Structs --- /// VoteOption represents a specific choice that can be voted on -#[derive(Debug, Clone, Serialize, Deserialize, CustomType)] +#[derive(Debug, Clone, Serialize, Deserialize, CustomType, Default)] pub struct VoteOption { pub id: u8, // Simple identifier for this option pub text: String, // Descriptive text of the option @@ -65,8 +67,8 @@ impl VoteOption { } /// Ballot represents an individual vote cast by a user -#[derive(Debug, Clone, Serialize, Deserialize, CustomType)] -#[rhai_model_export(db_type = "std::sync::Arc")] +#[derive(Debug, Clone, Serialize, Deserialize, CustomType, Default)] +// Removed rhai_model_export macro as it's causing compilation errors #[model] // Has base.Base in V spec pub struct Ballot { pub base_data: BaseModelData, @@ -102,7 +104,7 @@ impl Ballot { /// Proposal represents a governance proposal that can be voted upon. #[derive(Debug, Clone, Serialize, Deserialize, CustomType)] -#[rhai_model_export(db_type = "std::sync::Arc")] +// Removed rhai_model_export macro as it's causing compilation errors #[model] // Has base.Base in V spec pub struct Proposal { pub base_data: BaseModelData, @@ -113,9 +115,6 @@ pub struct Proposal { pub description: String, pub status: ProposalStatus, - pub created_at: DateTime, - pub updated_at: DateTime, - // Voting event aspects pub vote_start_date: DateTime, pub vote_end_date: DateTime, @@ -123,6 +122,35 @@ pub struct Proposal { pub options: Vec, pub ballots: Vec, // This will store actual Ballot structs pub private_group: Option>, // Optional list of eligible user IDs + + pub tags: Vec, + pub comments: Vec, + pub attached_files: Vec, + pub urgency_score: Option, +} + +impl Default for Proposal { + fn default() -> Self { + Self { + base_data: BaseModelData::new(), + creator_id: "".to_string(), + creator_name: String::new(), // Added missing field + title: "".to_string(), + description: "".to_string(), + status: ProposalStatus::Draft, + // created_at and updated_at are now in base_data + vote_start_date: Utc::now(), + vote_end_date: Utc::now(), + vote_status: VoteEventStatus::Upcoming, + options: vec![], + ballots: vec![], + private_group: None, + tags: vec![], + comments: vec![], + attached_files: vec![], + urgency_score: None, + } + } } impl Proposal { @@ -142,10 +170,8 @@ impl Proposal { title: impl ToString, description: impl ToString, status: ProposalStatus, - created_at: DateTime, - updated_at: DateTime, - vote_start_date: DateTime, - vote_end_date: DateTime, + tags: Vec, + urgency_score: Option, ) -> Self { let mut base_data = BaseModelData::new(); if let Some(id) = id { @@ -159,14 +185,16 @@ impl Proposal { title: title.to_string(), description: description.to_string(), status, - created_at, - updated_at, - vote_start_date, - vote_end_date, - vote_status: VoteEventStatus::Open, // Default to open when created + vote_start_date: Utc::now(), + vote_end_date: Utc::now(), + vote_status: VoteEventStatus::Upcoming, options: Vec::new(), ballots: Vec::new(), private_group: None, + tags, + comments: Vec::new(), + attached_files: Vec::new(), + urgency_score, } } diff --git a/heromodels/src/models/legal/contract.rs b/heromodels/src/models/legal/contract.rs index 4e871fc..f6cdd79 100644 --- a/heromodels/src/models/legal/contract.rs +++ b/heromodels/src/models/legal/contract.rs @@ -51,7 +51,7 @@ impl fmt::Display for SignerStatus { // --- Structs for nested data --- /// ContractRevision represents a version of the contract content -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)] pub struct ContractRevision { pub version: u32, pub content: String, @@ -78,7 +78,7 @@ impl ContractRevision { } /// ContractSigner represents a party involved in signing a contract -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)] pub struct ContractSigner { pub id: String, // Unique ID for the signer (UUID string) pub name: String, @@ -129,7 +129,7 @@ impl ContractSigner { // --- Main Contract Model --- /// Represents a legal agreement -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)] #[model] pub struct Contract { pub base_data: BaseModelData, // Provides id (u32), created_at (u64), updated_at (u64) diff --git a/heromodels/src/models/mod.rs b/heromodels/src/models/mod.rs index 5e37df7..b4c8db2 100644 --- a/heromodels/src/models/mod.rs +++ b/heromodels/src/models/mod.rs @@ -15,7 +15,7 @@ pub use core::Comment; pub use userexample::User; // pub use productexample::Product; // Temporarily remove pub use calendar::{Calendar, Event, Attendee, AttendanceStatus}; -pub use governance::{Proposal, ProposalStatus, VoteEventStatus, Ballot, VoteOption}; +pub use governance::{Proposal, ProposalStatus, VoteEventStatus, Ballot, VoteOption, AttachedFile}; pub use finance::{Account, Asset, AssetType}; pub use finance::marketplace::{Listing, ListingStatus, ListingType, Bid, BidStatus}; pub use legal::{Contract, ContractRevision, ContractSigner, ContractStatus, SignerStatus}; @@ -23,6 +23,7 @@ pub use flow::{Flow, FlowStep, SignatureRequirement}; pub use biz::{Sale, SaleItem, SaleStatus}; pub use flow::register_flow_rhai_module; +#[cfg(feature = "rhai")] pub use calendar::register_calendar_rhai_module; pub use legal::register_legal_rhai_module; #[cfg(feature = "rhai")] diff --git a/heromodels/src/models/projects/epic.rs b/heromodels/src/models/projects/epic.rs new file mode 100644 index 0000000..d96b4d2 --- /dev/null +++ b/heromodels/src/models/projects/epic.rs @@ -0,0 +1,83 @@ +// heromodels/src/models/projects/epic.rs + +use chrono::{DateTime, Utc}; +use heromodels_core::BaseModelData; +use heromodels_derive::model; +use serde::{Deserialize, Serialize}; +use rhai::{CustomType, TypeBuilder}; + +use super::base::Status as ProjectStatus; // Using the generic project status for now + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, CustomType)] +#[model] +pub struct Epic { + pub base_data: BaseModelData, + pub name: String, + pub description: Option, + pub status: ProjectStatus, // Or a new EpicStatus enum if needed + + pub project_id: Option, // Link to a general project/board + + pub start_date: Option>, + pub due_date: Option>, + + pub tags: Vec, + + // Explicitly list task IDs belonging to this epic + // This helps in querying and avoids relying solely on tasks pointing to the epic. + pub child_task_ids: Vec, +} + +impl Epic { + pub fn new( + name: String, + description: Option, + status: ProjectStatus, + project_id: Option, + start_date: Option>, + due_date: Option>, + tags: Vec, + ) -> Self { + Self { + base_data: BaseModelData::new(), + name, + description, + status, + project_id, + start_date, + due_date, + tags, + child_task_ids: Vec::new(), // Initialize as empty + } + } + + // Method to add a task to this epic + pub fn add_task_id(mut self, task_id: u32) -> Self { + if !self.child_task_ids.contains(&task_id) { + self.child_task_ids.push(task_id); + } + self + } + + // Method to remove a task from this epic + pub fn remove_task_id(mut self, task_id: u32) -> Self { + self.child_task_ids.retain(|&id| id != task_id); + self + } +} + +impl Default for Epic { + fn default() -> Self { + Self { + base_data: BaseModelData::new(), + name: String::new(), + description: None, + status: ProjectStatus::default(), + project_id: None, + start_date: None, + due_date: None, + tags: Vec::new(), + child_task_ids: Vec::new(), + } + } +} diff --git a/heromodels/src/models/projects/mod.rs b/heromodels/src/models/projects/mod.rs index 10dc342..ca20fb8 100644 --- a/heromodels/src/models/projects/mod.rs +++ b/heromodels/src/models/projects/mod.rs @@ -1,6 +1,11 @@ // heromodels/src/models/projects/mod.rs pub mod base; +pub mod task_enums; +pub mod task; +pub mod epic; +pub mod sprint_enums; +pub mod sprint; // pub mod epic; // pub mod issue; // pub mod kanban; @@ -8,6 +13,11 @@ pub mod base; // pub mod story; pub use base::*; +pub use task_enums::*; +pub use task::*; +pub use epic::*; +pub use sprint_enums::*; +pub use sprint::*; // pub use epic::*; // pub use issue::*; // pub use kanban::*; diff --git a/heromodels/src/models/projects/sprint.rs b/heromodels/src/models/projects/sprint.rs new file mode 100644 index 0000000..883857f --- /dev/null +++ b/heromodels/src/models/projects/sprint.rs @@ -0,0 +1,81 @@ +// heromodels/src/models/projects/sprint.rs + +use chrono::{DateTime, Utc}; +use heromodels_core::BaseModelData; +use heromodels_derive::model; +use serde::{Deserialize, Serialize}; +use rhai::{CustomType, TypeBuilder}; + +use super::sprint_enums::SprintStatus; // Import our new enum + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, CustomType)] +#[model] +pub struct Sprint { + pub base_data: BaseModelData, + pub name: String, + pub description: Option, + pub status: SprintStatus, + pub goal: Option, // Sprint goal + + pub project_id: Option, // Link to a general project/board + + pub start_date: Option>, + pub end_date: Option>, // Changed from due_date for sprints + + // Explicitly list task IDs belonging to this sprint + pub task_ids: Vec, +} + +impl Sprint { + pub fn new( + name: String, + description: Option, + status: SprintStatus, + goal: Option, + project_id: Option, + start_date: Option>, + end_date: Option>, + ) -> Self { + Self { + base_data: BaseModelData::new(), + name, + description, + status, + goal, + project_id, + start_date, + end_date, + task_ids: Vec::new(), // Initialize as empty + } + } + + // Method to add a task to this sprint + pub fn add_task_id(mut self, task_id: u32) -> Self { + if !self.task_ids.contains(&task_id) { + self.task_ids.push(task_id); + } + self + } + + // Method to remove a task from this sprint + pub fn remove_task_id(mut self, task_id: u32) -> Self { + self.task_ids.retain(|&id| id != task_id); + self + } +} + +impl Default for Sprint { + fn default() -> Self { + Self { + base_data: BaseModelData::new(), + name: String::new(), + description: None, + status: SprintStatus::default(), + goal: None, + project_id: None, + start_date: None, + end_date: None, + task_ids: Vec::new(), + } + } +} diff --git a/heromodels/src/models/projects/sprint_enums.rs b/heromodels/src/models/projects/sprint_enums.rs new file mode 100644 index 0000000..c723752 --- /dev/null +++ b/heromodels/src/models/projects/sprint_enums.rs @@ -0,0 +1,16 @@ +// heromodels/src/models/projects/sprint_enums.rs +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub enum SprintStatus { + Planning, + Active, + Completed, + Paused, +} + +impl Default for SprintStatus { + fn default() -> Self { + SprintStatus::Planning + } +} diff --git a/heromodels/src/models/projects/task.rs b/heromodels/src/models/projects/task.rs new file mode 100644 index 0000000..4d008dc --- /dev/null +++ b/heromodels/src/models/projects/task.rs @@ -0,0 +1,93 @@ +// heromodels/src/models/projects/task.rs + +use chrono::{DateTime, Utc}; +use heromodels_core::BaseModelData; +use heromodels_derive::model; +use serde::{Deserialize, Serialize}; +use rhai::{CustomType, TypeBuilder}; // Assuming rhai might be used + +use super::task_enums::{TaskStatus, TaskPriority}; // Import our new enums + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, CustomType)] +#[model] // This will provide id, created_at, updated_at via base_data +pub struct Task { + pub base_data: BaseModelData, + pub title: String, + pub description: Option, + pub status: TaskStatus, + pub priority: TaskPriority, + + pub assignee_id: Option, // User ID + pub reporter_id: Option, // User ID + + pub parent_task_id: Option, // For subtasks + pub epic_id: Option, + pub sprint_id: Option, + pub project_id: Option, // Link to a general project/board if applicable + + pub due_date: Option>, + pub estimated_time_hours: Option, + pub logged_time_hours: Option, + pub tags: Vec, +} + +impl Task { + #[allow(clippy::too_many_arguments)] + pub fn new( + title: String, + description: Option, + status: TaskStatus, + priority: TaskPriority, + assignee_id: Option, + reporter_id: Option, + parent_task_id: Option, + epic_id: Option, + sprint_id: Option, + project_id: Option, + due_date: Option>, + estimated_time_hours: Option, + logged_time_hours: Option, + tags: Vec, + ) -> Self { + Self { + base_data: BaseModelData::new(), + title, + description, + status, + priority, + assignee_id, + reporter_id, + parent_task_id, + epic_id, + sprint_id, + project_id, + due_date, + estimated_time_hours, + logged_time_hours, + tags, + } + } +} + +// Add Default implementation +impl Default for Task { + fn default() -> Self { + Self { + base_data: BaseModelData::new(), + title: String::new(), + description: None, + status: TaskStatus::default(), + priority: TaskPriority::default(), + assignee_id: None, + reporter_id: None, + parent_task_id: None, + epic_id: None, + sprint_id: None, + project_id: None, + due_date: None, + estimated_time_hours: None, + logged_time_hours: None, + tags: Vec::new(), + } + } +} diff --git a/heromodels/src/models/projects/task_enums.rs b/heromodels/src/models/projects/task_enums.rs new file mode 100644 index 0000000..811e19d --- /dev/null +++ b/heromodels/src/models/projects/task_enums.rs @@ -0,0 +1,32 @@ +// heromodels/src/models/projects/task_enums.rs +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub enum TaskStatus { + ToDo, + InProgress, + InReview, + Done, + Blocked, + Backlog, +} + +impl Default for TaskStatus { + fn default() -> Self { + TaskStatus::ToDo + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub enum TaskPriority { + Low, + Medium, + High, + Urgent, +} + +impl Default for TaskPriority { + fn default() -> Self { + TaskPriority::Medium + } +} diff --git a/heromodels/src/models/userexample/user.rs b/heromodels/src/models/userexample/user.rs index c9f68f0..76d0598 100644 --- a/heromodels/src/models/userexample/user.rs +++ b/heromodels/src/models/userexample/user.rs @@ -3,7 +3,7 @@ use heromodels_derive::model; use serde::{Deserialize, Serialize}; /// Represents a user in the system -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, Default)] #[model] pub struct User { /// Base model data diff --git a/heromodels/tmp/event_test_db/index/0.db b/heromodels/tmp/event_test_db/index/0.db new file mode 100644 index 0000000..0ad682e Binary files /dev/null and b/heromodels/tmp/event_test_db/index/0.db differ diff --git a/heromodels/tmp/event_test_db/index/lookup/.inc b/heromodels/tmp/event_test_db/index/lookup/.inc new file mode 100644 index 0000000..d8263ee --- /dev/null +++ b/heromodels/tmp/event_test_db/index/lookup/.inc @@ -0,0 +1 @@ +2 \ No newline at end of file