fmt, fixes and additions

This commit is contained in:
timurgordon 2025-06-19 13:18:10 +03:00
parent 6b3cbfc4b2
commit e91a44ce37
86 changed files with 5292 additions and 2844 deletions

View File

@ -1,5 +1,5 @@
use heromodels_derive::model;
use serde::{Serialize, Deserialize};
use serde::{Deserialize, Serialize};
// Define the necessary structs and traits for testing
#[derive(Debug, Clone, Serialize, Deserialize)]
@ -46,10 +46,10 @@ pub trait Index {
#[model]
struct TestUser {
base_data: BaseModelData,
#[index]
username: String,
#[index]
is_active: bool,
}
@ -59,10 +59,10 @@ struct TestUser {
#[model]
struct TestUserWithCustomIndex {
base_data: BaseModelData,
#[index(name = "custom_username")]
username: String,
#[index]
is_active: bool,
}
@ -70,13 +70,13 @@ struct TestUserWithCustomIndex {
#[test]
fn test_basic_model() {
assert_eq!(TestUser::db_prefix(), "test_user");
let user = TestUser {
base_data: BaseModelData::new(1),
username: "test".to_string(),
is_active: true,
};
let keys = user.db_keys();
assert_eq!(keys.len(), 2);
assert_eq!(keys[0].name, "username");
@ -92,10 +92,10 @@ fn test_custom_index_name() {
username: "test".to_string(),
is_active: true,
};
// Check that the Username struct uses the custom index name
assert_eq!(Username::key(), "custom_username");
// Check that the db_keys method returns the correct keys
let keys = user.db_keys();
assert_eq!(keys.len(), 2);
@ -103,4 +103,4 @@ fn test_custom_index_name() {
assert_eq!(keys[0].value, "test");
assert_eq!(keys[1].name, "is_active");
assert_eq!(keys[1].value, "true");
}
}

View File

@ -42,10 +42,6 @@ path = "examples/finance_example/main.rs"
name = "calendar_rhai"
path = "examples/calendar_rhai/example.rs"
[[example]]
name = "calendar_rhai_client"
path = "examples/calendar_rhai_client/example.rs"
[[example]]
name = "flow_rhai"
path = "examples/flow_rhai/example.rs"

View File

@ -68,10 +68,26 @@ fn main() {
.build();
// Save all users to database and get their assigned IDs and updated models
let (user1_id, db_user1) = db.collection().expect("can open user collection").set(&user1).expect("can set user");
let (user2_id, db_user2) = db.collection().expect("can open user collection").set(&user2).expect("can set user");
let (user3_id, db_user3) = db.collection().expect("can open user collection").set(&user3).expect("can set user");
let (user4_id, db_user4) = db.collection().expect("can open user collection").set(&user4).expect("can set user");
let (user1_id, db_user1) = db
.collection()
.expect("can open user collection")
.set(&user1)
.expect("can set user");
let (user2_id, db_user2) = db
.collection()
.expect("can open user collection")
.set(&user2)
.expect("can set user");
let (user3_id, db_user3) = db
.collection()
.expect("can open user collection")
.set(&user3)
.expect("can set user");
let (user4_id, db_user4) = db
.collection()
.expect("can open user collection")
.set(&user4)
.expect("can set user");
println!("User 1 assigned ID: {}", user1_id);
println!("User 2 assigned ID: {}", user2_id);
@ -170,7 +186,8 @@ fn main() {
.build();
// Save the comment and get its assigned ID and updated model
let (comment_id, db_comment) = db.collection()
let (comment_id, db_comment) = db
.collection()
.expect("can open comment collection")
.set(&comment)
.expect("can set comment");
@ -186,7 +203,8 @@ fn main() {
updated_user.base_data.add_comment(db_comment.get_id());
// Save the updated user and get the new version
let (_, user_with_comment) = db.collection::<User>()
let (_, user_with_comment) = db
.collection::<User>()
.expect("can open user collection")
.set(&updated_user)
.expect("can set updated user");

View File

@ -1,8 +1,8 @@
use rhai::{Engine, EvalAltResult, Scope};
use std::sync::Arc;
use heromodels::db::hero::OurDB; // Corrected path for OurDB
use heromodels::models::biz::register_biz_rhai_module; // Corrected path
use rhai::{Engine, EvalAltResult, Scope};
use std::fs;
use std::sync::Arc;
fn main() -> Result<(), Box<EvalAltResult>> {
println!("Executing Rhai script: examples/biz_rhai/biz.rhai");
@ -20,8 +20,12 @@ fn main() -> Result<(), Box<EvalAltResult>> {
// Read the Rhai script from file
let script_path = "examples/biz_rhai/biz.rhai";
let script_content = fs::read_to_string(script_path)
.map_err(|e| Box::new(EvalAltResult::ErrorSystem(format!("Cannot read script file: {}", script_path), e.into())))?;
let script_content = fs::read_to_string(script_path).map_err(|e| {
Box::new(EvalAltResult::ErrorSystem(
format!("Cannot read script file: {}", script_path),
e.into(),
))
})?;
// Create a new scope
let mut scope = Scope::new();

View File

@ -1,6 +1,6 @@
use chrono::{Duration, Utc};
use heromodels::db::{Collection, Db};
use heromodels::models::calendar::{Attendee, AttendanceStatus, Calendar, Event};
use heromodels::models::calendar::{AttendanceStatus, Attendee, Calendar, Event};
use heromodels_core::Model;
fn main() {
@ -12,10 +12,8 @@ fn main() {
println!("====================================");
// --- Create Attendees ---
let attendee1 = Attendee::new("user_123".to_string())
.status(AttendanceStatus::Accepted);
let attendee2 = Attendee::new("user_456".to_string())
.status(AttendanceStatus::Tentative);
let attendee1 = Attendee::new("user_123".to_string()).status(AttendanceStatus::Accepted);
let attendee2 = Attendee::new("user_456".to_string()).status(AttendanceStatus::Tentative);
let attendee3 = Attendee::new("user_789".to_string()); // Default NoResponse
// --- Create Events ---
@ -45,7 +43,7 @@ fn main() {
"event_gamma".to_string(),
"Client Call",
now + Duration::days(2),
now + Duration::days(2) + Duration::seconds(3600)
now + Duration::days(2) + Duration::seconds(3600),
);
// --- Create Calendars ---
@ -58,25 +56,43 @@ fn main() {
.add_event(event2.clone());
// Create a calendar with auto-generated ID (explicit IDs are no longer supported)
let calendar2 = Calendar::new(None, "Personal Calendar")
.add_event(event3_for_calendar2.clone());
let calendar2 =
Calendar::new(None, "Personal Calendar").add_event(event3_for_calendar2.clone());
// --- Store Calendars in DB ---
let cal_collection = db.collection::<Calendar>().expect("can open calendar collection");
let cal_collection = db
.collection::<Calendar>()
.expect("can open calendar collection");
let (_, calendar1) = cal_collection.set(&calendar1).expect("can set calendar1");
let (_, calendar2) = cal_collection.set(&calendar2).expect("can set calendar2");
println!("Created calendar1 (ID: {}): Name - '{}'", calendar1.get_id(), calendar1.name);
println!("Created calendar2 (ID: {}): Name - '{}'", calendar2.get_id(), calendar2.name);
println!(
"Created calendar1 (ID: {}): Name - '{}'",
calendar1.get_id(),
calendar1.name
);
println!(
"Created calendar2 (ID: {}): Name - '{}'",
calendar2.get_id(),
calendar2.name
);
// --- Retrieve a Calendar by ID ---
let stored_calendar1_opt = cal_collection.get_by_id(calendar1.get_id()).expect("can try to load calendar1");
assert!(stored_calendar1_opt.is_some(), "Calendar1 should be found in DB");
let stored_calendar1_opt = cal_collection
.get_by_id(calendar1.get_id())
.expect("can try to load calendar1");
assert!(
stored_calendar1_opt.is_some(),
"Calendar1 should be found in DB"
);
let mut stored_calendar1 = stored_calendar1_opt.unwrap();
println!("\nRetrieved calendar1 from DB: Name - '{}', Events count: {}", stored_calendar1.name, stored_calendar1.events.len());
println!(
"\nRetrieved calendar1 from DB: Name - '{}', Events count: {}",
stored_calendar1.name,
stored_calendar1.events.len()
);
assert_eq!(stored_calendar1.name, "Work Calendar");
assert_eq!(stored_calendar1.events.len(), 2);
assert_eq!(stored_calendar1.events[0].title, "Team Meeting");
@ -84,49 +100,83 @@ fn main() {
// --- Modify a Calendar (Reschedule an Event) ---
let event_id_to_reschedule = event1.id.as_str();
let new_start_time = now + Duration::seconds(10800); // 3 hours from now
let new_end_time = now + Duration::seconds(14400); // 4 hours from now
let new_end_time = now + Duration::seconds(14400); // 4 hours from now
stored_calendar1 = stored_calendar1.update_event(event_id_to_reschedule, |event_to_update| {
println!("Rescheduling event '{}'...", event_to_update.title);
event_to_update.reschedule(new_start_time, new_end_time)
});
let rescheduled_event = stored_calendar1.events.iter().find(|e| e.id == event_id_to_reschedule)
let rescheduled_event = stored_calendar1
.events
.iter()
.find(|e| e.id == event_id_to_reschedule)
.expect("Rescheduled event should exist");
assert_eq!(rescheduled_event.start_time, new_start_time);
assert_eq!(rescheduled_event.end_time, new_end_time);
println!("Event '{}' rescheduled in stored_calendar1.", rescheduled_event.title);
println!(
"Event '{}' rescheduled in stored_calendar1.",
rescheduled_event.title
);
// --- Store the modified calendar ---
let (_, mut stored_calendar1) = cal_collection.set(&stored_calendar1).expect("can set modified calendar1");
let re_retrieved_calendar1_opt = cal_collection.get_by_id(calendar1.get_id()).expect("can try to load modified calendar1");
let (_, mut stored_calendar1) = cal_collection
.set(&stored_calendar1)
.expect("can set modified calendar1");
let re_retrieved_calendar1_opt = cal_collection
.get_by_id(calendar1.get_id())
.expect("can try to load modified calendar1");
let re_retrieved_calendar1 = re_retrieved_calendar1_opt.unwrap();
let re_retrieved_event = re_retrieved_calendar1.events.iter().find(|e| e.id == event_id_to_reschedule)
let re_retrieved_event = re_retrieved_calendar1
.events
.iter()
.find(|e| e.id == event_id_to_reschedule)
.expect("Rescheduled event should exist in re-retrieved calendar");
assert_eq!(re_retrieved_event.start_time, new_start_time, "Reschedule not persisted correctly");
assert_eq!(
re_retrieved_event.start_time, new_start_time,
"Reschedule not persisted correctly"
);
println!("\nModified and re-saved calendar1. Rescheduled event start time: {}", re_retrieved_event.start_time);
println!(
"\nModified and re-saved calendar1. Rescheduled event start time: {}",
re_retrieved_event.start_time
);
// --- Add a new event to an existing calendar ---
let event4_new = Event::new(
"event_delta".to_string(),
"1-on-1",
now + Duration::days(3),
now + Duration::days(3) + Duration::seconds(1800) // 30 minutes
now + Duration::days(3) + Duration::seconds(1800), // 30 minutes
);
stored_calendar1 = stored_calendar1.add_event(event4_new);
assert_eq!(stored_calendar1.events.len(), 3);
let (_, stored_calendar1) = cal_collection.set(&stored_calendar1).expect("can set calendar1 after adding new event");
println!("Added new event '1-on-1' to stored_calendar1. Total events: {}", stored_calendar1.events.len());
let (_, stored_calendar1) = cal_collection
.set(&stored_calendar1)
.expect("can set calendar1 after adding new event");
println!(
"Added new event '1-on-1' to stored_calendar1. Total events: {}",
stored_calendar1.events.len()
);
// --- Delete a Calendar ---
cal_collection.delete_by_id(calendar2.get_id()).expect("can delete calendar2");
let deleted_calendar2_opt = cal_collection.get_by_id(calendar2.get_id()).expect("can try to load deleted calendar2");
assert!(deleted_calendar2_opt.is_none(), "Calendar2 should be deleted from DB");
cal_collection
.delete_by_id(calendar2.get_id())
.expect("can delete calendar2");
let deleted_calendar2_opt = cal_collection
.get_by_id(calendar2.get_id())
.expect("can try to load deleted calendar2");
assert!(
deleted_calendar2_opt.is_none(),
"Calendar2 should be deleted from DB"
);
println!("\nDeleted calendar2 (ID: {}) from DB.", calendar2.get_id());
println!("Calendar model DB Prefix: {}", Calendar::db_prefix());
println!("\nExample finished. DB stored at {}", db_path);
println!("To clean up, you can manually delete the directory: {}", db_path);
println!(
"To clean up, you can manually delete the directory: {}",
db_path
);
}

View File

@ -1,18 +1,17 @@
use heromodels::db::hero::OurDB;
use heromodels::models::calendar::{Attendee, AttendanceStatus, Calendar, Event};
use heromodels::models::calendar::rhai::register_rhai_engine_functions;
use heromodels::models::calendar::{AttendanceStatus, Attendee, Calendar, Event};
use rhai::Engine;
use rhai_wrapper::wrap_vec_return;
use std::sync::Arc;
use std::{fs, path::Path};
use rhai_wrapper::wrap_vec_return;
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Initialize Rhai engine
let mut engine = Engine::new();
// Initialize database with OurDB
let db = Arc::new(OurDB::new("temp_calendar_db", true).expect("Failed to create database"));
let db = Arc::new(OurDB::new("temp_calendar_db", true).expect("Failed to create database"));
// Register the Calendar type with Rhai
// This function is generated by the #[rhai_model_export] attribute
@ -29,9 +28,12 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
});
// Register setter methods for Calendar properties
engine.register_fn("set_description", |calendar: &mut Calendar, desc: String| {
calendar.description = Some(desc);
});
engine.register_fn(
"set_description",
|calendar: &mut Calendar, desc: String| {
calendar.description = Some(desc);
},
);
// Register getter methods for Calendar properties
engine.register_fn("get_description", |calendar: Calendar| -> String {
@ -49,10 +51,13 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("Calendar saved: {}", _calendar.name);
});
engine.register_fn("get_calendar_by_id", |_db: Arc<OurDB>, id: i64| -> Calendar {
// In a real implementation, this would retrieve the calendar from the database
Calendar::new(Some(id as u32), "Retrieved Calendar")
});
engine.register_fn(
"get_calendar_by_id",
|_db: Arc<OurDB>, id: i64| -> Calendar {
// In a real implementation, this would retrieve the calendar from the database
Calendar::new(Some(id as u32), "Retrieved Calendar")
},
);
// Register a function to check if a calendar exists
engine.register_fn("calendar_exists", |_db: Arc<OurDB>, id: i64| -> bool {
@ -63,11 +68,17 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
// Define the function separately to use with the wrap_vec_return macro
fn get_all_calendars(_db: Arc<OurDB>) -> Vec<Calendar> {
// In a real implementation, this would retrieve all calendars from the database
vec![Calendar::new(Some(1), "Calendar 1"), Calendar::new(Some(2), "Calendar 2")]
vec![
Calendar::new(Some(1), "Calendar 1"),
Calendar::new(Some(2), "Calendar 2"),
]
}
// Register the function with the wrap_vec_return macro
engine.register_fn("get_all_calendars", wrap_vec_return!(get_all_calendars, Arc<OurDB> => Calendar));
engine.register_fn(
"get_all_calendars",
wrap_vec_return!(get_all_calendars, Arc<OurDB> => Calendar),
);
engine.register_fn("delete_calendar_by_id", |_db: Arc<OurDB>, _id: i64| {
// In a real implementation, this would delete the calendar from the database
@ -84,4 +95,4 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
}
Ok(())
}
}

View File

@ -41,11 +41,16 @@ fn main() {
println!("Before saving - CustomUser DB Keys: {:?}", user.db_keys());
// Save the model to the database
let collection = db.collection::<CustomUser>().expect("can open user collection");
let collection = db
.collection::<CustomUser>()
.expect("can open user collection");
let (user_id, saved_user) = collection.set(&user).expect("can save user");
println!("\nAfter saving - CustomUser ID: {}", saved_user.get_id());
println!("After saving - CustomUser DB Keys: {:?}", saved_user.db_keys());
println!(
"After saving - CustomUser DB Keys: {:?}",
saved_user.db_keys()
);
println!("Returned ID: {}", user_id);
// Verify that the ID was auto-generated
@ -53,5 +58,8 @@ fn main() {
assert_ne!(saved_user.get_id(), 0);
println!("\nExample finished. DB stored at {}", db_path);
println!("To clean up, you can manually delete the directory: {}", db_path);
println!(
"To clean up, you can manually delete the directory: {}",
db_path
);
}

View File

@ -1,8 +1,10 @@
// heromodels/examples/finance_example/main.rs
use chrono::{Utc, Duration};
use chrono::{Duration, Utc};
use heromodels::models::finance::marketplace::{
Bid, BidStatus, Listing, ListingStatus, ListingType,
};
use heromodels::models::finance::{Account, Asset, AssetType};
use heromodels::models::finance::marketplace::{Listing, ListingType, ListingStatus, Bid, BidStatus};
fn main() {
println!("Finance Models Example\n");
@ -12,16 +14,19 @@ fn main() {
// Create a new account with auto-generated ID
let mut account = Account::new(
None, // id (auto-generated)
"Main ETH Wallet", // name
1001, // user_id
"My primary Ethereum wallet", // description
"ethereum", // ledger
None, // id (auto-generated)
"Main ETH Wallet", // name
1001, // user_id
"My primary Ethereum wallet", // description
"ethereum", // ledger
"0x1234567890abcdef1234567890abcdef12345678", // address
"0xpubkey123456789" // pubkey
"0xpubkey123456789", // pubkey
);
println!("Created Account: '{}' (ID: {})", account.name, account.base_data.id);
println!(
"Created Account: '{}' (ID: {})",
account.name, account.base_data.id
);
println!("Owner: User {}", account.user_id);
println!("Blockchain: {}", account.ledger);
println!("Address: {}", account.address);
@ -30,34 +35,34 @@ fn main() {
// Create some assets
// Asset with auto-generated ID
let eth_asset = Asset::new(
None, // id (auto-generated)
"Ethereum", // name
"Native ETH cryptocurrency", // description
1.5, // amount
None, // id (auto-generated)
"Ethereum", // name
"Native ETH cryptocurrency", // description
1.5, // amount
"0x0000000000000000000000000000000000000000", // address (ETH has no contract address)
AssetType::Native, // asset_type
18, // decimals
AssetType::Native, // asset_type
18, // decimals
);
// Assets with explicit IDs
let usdc_asset = Asset::new(
Some(102), // id
"USDC", // name
"USD Stablecoin on Ethereum", // description
1000.0, // amount
Some(102), // id
"USDC", // name
"USD Stablecoin on Ethereum", // description
1000.0, // amount
"0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", // address (USDC contract)
AssetType::Erc20, // asset_type
6, // decimals
AssetType::Erc20, // asset_type
6, // decimals
);
let nft_asset = Asset::new(
Some(103), // id
"CryptoPunk #1234", // name
"Rare digital collectible", // description
1.0, // amount
Some(103), // id
"CryptoPunk #1234", // name
"Rare digital collectible", // description
1.0, // amount
"0xb47e3cd837ddf8e4c57f05d70ab865de6e193bbb", // address (CryptoPunks contract)
AssetType::Erc721, // asset_type
0, // decimals
AssetType::Erc721, // asset_type
0, // decimals
);
// Add assets to the account
@ -67,7 +72,12 @@ fn main() {
println!("Added Assets to Account:");
for asset in &account.assets {
println!("- {} ({:?}): {} units", asset.name, asset.asset_type, asset.formatted_amount());
println!(
"- {} ({:?}): {} units",
asset.name,
asset.asset_type,
asset.formatted_amount()
);
}
println!("\nTotal Account Value (raw sum): {}", account.total_value());
@ -75,10 +85,10 @@ fn main() {
// Update account details
account = account.update_details(
Some("Primary Ethereum Wallet"), // new name
None::<String>, // keep same description
None::<String>, // keep same address
Some("0xnewpubkey987654321"), // new pubkey
Some("Primary Ethereum Wallet"), // new name
None::<String>, // keep same description
None::<String>, // keep same address
Some("0xnewpubkey987654321"), // new pubkey
);
println!("Updated Account Details:");
@ -99,23 +109,32 @@ fn main() {
// Create a fixed price listing with auto-generated ID
let mut fixed_price_listing = Listing::new(
None, // id (auto-generated)
"1000 USDC for Sale", // title
"Selling 1000 USDC tokens at fixed price", // description
"102", // asset_id (referencing the USDC asset)
AssetType::Erc20, // asset_type
"1001", // seller_id
1.05, // price (in ETH)
"ETH", // currency
ListingType::FixedPrice, // listing_type
None, // id (auto-generated)
"1000 USDC for Sale", // title
"Selling 1000 USDC tokens at fixed price", // description
"102", // asset_id (referencing the USDC asset)
AssetType::Erc20, // asset_type
"1001", // seller_id
1.05, // price (in ETH)
"ETH", // currency
ListingType::FixedPrice, // listing_type
Some(Utc::now() + Duration::days(7)), // expires_at (7 days from now)
vec!["token".to_string(), "stablecoin".to_string()], // tags
Some("https://example.com/usdc.png"), // image_url
);
println!("Created Fixed Price Listing: '{}' (ID: {})", fixed_price_listing.title, fixed_price_listing.base_data.id);
println!("Price: {} {}", fixed_price_listing.price, fixed_price_listing.currency);
println!("Type: {:?}, Status: {:?}", fixed_price_listing.listing_type, fixed_price_listing.status);
println!(
"Created Fixed Price Listing: '{}' (ID: {})",
fixed_price_listing.title, fixed_price_listing.base_data.id
);
println!(
"Price: {} {}",
fixed_price_listing.price, fixed_price_listing.currency
);
println!(
"Type: {:?}, Status: {:?}",
fixed_price_listing.listing_type, fixed_price_listing.status
);
println!("Expires: {}", fixed_price_listing.expires_at.unwrap());
println!("");
@ -126,54 +145,71 @@ fn main() {
println!("Fixed Price Sale Completed:");
println!("Status: {:?}", fixed_price_listing.status);
println!("Buyer: {}", fixed_price_listing.buyer_id.unwrap());
println!("Sale Price: {} {}", fixed_price_listing.sale_price.unwrap(), fixed_price_listing.currency);
println!(
"Sale Price: {} {}",
fixed_price_listing.sale_price.unwrap(),
fixed_price_listing.currency
);
println!("Sold At: {}", fixed_price_listing.sold_at.unwrap());
println!("");
},
}
Err(e) => println!("Error completing sale: {}", e),
}
// Create an auction listing for the NFT with explicit ID
let mut auction_listing = Listing::new(
Some(202), // id (explicit)
"CryptoPunk #1234 Auction", // title
"Rare CryptoPunk NFT for auction", // description
"103", // asset_id (referencing the NFT asset)
AssetType::Erc721, // asset_type
"1001", // seller_id
10.0, // starting_price (in ETH)
"ETH", // currency
ListingType::Auction, // listing_type
Some(202), // id (explicit)
"CryptoPunk #1234 Auction", // title
"Rare CryptoPunk NFT for auction", // description
"103", // asset_id (referencing the NFT asset)
AssetType::Erc721, // asset_type
"1001", // seller_id
10.0, // starting_price (in ETH)
"ETH", // currency
ListingType::Auction, // listing_type
Some(Utc::now() + Duration::days(3)), // expires_at (3 days from now)
vec!["nft".to_string(), "collectible".to_string(), "cryptopunk".to_string()], // tags
vec![
"nft".to_string(),
"collectible".to_string(),
"cryptopunk".to_string(),
], // tags
Some("https://example.com/cryptopunk1234.png"), // image_url
);
println!("Created Auction Listing: '{}' (ID: {})", auction_listing.title, auction_listing.base_data.id);
println!("Starting Price: {} {}", auction_listing.price, auction_listing.currency);
println!("Type: {:?}, Status: {:?}", auction_listing.listing_type, auction_listing.status);
println!(
"Created Auction Listing: '{}' (ID: {})",
auction_listing.title, auction_listing.base_data.id
);
println!(
"Starting Price: {} {}",
auction_listing.price, auction_listing.currency
);
println!(
"Type: {:?}, Status: {:?}",
auction_listing.listing_type, auction_listing.status
);
println!("");
// Create some bids
let bid1 = Bid::new(
auction_listing.base_data.id.to_string(), // listing_id
2001, // bidder_id
11.0, // amount
"ETH", // currency
2001, // bidder_id
11.0, // amount
"ETH", // currency
);
let bid2 = Bid::new(
auction_listing.base_data.id.to_string(), // listing_id
2002, // bidder_id
12.5, // amount
"ETH", // currency
2002, // bidder_id
12.5, // amount
"ETH", // currency
);
let bid3 = Bid::new(
auction_listing.base_data.id.to_string(), // listing_id
2003, // bidder_id
15.0, // amount
"ETH", // currency
2003, // bidder_id
15.0, // amount
"ETH", // currency
);
// Add bids to the auction
@ -184,7 +220,7 @@ fn main() {
Ok(updated_listing) => {
auction_listing = updated_listing;
println!("- Bid added: 11.0 ETH from User 2001");
},
}
Err(e) => println!("Error adding bid: {}", e),
}
@ -192,7 +228,7 @@ fn main() {
Ok(updated_listing) => {
auction_listing = updated_listing;
println!("- Bid added: 12.5 ETH from User 2002");
},
}
Err(e) => println!("Error adding bid: {}", e),
}
@ -200,18 +236,21 @@ fn main() {
Ok(updated_listing) => {
auction_listing = updated_listing;
println!("- Bid added: 15.0 ETH from User 2003");
},
}
Err(e) => println!("Error adding bid: {}", e),
}
println!("\nCurrent Auction Status:");
println!("Current Price: {} {}", auction_listing.price, auction_listing.currency);
println!(
"Current Price: {} {}",
auction_listing.price, auction_listing.currency
);
if let Some(highest_bid) = auction_listing.highest_bid() {
println!("Highest Bid: {} {} from User {}",
highest_bid.amount,
highest_bid.currency,
highest_bid.bidder_id);
println!(
"Highest Bid: {} {} from User {}",
highest_bid.amount, highest_bid.currency, highest_bid.bidder_id
);
}
println!("Total Bids: {}", auction_listing.bids.len());
@ -223,42 +262,57 @@ fn main() {
auction_listing = updated_listing;
println!("Auction Completed:");
println!("Status: {:?}", auction_listing.status);
println!("Winner: User {}", auction_listing.buyer_id.as_ref().unwrap());
println!("Winning Bid: {} {}", auction_listing.sale_price.as_ref().unwrap(), auction_listing.currency);
println!(
"Winner: User {}",
auction_listing.buyer_id.as_ref().unwrap()
);
println!(
"Winning Bid: {} {}",
auction_listing.sale_price.as_ref().unwrap(),
auction_listing.currency
);
println!("");
println!("Final Bid Statuses:");
for bid in &auction_listing.bids {
println!("- User {}: {} {} (Status: {:?})",
bid.bidder_id,
bid.amount,
bid.currency,
bid.status);
println!(
"- User {}: {} {} (Status: {:?})",
bid.bidder_id, bid.amount, bid.currency, bid.status
);
}
println!("");
},
}
Err(e) => println!("Error completing auction: {}", e),
}
// Create an exchange listing with auto-generated ID
let exchange_listing = Listing::new(
None, // id (auto-generated)
"ETH for BTC Exchange", // title
"Looking to exchange ETH for BTC", // description
"101", // asset_id (referencing the ETH asset)
AssetType::Native, // asset_type
"1001", // seller_id
1.0, // amount (1 ETH)
"BTC", // currency (what they want in exchange)
ListingType::Exchange, // listing_type
Some(Utc::now() + Duration::days(14)), // expires_at (14 days from now)
None, // id (auto-generated)
"ETH for BTC Exchange", // title
"Looking to exchange ETH for BTC", // description
"101", // asset_id (referencing the ETH asset)
AssetType::Native, // asset_type
"1001", // seller_id
1.0, // amount (1 ETH)
"BTC", // currency (what they want in exchange)
ListingType::Exchange, // listing_type
Some(Utc::now() + Duration::days(14)), // expires_at (14 days from now)
vec!["exchange".to_string(), "crypto".to_string()], // tags
None::<String>, // image_url
None::<String>, // image_url
);
println!("Created Exchange Listing: '{}' (ID: {})", exchange_listing.title, exchange_listing.base_data.id);
println!("Offering: Asset {} ({:?})", exchange_listing.asset_id, exchange_listing.asset_type);
println!("Wanted: {} {}", exchange_listing.price, exchange_listing.currency);
println!(
"Created Exchange Listing: '{}' (ID: {})",
exchange_listing.title, exchange_listing.base_data.id
);
println!(
"Offering: Asset {} ({:?})",
exchange_listing.asset_id, exchange_listing.asset_type
);
println!(
"Wanted: {} {}",
exchange_listing.price, exchange_listing.currency
);
println!("");
// --- PART 3: DEMONSTRATING EDGE CASES ---
@ -266,26 +320,26 @@ fn main() {
// Create a new auction listing for edge case testing with explicit ID
let test_auction = Listing::new(
Some(205), // id (explicit)
"Test Auction", // title
"For testing edge cases", // description
"101", // asset_id
AssetType::Native, // asset_type
"1001", // seller_id
10.0, // starting_price
"ETH", // currency
ListingType::Auction, // listing_type
Some(205), // id (explicit)
"Test Auction", // title
"For testing edge cases", // description
"101", // asset_id
AssetType::Native, // asset_type
"1001", // seller_id
10.0, // starting_price
"ETH", // currency
ListingType::Auction, // listing_type
Some(Utc::now() + Duration::days(1)), // expires_at
vec![], // tags
None::<String>, // image_url
vec![], // tags
None::<String>, // image_url
);
// Try to add a bid that's too low
let low_bid = Bid::new(
test_auction.base_data.id.to_string(), // listing_id
2004, // bidder_id
5.0, // amount (lower than starting price)
"ETH", // currency
2004, // bidder_id
5.0, // amount (lower than starting price)
"ETH", // currency
);
println!("Attempting to add a bid that's too low (5.0 ETH):");
@ -305,21 +359,24 @@ fn main() {
// Create a listing that will expire with auto-generated ID
let mut expiring_listing = Listing::new(
None, // id (auto-generated)
"About to Expire", // title
None, // id (auto-generated)
"About to Expire", // title
"This listing will expire immediately", // description
"101", // asset_id
AssetType::Native, // asset_type
"1001", // seller_id
0.1, // price
"ETH", // currency
ListingType::FixedPrice, // listing_type
Some(Utc::now() - Duration::hours(1)), // expires_at (1 hour ago)
vec![], // tags
None::<String>, // image_url
"101", // asset_id
AssetType::Native, // asset_type
"1001", // seller_id
0.1, // price
"ETH", // currency
ListingType::FixedPrice, // listing_type
Some(Utc::now() - Duration::hours(1)), // expires_at (1 hour ago)
vec![], // tags
None::<String>, // image_url
);
println!("Created Expiring Listing: '{}' (ID: {})", expiring_listing.title, expiring_listing.base_data.id);
println!(
"Created Expiring Listing: '{}' (ID: {})",
expiring_listing.title, expiring_listing.base_data.id
);
println!("Initial Status: {:?}", expiring_listing.status);
// Check expiration

View File

@ -1,12 +1,12 @@
use rhai::{Engine, Scope, EvalAltResult};
use std::sync::{Arc, Mutex};
use rhai::{Engine, EvalAltResult, Scope};
use std::collections::HashMap;
use std::fs;
use std::sync::{Arc, Mutex};
// Import the models and the registration function
use heromodels::models::finance::account::Account;
use heromodels::models::finance::asset::{Asset};
use heromodels::models::finance::marketplace::{Listing};
use heromodels::models::finance::asset::Asset;
use heromodels::models::finance::marketplace::Listing;
use heromodels::models::finance::rhai::register_rhai_engine_functions;
// Define a simple in-memory mock database for the example
@ -39,10 +39,10 @@ fn main() -> Result<(), Box<EvalAltResult>> {
// Register finance functions and types with the engine
register_rhai_engine_functions(
&mut engine,
&mut engine,
Arc::clone(&mock_db.accounts),
Arc::clone(&mock_db.assets),
Arc::clone(&mock_db.listings)
Arc::clone(&mock_db.listings),
);
println!("Rhai functions registered.");
@ -77,8 +77,13 @@ fn main() -> Result<(), Box<EvalAltResult>> {
println!("No accounts in mock DB.");
}
for (id, account) in final_accounts.iter() {
println!("Account ID: {}, Name: '{}', User ID: {}, Assets: {}",
id, account.name, account.user_id, account.assets.len());
println!(
"Account ID: {}, Name: '{}', User ID: {}, Assets: {}",
id,
account.name,
account.user_id,
account.assets.len()
);
}
// Print final state of Assets
@ -88,8 +93,10 @@ fn main() -> Result<(), Box<EvalAltResult>> {
println!("No assets in mock DB.");
}
for (id, asset) in final_assets.iter() {
println!("Asset ID: {}, Name: '{}', Amount: {}, Type: {:?}",
id, asset.name, asset.amount, asset.asset_type);
println!(
"Asset ID: {}, Name: '{}', Amount: {}, Type: {:?}",
id, asset.name, asset.amount, asset.asset_type
);
}
// Print final state of Listings
@ -100,8 +107,13 @@ fn main() -> Result<(), Box<EvalAltResult>> {
}
for (id, listing) in final_listings.iter() {
println!(
"Listing ID: {}, Title: '{}', Type: {:?}, Status: {:?}, Price: {}, Bids: {}",
id, listing.title, listing.listing_type, listing.status, listing.price, listing.bids.len()
"Listing ID: {}, Title: '{}', Type: {:?}, Status: {:?}, Price: {}, Bids: {}",
id,
listing.title,
listing.listing_type,
listing.status,
listing.price,
listing.bids.len()
);
}

View File

@ -9,8 +9,8 @@ use heromodels_core::Model;
fn main() {
// Create a new DB instance in /tmp/ourdb_flowbroker, and reset before every run
let db = heromodels::db::hero::OurDB::new("/tmp/ourdb_flowbroker", true)
.expect("Can create DB");
let db =
heromodels::db::hero::OurDB::new("/tmp/ourdb_flowbroker", true).expect("Can create DB");
println!("Hero Models - Flow Example");
println!("===========================");
@ -20,56 +20,71 @@ fn main() {
let new_flow_uuid = "a1b2c3d4-e5f6-7890-1234-567890abcdef"; // Example UUID
let flow1 = Flow::new(
1, // id
new_flow_uuid, // flow_uuid
"Document Approval Flow", // name
"Pending", // status
1, // id
new_flow_uuid, // flow_uuid
"Document Approval Flow", // name
"Pending", // status
);
db.collection().expect("can open flow collection").set(&flow1).expect("can set flow1");
db.collection()
.expect("can open flow collection")
.set(&flow1)
.expect("can set flow1");
println!("Created Flow: {:?}", flow1);
println!("Flow ID: {}", flow1.get_id());
println!("Flow DB Prefix: {}", Flow::db_prefix());
// --- Create FlowSteps for Flow1 ---
let step1_flow1 = FlowStep::new(
101, // id
flow1.get_id(), // flow_id
1, // step_order
"Pending", // status
101, // id
flow1.get_id(), // flow_id
1, // step_order
"Pending", // status
)
.description("Initial review by manager");
db.collection().expect("can open flow_step collection").set(&step1_flow1).expect("can set step1_flow1");
db.collection()
.expect("can open flow_step collection")
.set(&step1_flow1)
.expect("can set step1_flow1");
println!("Created FlowStep: {:?}", step1_flow1);
let step2_flow1 = FlowStep::new(
102, // id
flow1.get_id(), // flow_id
2, // step_order
"Pending", // status
102, // id
flow1.get_id(), // flow_id
2, // step_order
"Pending", // status
)
.description("Legal team sign-off");
db.collection().expect("can open flow_step collection").set(&step2_flow1).expect("can set step2_flow1");
db.collection()
.expect("can open flow_step collection")
.set(&step2_flow1)
.expect("can set step2_flow1");
println!("Created FlowStep: {:?}", step2_flow1);
// --- Create SignatureRequirements for step2_flow1 ---
let sig_req1_step2 = SignatureRequirement::new(
201, // id
step2_flow1.get_id(), // flow_step_id
"pubkey_legal_team_lead_hex", // public_key
201, // id
step2_flow1.get_id(), // flow_step_id
"pubkey_legal_team_lead_hex", // public_key
"I approve this document for legal compliance.", // message
"Pending", // status
"Pending", // status
);
db.collection().expect("can open sig_req collection").set(&sig_req1_step2).expect("can set sig_req1_step2");
db.collection()
.expect("can open sig_req collection")
.set(&sig_req1_step2)
.expect("can set sig_req1_step2");
println!("Created SignatureRequirement: {:?}", sig_req1_step2);
let sig_req2_step2 = SignatureRequirement::new(
202, // id
step2_flow1.get_id(), // flow_step_id
"pubkey_general_counsel_hex", // public_key
202, // id
step2_flow1.get_id(), // flow_step_id
"pubkey_general_counsel_hex", // public_key
"I, as General Counsel, approve this document.", // message
"Pending", // status
"Pending", // status
);
db.collection().expect("can open sig_req collection").set(&sig_req2_step2).expect("can set sig_req2_step2");
db.collection()
.expect("can open sig_req collection")
.set(&sig_req2_step2)
.expect("can set sig_req2_step2");
println!("Created SignatureRequirement: {:?}", sig_req2_step2);
// --- Retrieve and Verify ---
@ -101,9 +116,18 @@ fn main() {
.get::<flow_step_flow_id_idx, _>(&retrieved_flow.get_id())
.expect("can load steps for flow1");
assert_eq!(steps_for_flow1.len(), 2);
println!("Retrieved {} FlowSteps for Flow ID {}:", steps_for_flow1.len(), retrieved_flow.get_id());
println!(
"Retrieved {} FlowSteps for Flow ID {}:",
steps_for_flow1.len(),
retrieved_flow.get_id()
);
for step in &steps_for_flow1 {
println!(" - Step ID: {}, Order: {}, Desc: {:?}", step.get_id(), step.step_order, step.description);
println!(
" - Step ID: {}, Order: {}, Desc: {:?}",
step.get_id(),
step.step_order,
step.description
);
}
// --- Update a SignatureRequirement (simulate signing) ---
@ -114,12 +138,18 @@ fn main() {
.expect("can load sig_req1")
.unwrap();
println!("\nUpdating SignatureRequirement ID: {}", retrieved_sig_req1.get_id());
println!(
"\nUpdating SignatureRequirement ID: {}",
retrieved_sig_req1.get_id()
);
retrieved_sig_req1.status = "Signed".to_string();
retrieved_sig_req1.signed_by = Some("pubkey_legal_team_lead_hex_actual_signer".to_string());
retrieved_sig_req1.signature = Some("mock_signature_base64_encoded".to_string());
db.collection().expect("can open sig_req collection").set(&retrieved_sig_req1).expect("can update sig_req1");
db.collection()
.expect("can open sig_req collection")
.set(&retrieved_sig_req1)
.expect("can update sig_req1");
let updated_sig_req1 = db
.collection::<SignatureRequirement>()
@ -129,10 +159,13 @@ fn main() {
.unwrap();
assert_eq!(updated_sig_req1.status, "Signed");
assert_eq!(updated_sig_req1.signature.as_deref(), Some("mock_signature_base64_encoded"));
assert_eq!(
updated_sig_req1.signature.as_deref(),
Some("mock_signature_base64_encoded")
);
println!("Updated SignatureRequirement: {:?}", updated_sig_req1);
// --- Delete a FlowStep ---
// --- Delete a FlowStep ---
// (In a real app, you might also want to delete associated SignatureRequirements first, or handle via DB constraints/cascade if supported)
let step1_id_to_delete = step1_flow1.get_id();
db.collection::<FlowStep>()
@ -157,7 +190,11 @@ fn main() {
.expect("can load remaining steps for flow1");
assert_eq!(remaining_steps_for_flow1.len(), 1);
assert_eq!(remaining_steps_for_flow1[0].get_id(), step2_flow1.get_id());
println!("Remaining FlowSteps for Flow ID {}: count = {}", retrieved_flow.get_id(), remaining_steps_for_flow1.len());
println!(
"Remaining FlowSteps for Flow ID {}: count = {}",
retrieved_flow.get_id(),
remaining_steps_for_flow1.len()
);
println!("\nFlow example finished successfully!");
}

View File

@ -20,13 +20,18 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
let script_path = Path::new(script_path_str);
if !script_path.exists() {
eprintln!("Error: Rhai script not found at {}", script_path_str);
eprintln!("Please ensure the script 'flow.rhai' exists in the 'examples/flow_rhai/' directory.");
return Err(Box::new(std::io::Error::new(std::io::ErrorKind::NotFound, format!("Rhai script not found: {}", script_path_str))));
eprintln!(
"Please ensure the script 'flow.rhai' exists in the 'examples/flow_rhai/' directory."
);
return Err(Box::new(std::io::Error::new(
std::io::ErrorKind::NotFound,
format!("Rhai script not found: {}", script_path_str),
)));
}
println!("Executing Rhai script: {}", script_path_str);
let script = fs::read_to_string(script_path)?;
match engine.eval::<()>(&script) {
Ok(_) => println!("\nRhai script executed successfully!"),
Err(e) => eprintln!("\nRhai script execution failed: {}\nDetails: {:#?}", e, e),

View File

@ -101,23 +101,23 @@ fn main() {
// Example of voting with comments using the cast_vote_with_comment method
println!("Adding votes with comments...");
// User 7 votes for 'Approve Allocation' with a comment
proposal = proposal.cast_vote_with_comment(
Some(110), // ballot_id
7, // user_id
1, // chosen_option_id (Approve Allocation)
80, // shares
"I strongly support this proposal because it aligns with our community values."
7, // user_id
1, // chosen_option_id (Approve Allocation)
80, // shares
"I strongly support this proposal because it aligns with our community values.",
);
// User 8 votes for 'Reject Allocation' with a comment
proposal = proposal.cast_vote_with_comment(
Some(111), // ballot_id
8, // user_id
2, // chosen_option_id (Reject Allocation)
60, // shares
"I have concerns about the allocation priorities."
8, // user_id
2, // chosen_option_id (Reject Allocation)
60, // shares
"I have concerns about the allocation priorities.",
);
println!("\nBallots with Comments:");
@ -218,34 +218,34 @@ fn main() {
// Example of voting with comments on a private proposal
println!("\nAdding votes with comments to private proposal...");
// User 20 (eligible) votes with a comment
private_proposal = private_proposal.cast_vote_with_comment(
Some(202), // ballot_id
20, // user_id (eligible)
1, // chosen_option_id
75, // shares
"I support this restructuring plan with some reservations."
Some(202), // ballot_id
20, // user_id (eligible)
1, // chosen_option_id
75, // shares
"I support this restructuring plan with some reservations.",
);
// User 30 (eligible) votes with a comment
private_proposal = private_proposal.cast_vote_with_comment(
Some(203), // ballot_id
30, // user_id (eligible)
2, // chosen_option_id
90, // shares
"I believe we should reconsider the timing of these changes."
Some(203), // ballot_id
30, // user_id (eligible)
2, // chosen_option_id
90, // shares
"I believe we should reconsider the timing of these changes.",
);
// User 40 (ineligible) tries to vote with a comment
private_proposal = private_proposal.cast_vote_with_comment(
Some(204), // ballot_id
40, // user_id (ineligible)
1, // chosen_option_id
50, // shares
"This restructuring seems unnecessary."
Some(204), // ballot_id
40, // user_id (ineligible)
1, // chosen_option_id
50, // shares
"This restructuring seems unnecessary.",
);
println!("Eligible users 20 and 30 added votes with comments.");
println!("Ineligible user 40 attempted to vote with a comment (should be rejected).");

View File

@ -1,10 +1,12 @@
use chrono::{Duration, Utc};
use heromodels::db::hero::OurDB;
use heromodels::models::governance::{Proposal, ProposalStatus, VoteEventStatus, VoteOption, Ballot};
use heromodels::models::governance::{
Ballot, Proposal, ProposalStatus, VoteEventStatus, VoteOption,
};
use rhai::Engine;
use rhai_client_macros::rhai;
use rhai_wrapper::wrap_vec_return;
use std::sync::Arc;
use chrono::{Utc, Duration};
use rhai_client_macros::rhai;
// Define the functions we want to expose to Rhai
// We'll only use the #[rhai] attribute on functions with simple types
@ -13,7 +15,14 @@ use rhai_client_macros::rhai;
fn create_proposal(id: i64, creator_id: String, title: String, description: String) -> Proposal {
let start_date = Utc::now();
let end_date = start_date + Duration::days(14);
Proposal::new(id as u32, creator_id, title, description, start_date, end_date)
Proposal::new(
id as u32,
creator_id,
title,
description,
start_date,
end_date,
)
}
// Getter functions for Proposal properties
@ -46,7 +55,13 @@ fn add_option_to_proposal(proposal: Proposal, option_id: i64, option_text: Strin
proposal.add_option(option_id as u8, option_text)
}
fn cast_vote_on_proposal(proposal: Proposal, ballot_id: i64, user_id: i64, option_id: i64, shares: i64) -> Proposal {
fn cast_vote_on_proposal(
proposal: Proposal,
ballot_id: i64,
user_id: i64,
option_id: i64,
shares: i64,
) -> Proposal {
proposal.cast_vote(ballot_id as u32, user_id as u32, option_id as u8, shares)
}
@ -119,14 +134,24 @@ fn get_ballot_shares(ballot: &Ballot) -> i64 {
// Simple functions that we can use with the #[rhai] attribute
#[rhai]
fn create_proposal_wrapper(id: i64, creator_id: String, title: String, description: String) -> String {
fn create_proposal_wrapper(
id: i64,
creator_id: String,
title: String,
description: String,
) -> String {
let proposal = create_proposal(id, creator_id, title, description);
format!("Created proposal with ID: {}", proposal.base_data.id)
}
#[rhai]
fn add_option_wrapper(id: i64, option_id: i64, option_text: String) -> String {
let proposal = create_proposal(id, "user".to_string(), "title".to_string(), "description".to_string());
let proposal = create_proposal(
id,
"user".to_string(),
"title".to_string(),
"description".to_string(),
);
let updated = add_option_to_proposal(proposal, option_id, option_text.clone());
format!("Added option '{}' to proposal {}", option_text, id)
}
@ -141,8 +166,22 @@ fn get_all_proposals(_db: Arc<OurDB>) -> Vec<Proposal> {
let start_date = Utc::now();
let end_date = start_date + Duration::days(14);
vec![
Proposal::new(1, "Creator 1", "Proposal 1", "Description 1", start_date, end_date),
Proposal::new(2, "Creator 2", "Proposal 2", "Description 2", start_date, end_date)
Proposal::new(
1,
"Creator 1",
"Proposal 1",
"Description 1",
start_date,
end_date,
),
Proposal::new(
2,
"Creator 2",
"Proposal 2",
"Description 2",
start_date,
end_date,
),
]
}
@ -161,31 +200,49 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
// Register the Proposal type with Rhai
// This function is generated by the #[rhai_model_export] attribute
Proposal::register_rhai_bindings_for_proposal(&mut engine, db.clone());
// Register the Ballot type with Rhai
Ballot::register_rhai_bindings_for_ballot(&mut engine, db.clone());
// Create a clone of db for use in the get_db function
let db_for_get_db = db.clone();
// Register a function to get the database instance
engine.register_fn("get_db", move || db_for_get_db.clone());
// Register builder functions for Proposal and related types
engine.register_fn("create_proposal", |id: i64, creator_id: String, title: String, description: String| {
let start_date = Utc::now();
let end_date = start_date + Duration::days(14);
Proposal::new(id as u32, creator_id, title, description, start_date, end_date)
});
engine.register_fn(
"create_proposal",
|id: i64, creator_id: String, title: String, description: String| {
let start_date = Utc::now();
let end_date = start_date + Duration::days(14);
Proposal::new(
id as u32,
creator_id,
title,
description,
start_date,
end_date,
)
},
);
engine.register_fn("create_vote_option", |id: i64, text: String| {
VoteOption::new(id as u8, text)
});
engine.register_fn("create_ballot", |id: i64, user_id: i64, vote_option_id: i64, shares_count: i64| {
Ballot::new(id as u32, user_id as u32, vote_option_id as u8, shares_count)
});
engine.register_fn(
"create_ballot",
|id: i64, user_id: i64, vote_option_id: i64, shares_count: i64| {
Ballot::new(
id as u32,
user_id as u32,
vote_option_id as u8,
shares_count,
)
},
);
// Register getter and setter methods for Proposal properties
engine.register_fn("get_title", get_title);
engine.register_fn("get_description", get_description);
@ -193,34 +250,47 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
engine.register_fn("get_id", get_id);
engine.register_fn("get_status", get_status);
engine.register_fn("get_vote_status", get_vote_status);
// Register methods for proposal operations
engine.register_fn("add_option_to_proposal", add_option_to_proposal);
engine.register_fn("cast_vote_on_proposal", cast_vote_on_proposal);
engine.register_fn("change_proposal_status", change_proposal_status);
engine.register_fn("change_vote_event_status", change_vote_event_status);
// Register functions for database operations
engine.register_fn("save_proposal", save_proposal);
engine.register_fn("get_proposal_by_id", |_db: Arc<OurDB>, id: i64| -> Proposal {
// In a real implementation, this would retrieve the proposal from the database
let start_date = Utc::now();
let end_date = start_date + Duration::days(14);
Proposal::new(id as u32, "Retrieved Creator", "Retrieved Proposal", "Retrieved Description", start_date, end_date)
});
engine.register_fn(
"get_proposal_by_id",
|_db: Arc<OurDB>, id: i64| -> Proposal {
// In a real implementation, this would retrieve the proposal from the database
let start_date = Utc::now();
let end_date = start_date + Duration::days(14);
Proposal::new(
id as u32,
"Retrieved Creator",
"Retrieved Proposal",
"Retrieved Description",
start_date,
end_date,
)
},
);
// Register a function to check if a proposal exists
engine.register_fn("proposal_exists", |_db: Arc<OurDB>, id: i64| -> bool {
// In a real implementation, this would check if the proposal exists in the database
id == 1 || id == 2
});
// Register the function with the wrap_vec_return macro
engine.register_fn("get_all_proposals", wrap_vec_return!(get_all_proposals, Arc<OurDB> => Proposal));
engine.register_fn(
"get_all_proposals",
wrap_vec_return!(get_all_proposals, Arc<OurDB> => Proposal),
);
engine.register_fn("delete_proposal_by_id", delete_proposal_by_id);
// Register helper functions for accessing proposal options and ballots
engine.register_fn("get_option_count", get_option_count);
engine.register_fn("get_option_at", get_option_at);
@ -231,96 +301,108 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
engine.register_fn("get_ballot_user_id", get_ballot_user_id);
engine.register_fn("get_ballot_option_id", get_ballot_option_id);
engine.register_fn("get_ballot_shares", get_ballot_shares);
// Register our wrapper functions that use the #[rhai] attribute
engine.register_fn("create_proposal_wrapper", create_proposal_wrapper);
engine.register_fn("add_option_wrapper", add_option_wrapper);
// Now instead of loading and evaluating a Rhai script, we'll use direct function calls
// to implement the same functionality
// Use the database instance
// Create a new proposal
let proposal = create_proposal(1,
"user_creator_123".to_string(),
"Community Fund Allocation for Q3".to_string(),
"Proposal to allocate funds for community projects in the third quarter.".to_string());
println!("Created Proposal: '{}' (ID: {})",
get_title(&proposal),
get_id(&proposal));
println!("Status: {}, Vote Status: {}",
get_status(&proposal),
get_vote_status(&proposal));
let proposal = create_proposal(
1,
"user_creator_123".to_string(),
"Community Fund Allocation for Q3".to_string(),
"Proposal to allocate funds for community projects in the third quarter.".to_string(),
);
println!(
"Created Proposal: '{}' (ID: {})",
get_title(&proposal),
get_id(&proposal)
);
println!(
"Status: {}, Vote Status: {}",
get_status(&proposal),
get_vote_status(&proposal)
);
// Add vote options
let mut proposal_with_options = add_option_to_proposal(
proposal, 1, "Approve Allocation".to_string());
proposal_with_options = add_option_to_proposal(
proposal_with_options, 2, "Reject Allocation".to_string());
proposal_with_options = add_option_to_proposal(
proposal_with_options, 3, "Abstain".to_string());
let mut proposal_with_options =
add_option_to_proposal(proposal, 1, "Approve Allocation".to_string());
proposal_with_options =
add_option_to_proposal(proposal_with_options, 2, "Reject Allocation".to_string());
proposal_with_options = add_option_to_proposal(proposal_with_options, 3, "Abstain".to_string());
println!("\nAdded Vote Options:");
let option_count = get_option_count(&proposal_with_options);
for i in 0..option_count {
let option = get_option_at(&proposal_with_options, i);
println!("- Option ID: {}, Text: '{}', Votes: {}",
i, get_option_text(&option),
get_option_votes(&option));
println!(
"- Option ID: {}, Text: '{}', Votes: {}",
i,
get_option_text(&option),
get_option_votes(&option)
);
}
// Save the proposal to the database
save_proposal(db.clone(), proposal_with_options.clone());
println!("\nProposal saved to database");
// Simulate casting votes
println!("\nSimulating Votes...");
// User 1 votes for 'Approve Allocation' with 100 shares
let mut proposal_with_votes = cast_vote_on_proposal(
proposal_with_options, 101, 1, 1, 100);
let mut proposal_with_votes = cast_vote_on_proposal(proposal_with_options, 101, 1, 1, 100);
// User 2 votes for 'Reject Allocation' with 50 shares
proposal_with_votes = cast_vote_on_proposal(
proposal_with_votes, 102, 2, 2, 50);
proposal_with_votes = cast_vote_on_proposal(proposal_with_votes, 102, 2, 2, 50);
// User 3 votes for 'Approve Allocation' with 75 shares
proposal_with_votes = cast_vote_on_proposal(
proposal_with_votes, 103, 3, 1, 75);
proposal_with_votes = cast_vote_on_proposal(proposal_with_votes, 103, 3, 1, 75);
// User 4 abstains with 20 shares
proposal_with_votes = cast_vote_on_proposal(
proposal_with_votes, 104, 4, 3, 20);
proposal_with_votes = cast_vote_on_proposal(proposal_with_votes, 104, 4, 3, 20);
println!("\nVote Counts After Simulation:");
let option_count = get_option_count(&proposal_with_votes);
for i in 0..option_count {
let option = get_option_at(&proposal_with_votes, i);
println!("- Option ID: {}, Text: '{}', Votes: {}",
i, get_option_text(&option),
get_option_votes(&option));
println!(
"- Option ID: {}, Text: '{}', Votes: {}",
i,
get_option_text(&option),
get_option_votes(&option)
);
}
println!("\nBallots Cast:");
let ballot_count = get_ballot_count(&proposal_with_votes);
for i in 0..ballot_count {
let ballot = get_ballot_at(&proposal_with_votes, i);
println!("- Ballot ID: {}, User ID: {}, Option ID: {}, Shares: {}",
i, get_ballot_user_id(&ballot),
get_ballot_option_id(&ballot),
get_ballot_shares(&ballot));
println!(
"- Ballot ID: {}, User ID: {}, Option ID: {}, Shares: {}",
i,
get_ballot_user_id(&ballot),
get_ballot_option_id(&ballot),
get_ballot_shares(&ballot)
);
}
// Change proposal status
let active_proposal = change_proposal_status(
proposal_with_votes, "Active".to_string());
println!("\nChanged Proposal Status to: {}",
get_status(&active_proposal));
let active_proposal = change_proposal_status(proposal_with_votes, "Active".to_string());
println!(
"\nChanged Proposal Status to: {}",
get_status(&active_proposal)
);
// Simulate closing the vote
let closed_proposal = change_vote_event_status(
active_proposal, "Closed".to_string());
println!("Changed Vote Event Status to: {}",
get_vote_status(&closed_proposal));
let closed_proposal = change_vote_event_status(active_proposal, "Closed".to_string());
println!(
"Changed Vote Event Status to: {}",
get_vote_status(&closed_proposal)
);
// Final proposal state
println!("\nFinal Proposal State:");
println!("Title: '{}'", get_title(&closed_proposal));
@ -330,37 +412,46 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
let option_count = get_option_count(&closed_proposal);
for i in 0..option_count {
let option = get_option_at(&closed_proposal, i);
println!(" - {}: {} (Votes: {})",
i, get_option_text(&option),
get_option_votes(&option));
println!(
" - {}: {} (Votes: {})",
i,
get_option_text(&option),
get_option_votes(&option)
);
}
println!("Total Ballots: {}", get_ballot_count(&closed_proposal));
// Get all proposals from the database
let all_proposals = get_all_proposals(db.clone());
println!("\nTotal Proposals in Database: {}", all_proposals.len());
for proposal in all_proposals {
println!("Proposal ID: {}, Title: '{}'",
get_id(&proposal),
get_title(&proposal));
println!(
"Proposal ID: {}, Title: '{}'",
get_id(&proposal),
get_title(&proposal)
);
}
// Delete a proposal
delete_proposal_by_id(db.clone(), 1);
println!("\nDeleted proposal with ID 1");
// Demonstrate the use of Rhai client functions for simple types
println!("\nUsing Rhai client functions for simple types:");
let create_result = create_proposal_wrapper_rhai_client(&engine, 2,
"rhai_user".to_string(),
"Rhai Proposal".to_string(),
"This proposal was created using a Rhai client function".to_string());
let create_result = create_proposal_wrapper_rhai_client(
&engine,
2,
"rhai_user".to_string(),
"Rhai Proposal".to_string(),
"This proposal was created using a Rhai client function".to_string(),
);
println!("{}", create_result);
let add_option_result = add_option_wrapper_rhai_client(&engine, 2, 4, "Rhai Option".to_string());
let add_option_result =
add_option_wrapper_rhai_client(&engine, 2, 4, "Rhai Option".to_string());
println!("{}", add_option_result);
println!("\nGovernance Proposal Example Finished.");
Ok(())
}

View File

@ -70,7 +70,7 @@ fn main() {
.add_signer(signer2.clone())
.add_revision(revision1.clone())
.add_revision(revision2.clone());
// The `#[model]` derive handles `created_at` and `updated_at` in `base_data`.
// `base_data.touch()` might be called internally by setters or needs explicit call if fields are set directly.
// For builder pattern, the final state of `base_data.updated_at` reflects the time of the last builder call if `touch()` is implicit.
@ -87,7 +87,7 @@ fn main() {
println!("\n--- Contract Details After Signing ---");
println!("{:#?}", contract);
println!("\n--- Accessing Specific Fields ---");
println!("Contract Title: {}", contract.title);
println!("Contract Status: {:?}", contract.status);
@ -97,7 +97,10 @@ fn main() {
println!("Updated At (timestamp): {}", contract.base_data.modified_at); // From BaseModelData
if let Some(first_signer_details) = contract.signers.first() {
println!("\nFirst Signer: {} ({})", first_signer_details.name, first_signer_details.email);
println!(
"\nFirst Signer: {} ({})",
first_signer_details.name, first_signer_details.email
);
println!(" Status: {:?}", first_signer_details.status);
if let Some(signed_time) = first_signer_details.signed_at {
println!(" Signed At: {}", signed_time);
@ -110,6 +113,6 @@ fn main() {
println!(" Created By: {}", latest_rev.created_by);
println!(" Revision Created At: {}", latest_rev.created_at);
}
println!("\nLegal Contract Model demonstration complete.");
}

View File

@ -22,13 +22,18 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
if !script_path.exists() {
eprintln!("Error: Rhai script not found at {}", script_path_str);
eprintln!("Please ensure the script 'legal.rhai' exists in the 'examples/legal_rhai/' directory.");
return Err(Box::new(std::io::Error::new(std::io::ErrorKind::NotFound, format!("Rhai script not found: {}", script_path_str))));
eprintln!(
"Please ensure the script 'legal.rhai' exists in the 'examples/legal_rhai/' directory."
);
return Err(Box::new(std::io::Error::new(
std::io::ErrorKind::NotFound,
format!("Rhai script not found: {}", script_path_str),
)));
}
println!("Executing Rhai script: {}", script_path_str);
let script = fs::read_to_string(script_path)?;
match engine.eval::<()>(&script) {
Ok(_) => println!("\nRhai script executed successfully!"),
Err(e) => {

View File

@ -33,6 +33,5 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
fs::remove_dir_all(db_path)?;
println!("--- Cleaned up temporary database. ---");
Ok(())
}

View File

@ -59,21 +59,39 @@ fn main() {
println!("Before saving - SimpleUser DB Keys: {:?}", user.db_keys());
println!("\nBefore saving - CustomUser ID: {}", custom_user.get_id());
println!("Before saving - CustomUser DB Keys: {:?}", custom_user.db_keys());
println!(
"Before saving - CustomUser DB Keys: {:?}",
custom_user.db_keys()
);
// Save the models to the database
let simple_collection = db.collection::<SimpleUser>().expect("can open simple user collection");
let custom_collection = db.collection::<CustomUser>().expect("can open custom user collection");
let simple_collection = db
.collection::<SimpleUser>()
.expect("can open simple user collection");
let custom_collection = db
.collection::<CustomUser>()
.expect("can open custom user collection");
let (user_id, saved_user) = simple_collection.set(&user).expect("can save simple user");
let (custom_user_id, saved_custom_user) = custom_collection.set(&custom_user).expect("can save custom user");
let (custom_user_id, saved_custom_user) = custom_collection
.set(&custom_user)
.expect("can save custom user");
println!("\nAfter saving - SimpleUser ID: {}", saved_user.get_id());
println!("After saving - SimpleUser DB Keys: {:?}", saved_user.db_keys());
println!(
"After saving - SimpleUser DB Keys: {:?}",
saved_user.db_keys()
);
println!("Returned SimpleUser ID: {}", user_id);
println!("\nAfter saving - CustomUser ID: {}", saved_custom_user.get_id());
println!("After saving - CustomUser DB Keys: {:?}", saved_custom_user.db_keys());
println!(
"\nAfter saving - CustomUser ID: {}",
saved_custom_user.get_id()
);
println!(
"After saving - CustomUser DB Keys: {:?}",
saved_custom_user.db_keys()
);
println!("Returned CustomUser ID: {}", custom_user_id);
// Verify that the IDs were auto-generated
@ -83,5 +101,8 @@ fn main() {
assert_ne!(saved_custom_user.get_id(), 0);
println!("\nExample finished. DB stored at {}", db_path);
println!("To clean up, you can manually delete the directory: {}", db_path);
println!(
"To clean up, you can manually delete the directory: {}",
db_path
);
}

View File

@ -1,8 +1,8 @@
use rhai::{Engine, EvalAltResult, Scope};
use std::sync::Arc;
use heromodels::db::hero::OurDB;
use heromodels::models::projects::register_projects_rhai_module;
use rhai::{Engine, EvalAltResult, Scope};
use std::fs;
use std::sync::Arc;
fn main() -> Result<(), Box<EvalAltResult>> {
println!("Executing Rhai script: examples/project_rhai/project_test.rhai");
@ -18,8 +18,12 @@ fn main() -> Result<(), Box<EvalAltResult>> {
// Read the Rhai script from file
let script_path = "examples/project_rhai/project_test.rhai";
let script_content = fs::read_to_string(script_path)
.map_err(|e| Box::new(EvalAltResult::ErrorSystem(format!("Cannot read script file: {}", script_path), e.into())))?;
let script_content = fs::read_to_string(script_path).map_err(|e| {
Box::new(EvalAltResult::ErrorSystem(
format!("Cannot read script file: {}", script_path),
e.into(),
))
})?;
// Create a new scope
let mut scope = Scope::new();

View File

@ -34,11 +34,16 @@ fn main() {
println!("Before saving - SimpleUser DB Keys: {:?}", user.db_keys());
// Save the user to the database
let collection = db.collection::<SimpleUser>().expect("can open user collection");
let collection = db
.collection::<SimpleUser>()
.expect("can open user collection");
let (user_id, saved_user) = collection.set(&user).expect("can save user");
println!("\nAfter saving - SimpleUser ID: {}", saved_user.get_id());
println!("After saving - SimpleUser DB Keys: {:?}", saved_user.db_keys());
println!(
"After saving - SimpleUser DB Keys: {:?}",
saved_user.db_keys()
);
println!("Returned ID: {}", user_id);
// Verify that the ID was auto-generated
@ -46,6 +51,8 @@ fn main() {
assert_ne!(saved_user.get_id(), 0);
println!("\nExample finished. DB stored at {}", db_path);
println!("To clean up, you can manually delete the directory: {}", db_path);
println!(
"To clean up, you can manually delete the directory: {}",
db_path
);
}

View File

@ -57,7 +57,9 @@ where
fn get_all(&self) -> Result<Vec<V>, Error<Self::Error>>;
/// Begin a transaction for this collection
fn begin_transaction(&self) -> Result<Box<dyn Transaction<Error = Self::Error>>, Error<Self::Error>>;
fn begin_transaction(
&self,
) -> Result<Box<dyn Transaction<Error = Self::Error>>, Error<Self::Error>>;
}
/// Errors returned by the DB implementation

View File

@ -436,8 +436,12 @@ where
Ok(list_of_raw_ids_set_bytes) => {
for raw_ids_set_bytes in list_of_raw_ids_set_bytes {
// Each item in the list is a bincode-serialized HashSet<u32> of object IDs.
match bincode::serde::decode_from_slice::<HashSet<u32>, _>(&raw_ids_set_bytes, BINCODE_CONFIG) {
Ok((ids_set, _)) => { // Destructure the tuple (HashSet<u32>, usize)
match bincode::serde::decode_from_slice::<HashSet<u32>, _>(
&raw_ids_set_bytes,
BINCODE_CONFIG,
) {
Ok((ids_set, _)) => {
// Destructure the tuple (HashSet<u32>, usize)
all_object_ids.extend(ids_set);
}
Err(e) => {

View File

@ -55,4 +55,4 @@ impl Access {
self.expires_at = expires_at;
self
}
}
}

View File

@ -3,5 +3,5 @@ pub mod access;
pub mod rhai;
// Re-export contact, Group from the inner contact module (contact.rs) within src/models/contact/mod.rs
pub use self::access::{Access};
pub use self::access::Access;
pub use rhai::register_access_rhai_module;

View File

@ -1,22 +1,22 @@
use rhai::plugin::*;
use rhai::{Engine, EvalAltResult, Position, Module, INT, Dynamic, Array};
use std::sync::Arc;
use std::mem;
use crate::db::Db;
use rhai::plugin::*;
use rhai::{Array, Dynamic, Engine, EvalAltResult, INT, Module, Position};
use std::mem;
use std::sync::Arc;
use super::access::{Access};
use super::access::Access;
type RhaiAccess = Access;
use crate::db::hero::OurDB;
use crate::db::Collection;
use crate::db::hero::OurDB;
// Helper to convert i64 from Rhai to u32 for IDs
fn id_from_i64_to_u32(id_i64: i64) -> Result<u32, Box<EvalAltResult>> {
u32::try_from(id_i64).map_err(|_|
u32::try_from(id_i64).map_err(|_| {
Box::new(EvalAltResult::ErrorArithmetic(
format!("Failed to convert ID '{}' to u32", id_i64).into(),
Position::NONE
Position::NONE,
))
)
})
}
#[export_module]
@ -27,13 +27,16 @@ mod rhai_access_module {
let access = Access::new();
Ok(access)
}
/// Sets the access name
#[rhai_fn(name = "object_id", return_raw, global, pure)]
pub fn access_object_id(access: &mut RhaiAccess, object_id: u32) -> Result<RhaiAccess, Box<EvalAltResult>> {
pub fn access_object_id(
access: &mut RhaiAccess,
object_id: u32,
) -> Result<RhaiAccess, Box<EvalAltResult>> {
// Create a default Access to replace the taken one
let default_access = Access::new();
// Take ownership of the access, apply the builder method, then put it back
let owned_access = std::mem::replace(access, default_access);
*access = owned_access.object_id(object_id);
@ -41,21 +44,27 @@ mod rhai_access_module {
}
#[rhai_fn(name = "circle_id", return_raw, global, pure)]
pub fn access_circle_id(access: &mut RhaiAccess, circle_id: u32) -> Result<RhaiAccess, Box<EvalAltResult>> {
pub fn access_circle_id(
access: &mut RhaiAccess,
circle_id: u32,
) -> Result<RhaiAccess, Box<EvalAltResult>> {
// Create a default Access to replace the taken one
let default_access = Access::new();
// Take ownership of the access, apply the builder method, then put it back
let owned_access = std::mem::replace(access, default_access);
*access = owned_access.circle_id(circle_id);
Ok(access.clone())
}
#[rhai_fn(name = "group_id", return_raw, global, pure)]
pub fn access_group_id(access: &mut RhaiAccess, group_id: u32) -> Result<RhaiAccess, Box<EvalAltResult>> {
pub fn access_group_id(
access: &mut RhaiAccess,
group_id: u32,
) -> Result<RhaiAccess, Box<EvalAltResult>> {
// Create a default Access to replace the taken one
let default_access = Access::new();
// Take ownership of the access, apply the builder method, then put it back
let owned_access = std::mem::replace(access, default_access);
*access = owned_access.group_id(group_id);
@ -63,10 +72,13 @@ mod rhai_access_module {
}
#[rhai_fn(name = "contact_id", return_raw, global, pure)]
pub fn access_contact_id(access: &mut RhaiAccess, contact_id: u32) -> Result<RhaiAccess, Box<EvalAltResult>> {
pub fn access_contact_id(
access: &mut RhaiAccess,
contact_id: u32,
) -> Result<RhaiAccess, Box<EvalAltResult>> {
// Create a default Access to replace the taken one
let default_access = Access::new();
// Take ownership of the access, apply the builder method, then put it back
let owned_access = std::mem::replace(access, default_access);
*access = owned_access.contact_id(contact_id);
@ -74,10 +86,13 @@ mod rhai_access_module {
}
#[rhai_fn(name = "expires_at", return_raw, global, pure)]
pub fn access_expires_at(access: &mut RhaiAccess, expires_at: Option<u64>) -> Result<RhaiAccess, Box<EvalAltResult>> {
pub fn access_expires_at(
access: &mut RhaiAccess,
expires_at: Option<u64>,
) -> Result<RhaiAccess, Box<EvalAltResult>> {
// Create a default Access to replace the taken one
let default_access = Access::new();
// Take ownership of the access, apply the builder method, then put it back
let owned_access = std::mem::replace(access, default_access);
*access = owned_access.expires_at(expires_at);
@ -86,90 +101,136 @@ mod rhai_access_module {
// Access Getters
#[rhai_fn(get = "id", pure)]
pub fn get_access_id(access: &mut RhaiAccess) -> i64 { access.base_data.id as i64 }
pub fn get_access_id(access: &mut RhaiAccess) -> i64 {
access.base_data.id as i64
}
#[rhai_fn(get = "object_id", pure)]
pub fn get_access_object_id(access: &mut RhaiAccess) -> i64 { access.object_id as i64 }
pub fn get_access_object_id(access: &mut RhaiAccess) -> i64 {
access.object_id as i64
}
#[rhai_fn(get = "circle_id", pure)]
pub fn get_access_circle_id(access: &mut RhaiAccess) -> i64 { access.circle_id as i64 }
pub fn get_access_circle_id(access: &mut RhaiAccess) -> i64 {
access.circle_id as i64
}
#[rhai_fn(get = "group_id", pure)]
pub fn get_access_group_id(access: &mut RhaiAccess) -> i64 { access.group_id as i64 }
pub fn get_access_group_id(access: &mut RhaiAccess) -> i64 {
access.group_id as i64
}
#[rhai_fn(get = "contact_id", pure)]
pub fn get_access_contact_id(access: &mut RhaiAccess) -> i64 { access.contact_id as i64 }
pub fn get_access_contact_id(access: &mut RhaiAccess) -> i64 {
access.contact_id as i64
}
#[rhai_fn(get = "expires_at", pure)]
pub fn get_access_expires_at(access: &mut RhaiAccess) -> i64 { access.expires_at.unwrap_or(0) as i64 }
pub fn get_access_expires_at(access: &mut RhaiAccess) -> i64 {
access.expires_at.unwrap_or(0) as i64
}
#[rhai_fn(get = "created_at", pure)]
pub fn get_access_created_at(access: &mut RhaiAccess) -> i64 { access.base_data.created_at }
pub fn get_access_created_at(access: &mut RhaiAccess) -> i64 {
access.base_data.created_at
}
#[rhai_fn(get = "modified_at", pure)]
pub fn get_access_modified_at(access: &mut RhaiAccess) -> i64 { access.base_data.modified_at }
pub fn get_access_modified_at(access: &mut RhaiAccess) -> i64 {
access.base_data.modified_at
}
}
pub fn register_access_rhai_module(engine: &mut Engine, db: Arc<OurDB>) {
// Register the exported module globally
let module = exported_module!(rhai_access_module);
engine.register_global_module(module.into());
// Create a module for database functions
let mut db_module = Module::new();
let db_clone_set_access = db.clone();
db_module.set_native_fn("save_access", move |access: Access| -> Result<Access, Box<EvalAltResult>> {
// Use the Collection trait method directly
let result = db_clone_set_access.set(&access)
.map_err(|e| Box::new(EvalAltResult::ErrorRuntime(format!("DB Error set_access: {}", e).into(), Position::NONE)))?;
// Return the updated access with the correct ID
Ok(result.1)
});
db_module.set_native_fn(
"save_access",
move |access: Access| -> Result<Access, Box<EvalAltResult>> {
// Use the Collection trait method directly
let result = db_clone_set_access.set(&access).map_err(|e| {
Box::new(EvalAltResult::ErrorRuntime(
format!("DB Error set_access: {}", e).into(),
Position::NONE,
))
})?;
// Return the updated access with the correct ID
Ok(result.1)
},
);
// Manually register database functions as they need to capture 'db'
let db_clone_delete_access = db.clone();
db_module.set_native_fn("delete_access", move |access: Access| -> Result<(), Box<EvalAltResult>> {
// Use the Collection trait method directly
let result = db_clone_delete_access.collection::<Access>()
.expect("can open access collection")
.delete_by_id(access.base_data.id)
.expect("can delete event");
// Return the updated event with the correct ID
Ok(result)
});
db_module.set_native_fn(
"delete_access",
move |access: Access| -> Result<(), Box<EvalAltResult>> {
// Use the Collection trait method directly
let result = db_clone_delete_access
.collection::<Access>()
.expect("can open access collection")
.delete_by_id(access.base_data.id)
.expect("can delete event");
// Return the updated event with the correct ID
Ok(result)
},
);
let db_clone_get_access = db.clone();
db_module.set_native_fn("get_access_by_id", move |id_i64: INT| -> Result<Access, Box<EvalAltResult>> {
let id_u32 = id_from_i64_to_u32(id_i64)?;
// Use the Collection trait method directly
db_clone_get_access.get_by_id(id_u32)
.map_err(|e| Box::new(EvalAltResult::ErrorRuntime(format!("DB Error get_access_by_id: {}", e).into(), Position::NONE)))?
.ok_or_else(|| Box::new(EvalAltResult::ErrorRuntime(format!("Access with ID {} not found", id_u32).into(), Position::NONE)))
});
db_module.set_native_fn(
"get_access_by_id",
move |id_i64: INT| -> Result<Access, Box<EvalAltResult>> {
let id_u32 = id_from_i64_to_u32(id_i64)?;
// Use the Collection trait method directly
db_clone_get_access
.get_by_id(id_u32)
.map_err(|e| {
Box::new(EvalAltResult::ErrorRuntime(
format!("DB Error get_access_by_id: {}", e).into(),
Position::NONE,
))
})?
.ok_or_else(|| {
Box::new(EvalAltResult::ErrorRuntime(
format!("Access with ID {} not found", id_u32).into(),
Position::NONE,
))
})
},
);
// Add list_accesss function to get all accesss
let db_clone_list_accesss = db.clone();
db_module.set_native_fn("list_accesss", move || -> Result<Dynamic, Box<EvalAltResult>> {
let collection = db_clone_list_accesss.collection::<Access>()
.map_err(|e| Box::new(EvalAltResult::ErrorRuntime(
format!("Failed to get access collection: {:?}", e).into(),
Position::NONE
)))?;
let accesss = collection.get_all()
.map_err(|e| Box::new(EvalAltResult::ErrorRuntime(
format!("Failed to get all accesss: {:?}", e).into(),
Position::NONE
)))?;
let mut array = Array::new();
for access in accesss {
array.push(Dynamic::from(access));
}
Ok(Dynamic::from(array))
});
db_module.set_native_fn(
"list_accesss",
move || -> Result<Dynamic, Box<EvalAltResult>> {
let collection = db_clone_list_accesss.collection::<Access>().map_err(|e| {
Box::new(EvalAltResult::ErrorRuntime(
format!("Failed to get access collection: {:?}", e).into(),
Position::NONE,
))
})?;
let accesss = collection.get_all().map_err(|e| {
Box::new(EvalAltResult::ErrorRuntime(
format!("Failed to get all accesss: {:?}", e).into(),
Position::NONE,
))
})?;
let mut array = Array::new();
for access in accesss {
array.push(Dynamic::from(access));
}
Ok(Dynamic::from(array))
},
);
// Register the database module globally
engine.register_global_module(db_module.into());

View File

@ -1,8 +1,8 @@
use serde::{Deserialize, Serialize};
use heromodels_core::{BaseModelData, Index};
use rhai::{CustomType, TypeBuilder}; // For #[derive(CustomType)]
use heromodels_core::BaseModelDataOps;
use heromodels_core::{BaseModelData, Index};
use heromodels_derive::model;
use rhai::{CustomType, TypeBuilder}; // For #[derive(CustomType)]
use serde::{Deserialize, Serialize};
// --- Enums ---
@ -100,17 +100,17 @@ impl Company {
status: CompanyStatus::default(),
}
}
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

View File

@ -8,17 +8,16 @@ pub mod product;
// pub mod user;
// Re-export main types from sub-modules
pub use company::{Company, CompanyStatus, BusinessType};
pub use company::{BusinessType, Company, CompanyStatus};
pub mod shareholder;
pub use product::{Product, ProductComponent, ProductStatus, ProductType};
pub use shareholder::{Shareholder, ShareholderType};
pub use product::{Product, ProductType, ProductStatus, ProductComponent};
pub mod sale;
pub use sale::{Sale, SaleItem, SaleStatus};
// pub use user::{User}; // Assuming a simple User model for now
#[cfg(feature = "rhai")]
pub mod rhai;
#[cfg(feature = "rhai")]

View File

@ -1,6 +1,6 @@
use serde::{Serialize, Deserialize};
use heromodels_core::BaseModelData;
use heromodels_derive::model;
use serde::{Deserialize, Serialize};
// ProductType represents the type of a product
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
@ -137,7 +137,7 @@ impl Product {
self.components.push(component);
self
}
pub fn components(mut self, components: Vec<ProductComponent>) -> Self {
self.components = components;
self

View File

@ -1,15 +1,15 @@
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 crate::db::hero::OurDB;
use rhai::plugin::*;
use rhai::{Dynamic, Engine, EvalAltResult, INT, Module, Position};
use std::mem;
use std::sync::Arc;
use super::company::{Company, CompanyStatus, BusinessType};
use crate::models::biz::shareholder::{Shareholder, ShareholderType};
use crate::models::biz::product::{Product, ProductType, ProductStatus, ProductComponent};
use super::company::{BusinessType, Company, CompanyStatus};
use crate::models::biz::product::{Product, ProductComponent, ProductStatus, ProductType};
use crate::models::biz::sale::{Sale, SaleItem, SaleStatus};
use crate::models::biz::shareholder::{Shareholder, ShareholderType};
use heromodels_core::Model;
type RhaiCompany = Company;
@ -21,12 +21,12 @@ type RhaiSaleItem = SaleItem;
// Helper to convert i64 from Rhai to u32 for IDs
fn id_from_i64_to_u32(id_i64: i64) -> Result<u32, Box<EvalAltResult>> {
u32::try_from(id_i64).map_err(|_|
u32::try_from(id_i64).map_err(|_| {
Box::new(EvalAltResult::ErrorArithmetic(
format!("Failed to convert ID '{}' to u32", id_i64).into(),
Position::NONE
Position::NONE,
))
)
})
}
#[export_module]
@ -36,443 +36,541 @@ mod rhai_biz_module {
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<RhaiCompany, Box<EvalAltResult>> {
pub fn company_name(
company: &mut RhaiCompany,
name: String,
) -> Result<RhaiCompany, Box<EvalAltResult>> {
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<RhaiCompany, Box<EvalAltResult>> {
pub fn company_fiscal_year_end(
company: &mut RhaiCompany,
fiscal_year_end: String,
) -> Result<RhaiCompany, Box<EvalAltResult>> {
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<RhaiCompany, Box<EvalAltResult>> {
pub fn company_registration_number(
company: &mut RhaiCompany,
reg_num: String,
) -> Result<RhaiCompany, Box<EvalAltResult>> {
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<RhaiCompany, Box<EvalAltResult>> {
pub fn company_incorporation_date(
company: &mut RhaiCompany,
date: i64,
) -> Result<RhaiCompany, Box<EvalAltResult>> {
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<RhaiCompany, Box<EvalAltResult>> {
pub fn company_status(
company: &mut RhaiCompany,
status: CompanyStatus,
) -> Result<RhaiCompany, Box<EvalAltResult>> {
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<RhaiCompany, Box<EvalAltResult>> {
pub fn company_business_type(
company: &mut RhaiCompany,
business_type: BusinessType,
) -> Result<RhaiCompany, Box<EvalAltResult>> {
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<RhaiShareholder, Box<EvalAltResult>> {
pub fn shareholder_name(
shareholder: &mut RhaiShareholder,
name: String,
) -> Result<RhaiShareholder, Box<EvalAltResult>> {
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<RhaiShareholder, Box<EvalAltResult>> {
pub fn shareholder_company_id(
shareholder: &mut RhaiShareholder,
company_id: i64,
) -> Result<RhaiShareholder, Box<EvalAltResult>> {
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<RhaiShareholder, Box<EvalAltResult>> {
let owned_shareholder = mem::take(shareholder);
pub fn shareholder_share_count(
shareholder: &mut RhaiShareholder,
share_count: f64,
) -> Result<RhaiShareholder, Box<EvalAltResult>> {
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<RhaiShareholder, Box<EvalAltResult>> {
pub fn shareholder_type(
shareholder: &mut RhaiShareholder,
type_: ShareholderType,
) -> Result<RhaiShareholder, Box<EvalAltResult>> {
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<RhaiProductComponent, Box<EvalAltResult>> {
pub fn product_component_name(
component: &mut RhaiProductComponent,
name: String,
) -> Result<RhaiProductComponent, Box<EvalAltResult>> {
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<RhaiProductComponent, Box<EvalAltResult>> {
pub fn product_component_description(
component: &mut RhaiProductComponent,
description: String,
) -> Result<RhaiProductComponent, Box<EvalAltResult>> {
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<RhaiProductComponent, Box<EvalAltResult>> {
pub fn product_component_quantity(
component: &mut RhaiProductComponent,
quantity: i64,
) -> Result<RhaiProductComponent, Box<EvalAltResult>> {
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<RhaiProduct, Box<EvalAltResult>> {
pub fn product_name(
product: &mut RhaiProduct,
name: String,
) -> Result<RhaiProduct, Box<EvalAltResult>> {
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<RhaiProduct, Box<EvalAltResult>> {
pub fn product_description(
product: &mut RhaiProduct,
description: String,
) -> Result<RhaiProduct, Box<EvalAltResult>> {
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<RhaiProduct, Box<EvalAltResult>> {
pub fn product_price(
product: &mut RhaiProduct,
price: f64,
) -> Result<RhaiProduct, Box<EvalAltResult>> {
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<RhaiProduct, Box<EvalAltResult>> {
pub fn product_type(
product: &mut RhaiProduct,
type_: ProductType,
) -> Result<RhaiProduct, Box<EvalAltResult>> {
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<RhaiProduct, Box<EvalAltResult>> {
pub fn product_category(
product: &mut RhaiProduct,
category: String,
) -> Result<RhaiProduct, Box<EvalAltResult>> {
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<RhaiProduct, Box<EvalAltResult>> {
pub fn product_status(
product: &mut RhaiProduct,
status: ProductStatus,
) -> Result<RhaiProduct, Box<EvalAltResult>> {
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<RhaiProduct, Box<EvalAltResult>> {
pub fn product_max_amount(
product: &mut RhaiProduct,
max_amount: i64,
) -> Result<RhaiProduct, Box<EvalAltResult>> {
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<RhaiProduct, Box<EvalAltResult>> {
pub fn product_purchase_till(
product: &mut RhaiProduct,
purchase_till: i64,
) -> Result<RhaiProduct, Box<EvalAltResult>> {
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<RhaiProduct, Box<EvalAltResult>> {
pub fn product_active_till(
product: &mut RhaiProduct,
active_till: i64,
) -> Result<RhaiProduct, Box<EvalAltResult>> {
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<RhaiProduct, Box<EvalAltResult>> {
pub fn product_add_component(
product: &mut RhaiProduct,
component: RhaiProductComponent,
) -> Result<RhaiProduct, Box<EvalAltResult>> {
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<RhaiProductComponent>) -> Result<RhaiProduct, Box<EvalAltResult>> {
pub fn product_components(
product: &mut RhaiProduct,
components: Vec<RhaiProductComponent>,
) -> Result<RhaiProduct, Box<EvalAltResult>> {
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<RhaiProductComponent> {
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<i64> {
product.base_data.comments.iter().map(|&id| id as i64).collect()
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<RhaiSaleItem, Box<EvalAltResult>> {
pub fn sale_item_name(
item: &mut RhaiSaleItem,
name: String,
) -> Result<RhaiSaleItem, Box<EvalAltResult>> {
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<RhaiSaleItem, Box<EvalAltResult>> {
let owned_item = mem::take(item);
pub fn sale_item_price(
item: &mut RhaiSaleItem,
price: f64,
) -> Result<RhaiSaleItem, Box<EvalAltResult>> {
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<RhaiSaleItem, Box<EvalAltResult>> {
pub fn sale_item_quantity(
item: &mut RhaiSaleItem,
quantity: i64,
) -> Result<RhaiSaleItem, Box<EvalAltResult>> {
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<RhaiSaleItem, Box<EvalAltResult>> {
pub fn sale_item_product_id(
item: &mut RhaiSaleItem,
product_id: i64,
) -> Result<RhaiSaleItem, Box<EvalAltResult>> {
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<RhaiSale, Box<EvalAltResult>> {
let owned_sale = mem::take(sale);
pub fn sale_transaction_id(
sale: &mut RhaiSale,
transaction_id: u32,
) -> Result<RhaiSale, Box<EvalAltResult>> {
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<RhaiSale, Box<EvalAltResult>> {
pub fn sale_status(
sale: &mut RhaiSale,
status: SaleStatus,
) -> Result<RhaiSale, Box<EvalAltResult>> {
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<RhaiSale, Box<EvalAltResult>> {
pub fn sale_add_item(
sale: &mut RhaiSale,
item: RhaiSaleItem,
) -> Result<RhaiSale, Box<EvalAltResult>> {
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<RhaiSaleItem>) -> Result<RhaiSale, Box<EvalAltResult>> {
pub fn sale_items(
sale: &mut RhaiSale,
items: Vec<RhaiSaleItem>,
) -> Result<RhaiSale, Box<EvalAltResult>> {
let owned_sale = mem::take(sale);
*sale = owned_sale.items(items);
Ok(sale.clone())
@ -483,35 +581,39 @@ mod rhai_biz_module {
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<RhaiSaleItem> {
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<i64> {
sale.base_data.comments.iter().map(|&id| id as i64).collect()
sale.base_data
.comments
.iter()
.map(|&id| id as i64)
.collect()
}
}
@ -519,110 +621,174 @@ pub fn register_biz_rhai_module(engine: &mut Engine, db: Arc<OurDB>) {
// 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<INT, Box<EvalAltResult>> {
let company_collection_set = db_for_set_company.collection::<Company>().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
)))
});
db_module.set_native_fn(
"set_company",
move |company: Company| -> Result<INT, Box<EvalAltResult>> {
let company_collection_set = db_for_set_company
.collection::<Company>()
.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<Dynamic, Box<EvalAltResult>> {
let company_collection_get = db_for_get_company.collection::<Company>().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
)))
});
db_module.set_native_fn(
"get_company_by_id",
move |id: INT| -> Result<Dynamic, Box<EvalAltResult>> {
let company_collection_get = db_for_get_company
.collection::<Company>()
.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<INT, Box<EvalAltResult>> {
let shareholder_collection_set = db_for_set_shareholder.collection::<Shareholder>().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
)))
});
db_module.set_native_fn(
"set_shareholder",
move |shareholder: Shareholder| -> Result<INT, Box<EvalAltResult>> {
let shareholder_collection_set = db_for_set_shareholder
.collection::<Shareholder>()
.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<Dynamic, Box<EvalAltResult>> {
let shareholder_collection_get = db_for_get_shareholder.collection::<Shareholder>().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
)))
});
db_module.set_native_fn(
"get_shareholder_by_id",
move |id: INT| -> Result<Dynamic, Box<EvalAltResult>> {
let shareholder_collection_get = db_for_get_shareholder
.collection::<Shareholder>()
.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<INT, Box<EvalAltResult>> {
let product_collection_set = db_for_set_product.collection::<Product>().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
)))
});
db_module.set_native_fn(
"set_product",
move |product: Product| -> Result<INT, Box<EvalAltResult>> {
let product_collection_set = db_for_set_product
.collection::<Product>()
.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<Dynamic, Box<EvalAltResult>> {
let product_collection_get = db_for_get_product.collection::<Product>().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
)))
});
db_module.set_native_fn(
"get_product_by_id",
move |id: INT| -> Result<Dynamic, Box<EvalAltResult>> {
let product_collection_get = db_for_get_product
.collection::<Product>()
.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<INT, Box<EvalAltResult>> {
let sale_collection_set = db_for_set_sale.collection::<Sale>().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
)))
});
db_module.set_native_fn(
"set_sale",
move |sale: Sale| -> Result<INT, Box<EvalAltResult>> {
let sale_collection_set = db_for_set_sale
.collection::<Sale>()
.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<Dynamic, Box<EvalAltResult>> {
let sale_collection_get = db_for_get_sale.collection::<Sale>().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
)))
});
db_module.set_native_fn(
"get_sale_by_id",
move |id: INT| -> Result<Dynamic, Box<EvalAltResult>> {
let sale_collection_get = db_for_get_sale
.collection::<Sale>()
.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.");
}

View File

@ -1,5 +1,5 @@
use heromodels_core::{BaseModelData, BaseModelDataOps, Model};
use serde::{Deserialize, Serialize};
use heromodels_core::{BaseModelData, Model, BaseModelDataOps};
/// Represents the status of a sale.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]

View File

@ -1,6 +1,6 @@
use serde::{Deserialize, Serialize};
use heromodels_core::BaseModelData;
use heromodels_derive::model;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum ShareholderType {
@ -31,13 +31,13 @@ impl Shareholder {
pub fn new() -> Self {
Self {
base_data: BaseModelData::new(),
company_id: 0, // Default, to be set by builder
user_id: 0, // Default, to be set by builder
name: String::new(), // Default
shares: 0.0, // Default
percentage: 0.0, // Default
company_id: 0, // Default, to be set by builder
user_id: 0, // Default, to be set by builder
name: String::new(), // Default
shares: 0.0, // Default
percentage: 0.0, // Default
type_: ShareholderType::default(), // Uses ShareholderType's Default impl
since: 0, // Default timestamp, to be set by builder
since: 0, // Default timestamp, to be set by builder
}
}
@ -78,4 +78,4 @@ impl Shareholder {
}
// Base data operations are now handled by BaseModelDataOps trait
}
}

View File

@ -26,7 +26,7 @@ impl AttendanceStatus {
_ => Err(format!("Invalid attendance status: '{}'", s)),
}
}
/// Convert an AttendanceStatus to a string
pub fn to_string(&self) -> String {
match self {
@ -134,7 +134,11 @@ impl Event {
/// Adds an attendee to the event
pub fn add_attendee(mut self, attendee: Attendee) -> Self {
// Prevent duplicate attendees by contact_id
if !self.attendees.iter().any(|a| a.contact_id == attendee.contact_id) {
if !self
.attendees
.iter()
.any(|a| a.contact_id == attendee.contact_id)
{
self.attendees.push(attendee);
}
self
@ -148,18 +152,18 @@ impl Event {
/// Updates the status of an existing attendee
pub fn update_attendee_status(mut self, contact_id: u32, status: AttendanceStatus) -> Self {
if let Some(attendee) = self.attendees.iter_mut().find(|a| a.contact_id == contact_id) {
if let Some(attendee) = self
.attendees
.iter_mut()
.find(|a| a.contact_id == contact_id)
{
attendee.status = status;
}
self
}
/// Reschedules the event to new start and end times
pub fn reschedule(
mut self,
new_start_time: i64,
new_end_time: i64,
) -> Self {
pub fn reschedule(mut self, 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 {
self.start_time = new_start_time;
@ -236,7 +240,8 @@ impl Calendar {
/// Removes an event from the calendar by its ID
pub fn remove_event(mut self, event_id_to_remove: i64) -> Self {
self.events.retain(|&event_id_in_vec| event_id_in_vec != event_id_to_remove);
self.events
.retain(|&event_id_in_vec| event_id_in_vec != event_id_to_remove);
self
}
}

View File

@ -3,5 +3,5 @@ pub mod calendar;
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 self::calendar::{AttendanceStatus, Attendee, Calendar, Event};
pub use rhai::register_calendar_rhai_module;

View File

@ -1,24 +1,24 @@
use rhai::plugin::*;
use rhai::{Engine, EvalAltResult, Position, Module, INT, Dynamic, Array};
use std::sync::Arc;
use std::mem;
use crate::db::Db;
use rhai::plugin::*;
use rhai::{Array, Dynamic, Engine, EvalAltResult, INT, Module, Position};
use std::mem;
use std::sync::Arc;
use super::calendar::{Event, Attendee, Calendar, AttendanceStatus};
use super::calendar::{AttendanceStatus, Attendee, Calendar, Event};
type RhaiEvent = Event;
type RhaiAttendee = Attendee;
type RhaiCalendar = Calendar;
use crate::db::hero::OurDB;
use crate::db::Collection;
use crate::db::hero::OurDB;
// Helper to convert i64 from Rhai to u32 for IDs
fn id_from_i64_to_u32(id_i64: i64) -> Result<u32, Box<EvalAltResult>> {
u32::try_from(id_i64).map_err(|_|
u32::try_from(id_i64).map_err(|_| {
Box::new(EvalAltResult::ErrorArithmetic(
format!("Failed to convert ID '{}' to u32", id_i64).into(),
Position::NONE
Position::NONE,
))
)
})
}
#[export_module]
@ -28,26 +28,35 @@ mod rhai_calendar_module {
pub fn new_event() -> RhaiEvent {
Event::new()
}
/// Sets the event title
#[rhai_fn(name = "title", return_raw, global, pure)]
pub fn event_title(event: &mut RhaiEvent, title: String) -> Result<RhaiEvent, Box<EvalAltResult>> {
pub fn event_title(
event: &mut RhaiEvent,
title: String,
) -> Result<RhaiEvent, Box<EvalAltResult>> {
let owned_event = mem::take(event);
*event = owned_event.title(title);
Ok(event.clone())
}
/// Sets the event description
#[rhai_fn(name = "description", return_raw, global, pure)]
pub fn event_description(event: &mut RhaiEvent, description: String) -> Result<RhaiEvent, Box<EvalAltResult>> {
pub fn event_description(
event: &mut RhaiEvent,
description: String,
) -> Result<RhaiEvent, Box<EvalAltResult>> {
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<RhaiEvent, Box<EvalAltResult>> {
pub fn event_location(
event: &mut RhaiEvent,
location: String,
) -> Result<RhaiEvent, Box<EvalAltResult>> {
let owned_event = mem::take(event);
*event = owned_event.location(location);
Ok(event.clone())
@ -55,7 +64,10 @@ mod rhai_calendar_module {
/// 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<RhaiEvent, Box<EvalAltResult>> {
pub fn event_add_attendee(
event: &mut RhaiEvent,
attendee: RhaiAttendee,
) -> Result<RhaiEvent, Box<EvalAltResult>> {
// Use take to get ownership of the event
let owned_event = mem::take(event);
*event = owned_event.add_attendee(attendee);
@ -64,30 +76,38 @@ mod rhai_calendar_module {
/// 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<RhaiEvent, Box<EvalAltResult>> {
pub fn event_reschedule(
event: &mut RhaiEvent,
new_start_time: i64,
new_end_time: i64,
) -> Result<RhaiEvent, Box<EvalAltResult>> {
// 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
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<RhaiEvent, Box<EvalAltResult>> {
pub fn event_update_attendee_status(
event: &mut RhaiEvent,
contact_id: i64,
status_str: String,
) -> Result<RhaiEvent, Box<EvalAltResult>> {
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);
@ -96,50 +116,74 @@ mod rhai_calendar_module {
// Event Getters
#[rhai_fn(get = "id", pure)]
pub fn get_event_id(event: &mut RhaiEvent) -> i64 { event.base_data.id as i64 }
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 }
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 }
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() }
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<String> { event.description.clone() }
pub fn get_event_description(event: &mut RhaiEvent) -> Option<String> {
event.description.clone()
}
#[rhai_fn(get = "start_time", pure)]
pub fn get_event_start_time(event: &mut RhaiEvent) -> i64 { event.start_time }
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 }
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<String> { event.location.clone() }
pub fn get_event_location(event: &mut RhaiEvent) -> Option<String> {
event.location.clone()
}
#[rhai_fn(get = "attendees", pure)]
pub fn get_event_attendees(event: &mut RhaiEvent) -> Vec<RhaiAttendee> { event.attendees.clone() }
pub fn get_event_attendees(event: &mut RhaiEvent) -> Vec<RhaiAttendee> {
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<RhaiAttendee, Box<EvalAltResult>> {
pub fn attendee_with_contact_id(
attendee: &mut RhaiAttendee,
contact_id: i64,
) -> Result<RhaiAttendee, Box<EvalAltResult>> {
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<RhaiAttendee, Box<EvalAltResult>> {
pub fn attendee_with_status(
attendee: &mut RhaiAttendee,
status_str: String,
) -> Result<RhaiAttendee, Box<EvalAltResult>> {
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())
@ -149,9 +193,13 @@ mod rhai_calendar_module {
// Attendee Getters
#[rhai_fn(get = "contact_id", pure)]
pub fn get_attendee_contact_id(attendee: &mut RhaiAttendee) -> i64 { attendee.contact_id as i64 }
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() }
pub fn get_attendee_status(attendee: &mut RhaiAttendee) -> String {
attendee.status.to_string()
}
// --- Calendar Functions ---
#[rhai_fn(name = "new_calendar", return_raw)]
@ -159,25 +207,31 @@ mod rhai_calendar_module {
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<RhaiCalendar, Box<EvalAltResult>> {
pub fn calendar_name(
calendar: &mut RhaiCalendar,
name: String,
) -> Result<RhaiCalendar, Box<EvalAltResult>> {
// 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<RhaiCalendar, Box<EvalAltResult>> {
pub fn calendar_description(
calendar: &mut RhaiCalendar,
description: String,
) -> Result<RhaiCalendar, Box<EvalAltResult>> {
// 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);
@ -186,10 +240,13 @@ mod rhai_calendar_module {
}
#[rhai_fn(name = "add_event_to_calendar", return_raw, global, pure)]
pub fn calendar_add_event(calendar: &mut RhaiCalendar, event: RhaiEvent) -> Result<RhaiCalendar, Box<EvalAltResult>> {
pub fn calendar_add_event(
calendar: &mut RhaiCalendar,
event: RhaiEvent,
) -> Result<RhaiCalendar, Box<EvalAltResult>> {
// 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);
@ -197,10 +254,13 @@ mod rhai_calendar_module {
}
#[rhai_fn(name = "remove_event_from_calendar", return_raw)]
pub fn calendar_remove_event(calendar: &mut RhaiCalendar, event_id: i64) -> Result<(), Box<EvalAltResult>> {
pub fn calendar_remove_event(
calendar: &mut RhaiCalendar,
event_id: i64,
) -> Result<(), Box<EvalAltResult>> {
// 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);
@ -209,140 +269,213 @@ mod rhai_calendar_module {
// Calendar Getters
#[rhai_fn(get = "id", pure)]
pub fn get_calendar_id(calendar: &mut RhaiCalendar) -> i64 { calendar.base_data.id as i64 }
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() }
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 }
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 }
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<i64> { calendar.events.clone() }
pub fn get_calendar_events(calendar: &mut RhaiCalendar) -> Vec<i64> {
calendar.events.clone()
}
#[rhai_fn(get = "description", pure)]
pub fn get_calendar_description(calendar: &mut RhaiCalendar) -> Option<String> { calendar.description.clone() }
pub fn get_calendar_description(calendar: &mut RhaiCalendar) -> Option<String> {
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 }
}
pub fn register_calendar_rhai_module(engine: &mut Engine, db: Arc<OurDB>) {
// 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<Event, Box<EvalAltResult>> {
// 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)
});
db_module.set_native_fn(
"save_event",
move |event: Event| -> Result<Event, Box<EvalAltResult>> {
// 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<EvalAltResult>> {
// Use the Collection trait method directly
let result = db_clone_delete_event.collection::<Event>()
.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)
});
db_module.set_native_fn(
"delete_event",
move |event: Event| -> Result<(), Box<EvalAltResult>> {
// Use the Collection trait method directly
let result = db_clone_delete_event
.collection::<Event>()
.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<Event, Box<EvalAltResult>> {
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)))
});
db_module.set_native_fn(
"get_event_by_id",
move |id_i64: INT| -> Result<Event, Box<EvalAltResult>> {
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<Calendar, Box<EvalAltResult>> {
// 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)
});
db_module.set_native_fn(
"save_calendar",
move |calendar: Calendar| -> Result<Calendar, Box<EvalAltResult>> {
// 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<EvalAltResult>> {
// Use the Collection trait method directly
let result = db_clone_delete_calendar.collection::<Calendar>()
.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)
});
db_module.set_native_fn(
"delete_calendar",
move |calendar: Calendar| -> Result<(), Box<EvalAltResult>> {
// Use the Collection trait method directly
let result = db_clone_delete_calendar
.collection::<Calendar>()
.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<Calendar, Box<EvalAltResult>> {
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)))
});
db_module.set_native_fn(
"get_calendar_by_id",
move |id_i64: INT| -> Result<Calendar, Box<EvalAltResult>> {
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<Dynamic, Box<EvalAltResult>> {
let collection = db_clone_list_calendars.collection::<Calendar>()
.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))
});
db_module.set_native_fn(
"list_calendars",
move || -> Result<Dynamic, Box<EvalAltResult>> {
let collection = db_clone_list_calendars
.collection::<Calendar>()
.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<Dynamic, Box<EvalAltResult>> {
let collection = db_clone_list_events.collection::<Event>()
.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))
});
db_module.set_native_fn(
"list_events",
move || -> Result<Dynamic, Box<EvalAltResult>> {
let collection = db_clone_list_events.collection::<Event>().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());

View File

@ -19,6 +19,8 @@ pub struct Circle {
pub description: Option<String>,
/// List of related circles
pub circles: Vec<String>,
/// List of members in the circle (their public keys)
pub members: Vec<String>,
/// Logo URL or symbol for the circle
pub logo: Option<String>,
/// Theme settings for the circle (colors, styling, etc.)
@ -35,6 +37,7 @@ impl Circle {
description: None,
circles: Vec::new(),
logo: None,
members: Vec::new(),
theme: HashMap::new(),
}
}
@ -83,4 +86,13 @@ impl Circle {
}
self
}
}
/// Adds a member to the circle
pub fn add_member(mut self, member: String) -> Self {
// Prevent duplicate members
if !self.members.iter().any(|a| *a == member) {
self.members.push(member);
}
self
}
}

View File

@ -3,5 +3,5 @@ pub mod circle;
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::circle::{Circle};
pub use self::circle::Circle;
pub use rhai::register_circle_rhai_module;

View File

@ -1,16 +1,16 @@
use rhai::plugin::*;
use rhai::{Engine, EvalAltResult, Position, Module, INT, Dynamic, Array, CustomType};
use std::sync::Arc;
use std::mem;
use crate::db::Db;
use rhai::plugin::*;
use rhai::{Array, CustomType, Dynamic, Engine, EvalAltResult, INT, Module, Position};
use std::mem;
use std::sync::Arc;
use super::circle::{Circle};
use super::circle::Circle;
type RhaiCircle = Circle;
use crate::db::hero::OurDB;
use crate::db::Collection;
use crate::db::hero::OurDB;
use serde::Serialize;
use std::collections::HashMap;
use serde_json;
use std::collections::HashMap;
/// Registers a `.json()` method for any type `T` that implements the required traits.
fn register_json_method<T>(engine: &mut Engine)
@ -31,12 +31,12 @@ where
// Helper to convert i64 from Rhai to u32 for IDs
fn id_from_i64_to_u32(id_i64: i64) -> Result<u32, Box<EvalAltResult>> {
u32::try_from(id_i64).map_err(|_|
u32::try_from(id_i64).map_err(|_| {
Box::new(EvalAltResult::ErrorArithmetic(
format!("Failed to convert ID '{}' to u32", id_i64).into(),
Position::NONE
Position::NONE,
))
)
})
}
#[export_module]
@ -46,10 +46,13 @@ mod rhai_circle_module {
pub fn new_circle() -> RhaiCircle {
Circle::new()
}
/// Sets the circle title
#[rhai_fn(name = "title", return_raw, global, pure)]
pub fn circle_title(circle: &mut RhaiCircle, title: String) -> Result<RhaiCircle, Box<EvalAltResult>> {
pub fn circle_title(
circle: &mut RhaiCircle,
title: String,
) -> Result<RhaiCircle, Box<EvalAltResult>> {
let owned_circle = mem::take(circle);
*circle = owned_circle.title(title);
Ok(circle.clone())
@ -57,15 +60,21 @@ mod rhai_circle_module {
/// Sets the circle ws_url
#[rhai_fn(name = "ws_url", return_raw, global, pure)]
pub fn circle_ws_url(circle: &mut RhaiCircle, ws_url: String) -> Result<RhaiCircle, Box<EvalAltResult>> {
pub fn circle_ws_url(
circle: &mut RhaiCircle,
ws_url: String,
) -> Result<RhaiCircle, Box<EvalAltResult>> {
let owned_circle = mem::take(circle);
*circle = owned_circle.ws_url(ws_url);
Ok(circle.clone())
}
/// Sets the circle description
#[rhai_fn(name = "description", return_raw, global, pure)]
pub fn circle_description(circle: &mut RhaiCircle, description: String) -> Result<RhaiCircle, Box<EvalAltResult>> {
pub fn circle_description(
circle: &mut RhaiCircle,
description: String,
) -> Result<RhaiCircle, Box<EvalAltResult>> {
let owned_circle = mem::take(circle);
*circle = owned_circle.description(description);
Ok(circle.clone())
@ -73,7 +82,10 @@ mod rhai_circle_module {
/// Sets the circle logo
#[rhai_fn(name = "logo", return_raw, global, pure)]
pub fn circle_logo(circle: &mut RhaiCircle, logo: String) -> Result<RhaiCircle, Box<EvalAltResult>> {
pub fn circle_logo(
circle: &mut RhaiCircle,
logo: String,
) -> Result<RhaiCircle, Box<EvalAltResult>> {
let owned_circle = mem::take(circle);
*circle = owned_circle.logo(logo);
Ok(circle.clone())
@ -81,118 +93,194 @@ mod rhai_circle_module {
/// Sets the circle theme
#[rhai_fn(name = "theme", return_raw, global, pure)]
pub fn circle_theme(circle: &mut RhaiCircle, theme: HashMap<String, String>) -> Result<RhaiCircle, Box<EvalAltResult>> {
pub fn circle_theme(
circle: &mut RhaiCircle,
theme: HashMap<String, String>,
) -> Result<RhaiCircle, Box<EvalAltResult>> {
let owned_circle = mem::take(circle);
*circle = owned_circle.theme(theme);
Ok(circle.clone())
}
/// Adds an attendee to the circle
#[rhai_fn(name = "add_circle", return_raw, global, pure)]
pub fn circle_add_circle(circle: &mut RhaiCircle, added_circle: String) -> Result<RhaiCircle, Box<EvalAltResult>> {
pub fn circle_add_circle(
circle: &mut RhaiCircle,
added_circle: String,
) -> Result<RhaiCircle, Box<EvalAltResult>> {
// Use take to get ownership of the circle
let owned_circle = mem::take(circle);
*circle = owned_circle.add_circle(added_circle);
Ok(circle.clone())
}
/// Adds an attendee to the circle
#[rhai_fn(name = "add_member", return_raw, global, pure)]
pub fn circle_add_member(
circle: &mut RhaiCircle,
added_member: String,
) -> Result<RhaiCircle, Box<EvalAltResult>> {
// Use take to get ownership of the circle
let owned_circle = mem::take(circle);
*circle = owned_circle.add_member(added_member);
Ok(circle.clone())
}
// Circle Getters
#[rhai_fn(get = "id", pure)]
pub fn get_circle_id(circle: &mut RhaiCircle) -> i64 { circle.base_data.id as i64 }
pub fn get_circle_id(circle: &mut RhaiCircle) -> i64 {
circle.base_data.id as i64
}
#[rhai_fn(get = "created_at", pure)]
pub fn get_circle_created_at(circle: &mut RhaiCircle) -> i64 { circle.base_data.created_at }
pub fn get_circle_created_at(circle: &mut RhaiCircle) -> i64 {
circle.base_data.created_at
}
#[rhai_fn(get = "modified_at", pure)]
pub fn get_circle_modified_at(circle: &mut RhaiCircle) -> i64 { circle.base_data.modified_at }
pub fn get_circle_modified_at(circle: &mut RhaiCircle) -> i64 {
circle.base_data.modified_at
}
#[rhai_fn(get = "title", pure)]
pub fn get_circle_title(circle: &mut RhaiCircle) -> String { circle.title.clone() }
pub fn get_circle_title(circle: &mut RhaiCircle) -> String {
circle.title.clone()
}
#[rhai_fn(get = "description", pure)]
pub fn get_circle_description(circle: &mut RhaiCircle) -> Option<String> { circle.description.clone() }
pub fn get_circle_description(circle: &mut RhaiCircle) -> Option<String> {
circle.description.clone()
}
#[rhai_fn(get = "circles", pure)]
pub fn get_circle_circles(circle: &mut RhaiCircle) -> Vec<String> { circle.circles.clone() }
pub fn get_circle_circles(circle: &mut RhaiCircle) -> Vec<String> {
circle.circles.clone()
}
#[rhai_fn(get = "ws_url", pure)]
pub fn get_circle_ws_url(circle: &mut RhaiCircle) -> String { circle.ws_url.clone() }
pub fn get_circle_ws_url(circle: &mut RhaiCircle) -> String {
circle.ws_url.clone()
}
#[rhai_fn(get = "logo", pure)]
pub fn get_circle_logo(circle: &mut RhaiCircle) -> Option<String> { circle.logo.clone() }
pub fn get_circle_logo(circle: &mut RhaiCircle) -> Option<String> {
circle.logo.clone()
}
#[rhai_fn(get = "theme", pure)]
pub fn get_circle_theme(circle: &mut RhaiCircle) -> HashMap<String, String> { circle.theme.clone() }
pub fn get_circle_theme(circle: &mut RhaiCircle) -> HashMap<String, String> {
circle.theme.clone()
}
}
pub fn register_circle_rhai_module(engine: &mut Engine, db: Arc<OurDB>) {
// Register the exported module globally
let module = exported_module!(rhai_circle_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_circle = db.clone();
db_module.set_native_fn("save_circle", move |circle: Circle| -> Result<Circle, Box<EvalAltResult>> {
// Use the Collection trait method directly
let result = db_clone_set_circle.set(&circle)
.map_err(|e| Box::new(EvalAltResult::ErrorRuntime(format!("DB Error set_circle: {}", e).into(), Position::NONE)))?;
// Return the updated circle with the correct ID
Ok(result.1)
});
db_module.set_native_fn(
"save_circle",
move |circle: Circle| -> Result<Circle, Box<EvalAltResult>> {
// Use the Collection trait method directly
let result = db_clone_set_circle.set(&circle).map_err(|e| {
Box::new(EvalAltResult::ErrorRuntime(
format!("DB Error set_circle: {}", e).into(),
Position::NONE,
))
})?;
// Return the updated circle with the correct ID
Ok(result.1)
},
);
register_json_method::<Circle>(engine);
// Manually register database functions as they need to capture 'db'
let db_clone_delete_circle = db.clone();
db_module.set_native_fn("delete_circle", move |circle: Circle| -> Result<(), Box<EvalAltResult>> {
// Use the Collection trait method directly
let result = db_clone_delete_circle.collection::<Circle>()
.expect("can open circle collection")
.delete_by_id(circle.base_data.id)
.expect("can delete circle");
// Return the updated circle with the correct ID
Ok(result)
});
let db_clone_get_circle = db.clone();
db_module.set_native_fn("get_circle", move || -> Result<Circle, Box<EvalAltResult>> {
// Use the Collection trait method directly
let all_circles: Vec<Circle> = db_clone_get_circle.get_all()
.map_err(|e| Box::new(EvalAltResult::ErrorRuntime(format!("DB Error get_circle: {}", e).into(), Position::NONE)))?;
db_module.set_native_fn(
"delete_circle",
move |circle: Circle| -> Result<(), Box<EvalAltResult>> {
// Use the Collection trait method directly
let result = db_clone_delete_circle
.collection::<Circle>()
.expect("can open circle collection")
.delete_by_id(circle.base_data.id)
.expect("can delete circle");
if let Some(first_circle) = all_circles.first() {
Ok(first_circle.clone())
} else {
Err(Box::new(EvalAltResult::ErrorRuntime("Circle not found".into(), Position::NONE)))
}
});
// Return the updated circle with the correct ID
Ok(result)
},
);
let db_clone_get_circle = db.clone();
db_module.set_native_fn(
"get_circle",
move || -> Result<Circle, Box<EvalAltResult>> {
// Use the Collection trait method directly
let all_circles: Vec<Circle> = db_clone_get_circle.get_all().map_err(|e| {
Box::new(EvalAltResult::ErrorRuntime(
format!("DB Error get_circle: {}", e).into(),
Position::NONE,
))
})?;
if let Some(first_circle) = all_circles.first() {
Ok(first_circle.clone())
} else {
Err(Box::new(EvalAltResult::ErrorRuntime(
"Circle not found".into(),
Position::NONE,
)))
}
},
);
let db_clone_get_circle_by_id = db.clone();
db_module.set_native_fn("get_circle_by_id", move |id_i64: INT| -> Result<Circle, Box<EvalAltResult>> {
let id_u32 = id_from_i64_to_u32(id_i64)?;
// Use the Collection trait method directly
db_clone_get_circle_by_id.get_by_id(id_u32)
.map_err(|e| Box::new(EvalAltResult::ErrorRuntime(format!("DB Error get_circle_by_id: {}", e).into(), Position::NONE)))?
.ok_or_else(|| Box::new(EvalAltResult::ErrorRuntime(format!("Circle with ID {} not found", id_u32).into(), Position::NONE)))
});
db_module.set_native_fn(
"get_circle_by_id",
move |id_i64: INT| -> Result<Circle, Box<EvalAltResult>> {
let id_u32 = id_from_i64_to_u32(id_i64)?;
// Use the Collection trait method directly
db_clone_get_circle_by_id
.get_by_id(id_u32)
.map_err(|e| {
Box::new(EvalAltResult::ErrorRuntime(
format!("DB Error get_circle_by_id: {}", e).into(),
Position::NONE,
))
})?
.ok_or_else(|| {
Box::new(EvalAltResult::ErrorRuntime(
format!("Circle with ID {} not found", id_u32).into(),
Position::NONE,
))
})
},
);
// Add list_circles function to get all circles
let db_clone_list_circles = db.clone();
db_module.set_native_fn("list_circles", move || -> Result<Dynamic, Box<EvalAltResult>> {
let collection = db_clone_list_circles.collection::<Circle>()
.map_err(|e| Box::new(EvalAltResult::ErrorRuntime(
format!("Failed to get circle collection: {:?}", e).into(),
Position::NONE
)))?;
let circles = collection.get_all()
.map_err(|e| Box::new(EvalAltResult::ErrorRuntime(
format!("Failed to get all circles: {:?}", e).into(),
Position::NONE
)))?;
let mut array = Array::new();
for circle in circles {
array.push(Dynamic::from(circle));
}
Ok(Dynamic::from(array))
});
db_module.set_native_fn(
"list_circles",
move || -> Result<Dynamic, Box<EvalAltResult>> {
let collection = db_clone_list_circles.collection::<Circle>().map_err(|e| {
Box::new(EvalAltResult::ErrorRuntime(
format!("Failed to get circle collection: {:?}", e).into(),
Position::NONE,
))
})?;
let circles = collection.get_all().map_err(|e| {
Box::new(EvalAltResult::ErrorRuntime(
format!("Failed to get all circles: {:?}", e).into(),
Position::NONE,
))
})?;
let mut array = Array::new();
for circle in circles {
array.push(Dynamic::from(circle));
}
Ok(Dynamic::from(array))
},
);
// Register the database module globally
engine.register_global_module(db_module.into());

View File

@ -112,4 +112,4 @@ impl Group {
self.contacts.push(contact);
self
}
}
}

View File

@ -1,23 +1,23 @@
use rhai::plugin::*;
use rhai::{Engine, EvalAltResult, Position, Module, INT, Dynamic, Array};
use std::sync::Arc;
use std::mem;
use crate::db::Db;
use rhai::plugin::*;
use rhai::{Array, Dynamic, Engine, EvalAltResult, INT, Module, Position};
use std::mem;
use std::sync::Arc;
use super::contact::{Group, Contact};
use super::contact::{Contact, Group};
type RhaiGroup = Group;
type RhaiContact = Contact;
use crate::db::hero::OurDB;
use crate::db::Collection;
use crate::db::hero::OurDB;
// Helper to convert i64 from Rhai to u32 for IDs
fn id_from_i64_to_u32(id_i64: i64) -> Result<u32, Box<EvalAltResult>> {
u32::try_from(id_i64).map_err(|_|
u32::try_from(id_i64).map_err(|_| {
Box::new(EvalAltResult::ErrorArithmetic(
format!("Failed to convert ID '{}' to u32", id_i64).into(),
Position::NONE
Position::NONE,
))
)
})
}
#[export_module]
@ -27,18 +27,24 @@ mod rhai_contact_module {
pub fn new_group() -> RhaiGroup {
Group::new()
}
/// Sets the event title
#[rhai_fn(name = "name", return_raw, global, pure)]
pub fn group_name(group: &mut RhaiGroup, name: String) -> Result<RhaiGroup, Box<EvalAltResult>> {
pub fn group_name(
group: &mut RhaiGroup,
name: String,
) -> Result<RhaiGroup, Box<EvalAltResult>> {
let owned_group = mem::take(group);
*group = owned_group.name(name);
Ok(group.clone())
}
/// Sets the event description
#[rhai_fn(name = "description", return_raw, global, pure)]
pub fn group_description(group: &mut RhaiGroup, description: String) -> Result<RhaiGroup, Box<EvalAltResult>> {
pub fn group_description(
group: &mut RhaiGroup,
description: String,
) -> Result<RhaiGroup, Box<EvalAltResult>> {
let owned_group = mem::take(group);
*group = owned_group.description(description);
Ok(group.clone())
@ -46,7 +52,10 @@ mod rhai_contact_module {
/// Adds an attendee to the event
#[rhai_fn(name = "add_contact", return_raw, global, pure)]
pub fn group_add_contact(group: &mut RhaiGroup, contact_id: i64) -> Result<RhaiGroup, Box<EvalAltResult>> {
pub fn group_add_contact(
group: &mut RhaiGroup,
contact_id: i64,
) -> Result<RhaiGroup, Box<EvalAltResult>> {
// Use take to get ownership of the event
let owned_group = mem::take(group);
*group = owned_group.add_contact(contact_id as u32);
@ -54,21 +63,37 @@ mod rhai_contact_module {
}
#[rhai_fn(get = "contacts", pure)]
pub fn get_group_contacts(group: &mut RhaiGroup) -> Vec<i64> { group.contacts.clone().into_iter().map(|id| id as i64).collect() }
pub fn get_group_contacts(group: &mut RhaiGroup) -> Vec<i64> {
group
.contacts
.clone()
.into_iter()
.map(|id| id as i64)
.collect()
}
// Group Getters
#[rhai_fn(get = "id", pure)]
pub fn get_group_id(group: &mut RhaiGroup) -> i64 { group.base_data.id as i64 }
pub fn get_group_id(group: &mut RhaiGroup) -> i64 {
group.base_data.id as i64
}
#[rhai_fn(get = "created_at", pure)]
pub fn get_group_created_at(group: &mut RhaiGroup) -> i64 { group.base_data.created_at }
pub fn get_group_created_at(group: &mut RhaiGroup) -> i64 {
group.base_data.created_at
}
#[rhai_fn(get = "modified_at", pure)]
pub fn get_group_modified_at(group: &mut RhaiGroup) -> i64 { group.base_data.modified_at }
#[rhai_fn(get = "name", pure)]
pub fn get_group_name(group: &mut RhaiGroup) -> String { group.name.clone() }
#[rhai_fn(get = "description", pure)]
pub fn get_group_description(group: &mut RhaiGroup) -> Option<String> { group.description.clone() }
pub fn get_group_modified_at(group: &mut RhaiGroup) -> i64 {
group.base_data.modified_at
}
#[rhai_fn(get = "name", pure)]
pub fn get_group_name(group: &mut RhaiGroup) -> String {
group.name.clone()
}
#[rhai_fn(get = "description", pure)]
pub fn get_group_description(group: &mut RhaiGroup) -> Option<String> {
group.description.clone()
}
// --- Contact Functions ---
#[rhai_fn(name = "new_contact", return_raw)]
@ -76,25 +101,31 @@ mod rhai_contact_module {
let contact = Contact::new();
Ok(contact)
}
/// Sets the contact name
#[rhai_fn(name = "name", return_raw, global, pure)]
pub fn contact_name(contact: &mut RhaiContact, name: String) -> Result<RhaiContact, Box<EvalAltResult>> {
pub fn contact_name(
contact: &mut RhaiContact,
name: String,
) -> Result<RhaiContact, Box<EvalAltResult>> {
// Create a default Contact to replace the taken one
let default_contact = Contact::new();
// Take ownership of the contact, apply the builder method, then put it back
let owned_contact = std::mem::replace(contact, default_contact);
*contact = owned_contact.name(name);
Ok(contact.clone())
}
/// Sets the contact description
#[rhai_fn(name = "description", return_raw, global, pure)]
pub fn contact_description(contact: &mut RhaiContact, description: String) -> Result<RhaiContact, Box<EvalAltResult>> {
pub fn contact_description(
contact: &mut RhaiContact,
description: String,
) -> Result<RhaiContact, Box<EvalAltResult>> {
// Create a default Contact to replace the taken one
let default_contact = Contact::new();
// Take ownership of the contact, apply the builder method, then put it back
let owned_contact = std::mem::replace(contact, default_contact);
*contact = owned_contact.description(description);
@ -103,130 +134,200 @@ mod rhai_contact_module {
// Contact Getters
#[rhai_fn(get = "id", pure)]
pub fn get_contact_id(contact: &mut RhaiContact) -> i64 { contact.base_data.id as i64 }
pub fn get_contact_id(contact: &mut RhaiContact) -> i64 {
contact.base_data.id as i64
}
#[rhai_fn(get = "name", pure)]
pub fn get_contact_name(contact: &mut RhaiContact) -> String { contact.name.clone() }
pub fn get_contact_name(contact: &mut RhaiContact) -> String {
contact.name.clone()
}
#[rhai_fn(get = "created_at", pure)]
pub fn get_contact_created_at(contact: &mut RhaiContact) -> i64 { contact.base_data.created_at }
pub fn get_contact_created_at(contact: &mut RhaiContact) -> i64 {
contact.base_data.created_at
}
#[rhai_fn(get = "modified_at", pure)]
pub fn get_contact_modified_at(contact: &mut RhaiContact) -> i64 { contact.base_data.modified_at }
pub fn get_contact_modified_at(contact: &mut RhaiContact) -> i64 {
contact.base_data.modified_at
}
}
pub fn register_contact_rhai_module(engine: &mut Engine, db: Arc<OurDB>) {
// Register the exported module globally
let module = exported_module!(rhai_contact_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_group = db.clone();
db_module.set_native_fn("save_group", move |group: Group| -> Result<Group, Box<EvalAltResult>> {
// Use the Collection trait method directly
let result = db_clone_set_group.set(&group)
.map_err(|e| Box::new(EvalAltResult::ErrorRuntime(format!("DB Error set_group: {}", e).into(), Position::NONE)))?;
// Return the updated event with the correct ID
Ok(result.1)
});
db_module.set_native_fn(
"save_group",
move |group: Group| -> Result<Group, Box<EvalAltResult>> {
// Use the Collection trait method directly
let result = db_clone_set_group.set(&group).map_err(|e| {
Box::new(EvalAltResult::ErrorRuntime(
format!("DB Error set_group: {}", 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_group = db.clone();
db_module.set_native_fn("delete_group", move |group: Group| -> Result<(), Box<EvalAltResult>> {
// Use the Collection trait method directly
let result = db_clone_delete_group.collection::<Group>()
.expect("can open group collection")
.delete_by_id(group.base_data.id)
.expect("can delete group");
// Return the updated event with the correct ID
Ok(result)
});
db_module.set_native_fn(
"delete_group",
move |group: Group| -> Result<(), Box<EvalAltResult>> {
// Use the Collection trait method directly
let result = db_clone_delete_group
.collection::<Group>()
.expect("can open group collection")
.delete_by_id(group.base_data.id)
.expect("can delete group");
// Return the updated event with the correct ID
Ok(result)
},
);
let db_clone_get_group = db.clone();
db_module.set_native_fn("get_group_by_id", move |id_i64: INT| -> Result<Group, Box<EvalAltResult>> {
let id_u32 = id_from_i64_to_u32(id_i64)?;
// Use the Collection trait method directly
db_clone_get_group.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)))
});
db_module.set_native_fn(
"get_group_by_id",
move |id_i64: INT| -> Result<Group, Box<EvalAltResult>> {
let id_u32 = id_from_i64_to_u32(id_i64)?;
// Use the Collection trait method directly
db_clone_get_group
.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_contact = db.clone();
db_module.set_native_fn("save_contact", move |contact: Contact| -> Result<Contact, Box<EvalAltResult>> {
// Use the Collection trait method directly
let result = db_clone_set_contact.set(&contact)
.map_err(|e| Box::new(EvalAltResult::ErrorRuntime(format!("DB Error set_contact: {}", e).into(), Position::NONE)))?;
// Return the updated contact with the correct ID
Ok(result.1)
});
db_module.set_native_fn(
"save_contact",
move |contact: Contact| -> Result<Contact, Box<EvalAltResult>> {
// Use the Collection trait method directly
let result = db_clone_set_contact.set(&contact).map_err(|e| {
Box::new(EvalAltResult::ErrorRuntime(
format!("DB Error set_contact: {}", e).into(),
Position::NONE,
))
})?;
// Return the updated contact with the correct ID
Ok(result.1)
},
);
// Manually register database functions as they need to capture 'db'
let db_clone_delete_contact = db.clone();
db_module.set_native_fn("delete_contact", move |contact: Contact| -> Result<(), Box<EvalAltResult>> {
// Use the Collection trait method directly
let result = db_clone_delete_contact.collection::<Contact>()
.expect("can open contact collection")
.delete_by_id(contact.base_data.id)
.expect("can delete event");
// Return the updated event with the correct ID
Ok(result)
});
db_module.set_native_fn(
"delete_contact",
move |contact: Contact| -> Result<(), Box<EvalAltResult>> {
// Use the Collection trait method directly
let result = db_clone_delete_contact
.collection::<Contact>()
.expect("can open contact collection")
.delete_by_id(contact.base_data.id)
.expect("can delete event");
// Return the updated event with the correct ID
Ok(result)
},
);
let db_clone_get_contact = db.clone();
db_module.set_native_fn("get_contact_by_id", move |id_i64: INT| -> Result<Contact, Box<EvalAltResult>> {
let id_u32 = id_from_i64_to_u32(id_i64)?;
// Use the Collection trait method directly
db_clone_get_contact.get_by_id(id_u32)
.map_err(|e| Box::new(EvalAltResult::ErrorRuntime(format!("DB Error get_contact_by_id: {}", e).into(), Position::NONE)))?
.ok_or_else(|| Box::new(EvalAltResult::ErrorRuntime(format!("Contact with ID {} not found", id_u32).into(), Position::NONE)))
});
db_module.set_native_fn(
"get_contact_by_id",
move |id_i64: INT| -> Result<Contact, Box<EvalAltResult>> {
let id_u32 = id_from_i64_to_u32(id_i64)?;
// Use the Collection trait method directly
db_clone_get_contact
.get_by_id(id_u32)
.map_err(|e| {
Box::new(EvalAltResult::ErrorRuntime(
format!("DB Error get_contact_by_id: {}", e).into(),
Position::NONE,
))
})?
.ok_or_else(|| {
Box::new(EvalAltResult::ErrorRuntime(
format!("Contact with ID {} not found", id_u32).into(),
Position::NONE,
))
})
},
);
// Add list_contacts function to get all contacts
let db_clone_list_contacts = db.clone();
db_module.set_native_fn("list_contacts", move || -> Result<Dynamic, Box<EvalAltResult>> {
let collection = db_clone_list_contacts.collection::<Contact>()
.map_err(|e| Box::new(EvalAltResult::ErrorRuntime(
format!("Failed to get contact collection: {:?}", e).into(),
Position::NONE
)))?;
let contacts = collection.get_all()
.map_err(|e| Box::new(EvalAltResult::ErrorRuntime(
format!("Failed to get all contacts: {:?}", e).into(),
Position::NONE
)))?;
let mut array = Array::new();
for contact in contacts {
array.push(Dynamic::from(contact));
}
Ok(Dynamic::from(array))
});
db_module.set_native_fn(
"list_contacts",
move || -> Result<Dynamic, Box<EvalAltResult>> {
let collection = db_clone_list_contacts
.collection::<Contact>()
.map_err(|e| {
Box::new(EvalAltResult::ErrorRuntime(
format!("Failed to get contact collection: {:?}", e).into(),
Position::NONE,
))
})?;
let contacts = collection.get_all().map_err(|e| {
Box::new(EvalAltResult::ErrorRuntime(
format!("Failed to get all contacts: {:?}", e).into(),
Position::NONE,
))
})?;
let mut array = Array::new();
for contact in contacts {
array.push(Dynamic::from(contact));
}
Ok(Dynamic::from(array))
},
);
// Add list_events function to get all events
let db_clone_list_groups = db.clone();
db_module.set_native_fn("list_groups", move || -> Result<Dynamic, Box<EvalAltResult>> {
let collection = db_clone_list_groups.collection::<Group>()
.map_err(|e| Box::new(EvalAltResult::ErrorRuntime(
format!("Failed to get group collection: {:?}", e).into(),
Position::NONE
)))?;
let groups = collection.get_all()
.map_err(|e| Box::new(EvalAltResult::ErrorRuntime(
format!("Failed to get all groups: {:?}", e).into(),
Position::NONE
)))?;
let mut array = Array::new();
for group in groups {
array.push(Dynamic::from(group));
}
Ok(Dynamic::from(array))
});
db_module.set_native_fn(
"list_groups",
move || -> Result<Dynamic, Box<EvalAltResult>> {
let collection = db_clone_list_groups.collection::<Group>().map_err(|e| {
Box::new(EvalAltResult::ErrorRuntime(
format!("Failed to get group collection: {:?}", e).into(),
Position::NONE,
))
})?;
let groups = collection.get_all().map_err(|e| {
Box::new(EvalAltResult::ErrorRuntime(
format!("Failed to get all groups: {:?}", e).into(),
Position::NONE,
))
})?;
let mut array = Array::new();
for group in groups {
array.push(Dynamic::from(group));
}
Ok(Dynamic::from(array))
},
);
// Register the database module globally
engine.register_global_module(db_module.into());

View File

@ -8,7 +8,7 @@ use serde::{Deserialize, Serialize};
pub struct Comment {
pub base_data: BaseModelData, // Provides id, created_at, updated_at
#[index]
pub user_id: u32, // Maps to commenter_id
pub user_id: u32, // Maps to commenter_id
pub content: String, // Maps to text
pub parent_comment_id: Option<u32>, // For threaded comments
}

View File

@ -4,4 +4,3 @@ pub mod model;
// Re-export key types for convenience
pub use comment::Comment;

View File

@ -0,0 +1 @@

View File

@ -1,9 +1,9 @@
// heromodels/src/models/finance/account.rs
use serde::{Deserialize, Serialize};
use rhai::{CustomType, TypeBuilder};
use heromodels_derive::model;
use heromodels_core::BaseModelData;
use heromodels_derive::model;
use rhai::{CustomType, TypeBuilder};
use serde::{Deserialize, Serialize};
use super::asset::Asset;
@ -18,7 +18,7 @@ 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<u32>, // list of assets in this account
pub assets: Vec<u32>, // list of assets in this account
}
impl Account {
@ -87,6 +87,6 @@ impl Account {
/// Find an asset by name
pub fn find_asset_by_name(&self, _name: &str) -> Option<&Asset> {
// TODO: implement
return None
return None;
}
}

View File

@ -1,9 +1,9 @@
// heromodels/src/models/finance/asset.rs
use serde::{Deserialize, Serialize};
use rhai::{CustomType, TypeBuilder};
use heromodels_derive::model;
use heromodels_core::BaseModelData;
use heromodels_derive::model;
use rhai::{CustomType, TypeBuilder};
use serde::{Deserialize, Serialize};
/// AssetType defines the type of blockchain asset
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]

View File

@ -1,10 +1,10 @@
// heromodels/src/models/finance/marketplace.rs
use serde::{Deserialize, Serialize};
use rhai::{CustomType, TypeBuilder};
use chrono::{DateTime, Utc};
use heromodels_core::BaseModelData;
use heromodels_derive::model;
use rhai::{CustomType, TypeBuilder};
use serde::{Deserialize, Serialize};
use super::asset::AssetType;
@ -53,8 +53,7 @@ impl Default for BidStatus {
}
/// Bid represents a bid on an auction listing
#[derive(Debug, Clone, Serialize, Deserialize, CustomType)]
#[derive(Default)]
#[derive(Debug, Clone, Serialize, Deserialize, CustomType, 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
@ -272,7 +271,7 @@ impl Listing {
}
self.status = ListingStatus::Sold;
if self.sold_at.is_none() {
self.sold_at = Some(Utc::now());
}
@ -281,7 +280,7 @@ impl Listing {
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() == buyer_id_str && bid.amount == sale_price_val {
bid.status = BidStatus::Accepted;

View File

@ -9,4 +9,4 @@ pub mod rhai;
// Re-export main models for easier access
pub use self::account::Account;
pub use self::asset::{Asset, AssetType};
pub use self::marketplace::{Listing, ListingStatus, ListingType, Bid, BidStatus};
pub use self::marketplace::{Bid, BidStatus, Listing, ListingStatus, ListingType};

View File

@ -1,12 +1,12 @@
use rhai::plugin::*;
use rhai::{Engine, EvalAltResult, Position, Module, INT, Dynamic, Array};
use std::sync::Arc;
use std::mem;
use chrono::Utc;
use rhai::plugin::*;
use rhai::{Array, Dynamic, Engine, EvalAltResult, INT, Module, Position};
use std::mem;
use std::sync::Arc;
use super::account::Account;
use super::asset::{Asset, AssetType};
use super::marketplace::{Listing, Bid, ListingStatus, ListingType, BidStatus};
use super::marketplace::{Bid, BidStatus, Listing, ListingStatus, ListingType};
use crate::db::hero::OurDB;
use crate::db::{Collection, Db};
@ -18,12 +18,12 @@ type RhaiBid = Bid;
// Helper to convert i64 from Rhai to u32 for IDs
fn id_from_i64_to_u32(id_i64: i64) -> Result<u32, Box<EvalAltResult>> {
u32::try_from(id_i64).map_err(|_|
u32::try_from(id_i64).map_err(|_| {
Box::new(EvalAltResult::ErrorArithmetic(
format!("Failed to convert ID '{}' to u32", id_i64).into(),
Position::NONE
Position::NONE,
))
)
})
}
// Helper functions for enum conversions
@ -45,8 +45,12 @@ fn string_to_asset_type(s: &str) -> Result<AssetType, Box<EvalAltResult>> {
"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
format!(
"Invalid asset type: '{}'. Expected one of: Native, Erc20, Erc721, Erc1155",
s
)
.into(),
Position::NONE,
))),
}
}
@ -68,8 +72,12 @@ fn string_to_listing_status(s: &str) -> Result<ListingStatus, Box<EvalAltResult>
"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
format!(
"Invalid listing status: '{}'. Expected one of: Active, Sold, Cancelled, Expired",
s
)
.into(),
Position::NONE,
))),
}
}
@ -89,8 +97,12 @@ fn string_to_listing_type(s: &str) -> Result<ListingType, Box<EvalAltResult>> {
"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
format!(
"Invalid listing type: '{}'. Expected one of: FixedPrice, Auction, Exchange",
s
)
.into(),
Position::NONE,
))),
}
}
@ -112,8 +124,12 @@ fn string_to_bid_status(s: &str) -> Result<BidStatus, Box<EvalAltResult>> {
"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
format!(
"Invalid bid status: '{}'. Expected one of: Active, Accepted, Rejected, Cancelled",
s
)
.into(),
Position::NONE,
))),
}
}
@ -181,7 +197,10 @@ mod account_module {
// Setters using builder pattern with proper mutability handling
#[rhai_fn(return_raw, global)]
pub fn name(account: &mut RhaiAccount, name: String) -> Result<RhaiAccount, Box<EvalAltResult>> {
pub fn name(
account: &mut RhaiAccount,
name: String,
) -> Result<RhaiAccount, Box<EvalAltResult>> {
let mut acc = mem::take(account);
acc = acc.name(name);
*account = acc;
@ -189,7 +208,10 @@ mod account_module {
}
#[rhai_fn(return_raw, global)]
pub fn user_id(account: &mut RhaiAccount, user_id: INT) -> Result<RhaiAccount, Box<EvalAltResult>> {
pub fn user_id(
account: &mut RhaiAccount,
user_id: INT,
) -> Result<RhaiAccount, Box<EvalAltResult>> {
let user_id = id_from_i64_to_u32(user_id)?;
let mut acc = mem::take(account);
acc = acc.user_id(user_id);
@ -198,7 +220,10 @@ mod account_module {
}
#[rhai_fn(return_raw, global)]
pub fn description(account: &mut RhaiAccount, description: String) -> Result<RhaiAccount, Box<EvalAltResult>> {
pub fn description(
account: &mut RhaiAccount,
description: String,
) -> Result<RhaiAccount, Box<EvalAltResult>> {
let mut acc = mem::take(account);
acc = acc.description(description);
*account = acc;
@ -206,7 +231,10 @@ mod account_module {
}
#[rhai_fn(return_raw, global)]
pub fn ledger(account: &mut RhaiAccount, ledger: String) -> Result<RhaiAccount, Box<EvalAltResult>> {
pub fn ledger(
account: &mut RhaiAccount,
ledger: String,
) -> Result<RhaiAccount, Box<EvalAltResult>> {
let mut acc = mem::take(account);
acc = acc.ledger(ledger);
*account = acc;
@ -214,7 +242,10 @@ mod account_module {
}
#[rhai_fn(return_raw, global)]
pub fn address(account: &mut RhaiAccount, address: String) -> Result<RhaiAccount, Box<EvalAltResult>> {
pub fn address(
account: &mut RhaiAccount,
address: String,
) -> Result<RhaiAccount, Box<EvalAltResult>> {
let mut acc = mem::take(account);
acc = acc.address(address);
*account = acc;
@ -222,7 +253,10 @@ mod account_module {
}
#[rhai_fn(return_raw, global)]
pub fn pubkey(account: &mut RhaiAccount, pubkey: String) -> Result<RhaiAccount, Box<EvalAltResult>> {
pub fn pubkey(
account: &mut RhaiAccount,
pubkey: String,
) -> Result<RhaiAccount, Box<EvalAltResult>> {
let mut acc = mem::take(account);
acc = acc.pubkey(pubkey);
*account = acc;
@ -230,7 +264,10 @@ mod account_module {
}
#[rhai_fn(return_raw, global)]
pub fn add_asset(account: &mut RhaiAccount, asset_id: INT) -> Result<RhaiAccount, Box<EvalAltResult>> {
pub fn add_asset(
account: &mut RhaiAccount,
asset_id: INT,
) -> Result<RhaiAccount, Box<EvalAltResult>> {
let asset_id = id_from_i64_to_u32(asset_id)?;
let mut acc = mem::take(account);
acc = acc.add_asset(asset_id);
@ -301,7 +338,10 @@ mod asset_module {
}
#[rhai_fn(return_raw, global)]
pub fn description(asset: &mut RhaiAsset, description: String) -> Result<RhaiAsset, Box<EvalAltResult>> {
pub fn description(
asset: &mut RhaiAsset,
description: String,
) -> Result<RhaiAsset, Box<EvalAltResult>> {
let mut ast = mem::take(asset);
ast = ast.description(description);
*asset = ast;
@ -317,7 +357,10 @@ mod asset_module {
}
#[rhai_fn(return_raw, global)]
pub fn address(asset: &mut RhaiAsset, address: String) -> Result<RhaiAsset, Box<EvalAltResult>> {
pub fn address(
asset: &mut RhaiAsset,
address: String,
) -> Result<RhaiAsset, Box<EvalAltResult>> {
let mut ast = mem::take(asset);
ast = ast.address(address);
*asset = ast;
@ -325,7 +368,10 @@ mod asset_module {
}
#[rhai_fn(return_raw, global)]
pub fn asset_type(asset: &mut RhaiAsset, asset_type_str: String) -> Result<RhaiAsset, Box<EvalAltResult>> {
pub fn asset_type(
asset: &mut RhaiAsset,
asset_type_str: String,
) -> Result<RhaiAsset, Box<EvalAltResult>> {
let asset_type_enum = string_to_asset_type(&asset_type_str)?;
let mut ast = mem::take(asset);
ast = ast.asset_type(asset_type_enum);
@ -338,7 +384,7 @@ mod asset_module {
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
Position::NONE,
)));
}
let mut ast = mem::take(asset);
@ -441,7 +487,10 @@ mod listing_module {
// Setters using builder pattern with proper mutability handling
#[rhai_fn(return_raw, global)]
pub fn seller_id(listing: &mut RhaiListing, seller_id: INT) -> Result<RhaiListing, Box<EvalAltResult>> {
pub fn seller_id(
listing: &mut RhaiListing,
seller_id: INT,
) -> Result<RhaiListing, Box<EvalAltResult>> {
let seller_id = id_from_i64_to_u32(seller_id)?;
let mut lst = mem::take(listing);
lst = lst.seller_id(seller_id);
@ -450,7 +499,10 @@ mod listing_module {
}
#[rhai_fn(return_raw, global)]
pub fn asset_id(listing: &mut RhaiListing, asset_id: INT) -> Result<RhaiListing, Box<EvalAltResult>> {
pub fn asset_id(
listing: &mut RhaiListing,
asset_id: INT,
) -> Result<RhaiListing, Box<EvalAltResult>> {
let asset_id = id_from_i64_to_u32(asset_id)?;
let mut lst = mem::take(listing);
lst = lst.asset_id(asset_id);
@ -467,7 +519,10 @@ mod listing_module {
}
#[rhai_fn(return_raw, global)]
pub fn currency(listing: &mut RhaiListing, currency: String) -> Result<RhaiListing, Box<EvalAltResult>> {
pub fn currency(
listing: &mut RhaiListing,
currency: String,
) -> Result<RhaiListing, Box<EvalAltResult>> {
let mut lst = mem::take(listing);
lst = lst.currency(currency);
*listing = lst;
@ -475,7 +530,10 @@ mod listing_module {
}
#[rhai_fn(return_raw, global)]
pub fn listing_type(listing: &mut RhaiListing, listing_type_str: String) -> Result<RhaiListing, Box<EvalAltResult>> {
pub fn listing_type(
listing: &mut RhaiListing,
listing_type_str: String,
) -> Result<RhaiListing, Box<EvalAltResult>> {
let listing_type_enum = string_to_listing_type(&listing_type_str)?;
let mut lst = mem::take(listing);
lst = lst.listing_type(listing_type_enum);
@ -484,7 +542,10 @@ mod listing_module {
}
#[rhai_fn(return_raw, global)]
pub fn title(listing: &mut RhaiListing, title: String) -> Result<RhaiListing, Box<EvalAltResult>> {
pub fn title(
listing: &mut RhaiListing,
title: String,
) -> Result<RhaiListing, Box<EvalAltResult>> {
let mut lst = mem::take(listing);
lst = lst.title(title);
*listing = lst;
@ -492,7 +553,10 @@ mod listing_module {
}
#[rhai_fn(return_raw, global)]
pub fn description(listing: &mut RhaiListing, description: String) -> Result<RhaiListing, Box<EvalAltResult>> {
pub fn description(
listing: &mut RhaiListing,
description: String,
) -> Result<RhaiListing, Box<EvalAltResult>> {
let mut lst = mem::take(listing);
lst = lst.description(description);
*listing = lst;
@ -500,7 +564,10 @@ mod listing_module {
}
#[rhai_fn(return_raw, global)]
pub fn image_url(listing: &mut RhaiListing, image_url: String) -> Result<RhaiListing, Box<EvalAltResult>> {
pub fn image_url(
listing: &mut RhaiListing,
image_url: String,
) -> Result<RhaiListing, Box<EvalAltResult>> {
let mut lst = mem::take(listing);
lst = lst.image_url(Some(image_url));
*listing = lst;
@ -508,15 +575,21 @@ mod listing_module {
}
#[rhai_fn(return_raw, global)]
pub fn expires_at(listing: &mut RhaiListing, end_date_ts: INT) -> Result<RhaiListing, Box<EvalAltResult>> {
pub fn expires_at(
listing: &mut RhaiListing,
end_date_ts: INT,
) -> Result<RhaiListing, Box<EvalAltResult>> {
use chrono::TimeZone;
let end_date = chrono::Utc.timestamp_opt(end_date_ts, 0)
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
)))?;
.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;
@ -524,7 +597,10 @@ mod listing_module {
}
#[rhai_fn(return_raw, global)]
pub fn add_tag(listing: &mut RhaiListing, tag: String) -> Result<RhaiListing, Box<EvalAltResult>> {
pub fn add_tag(
listing: &mut RhaiListing,
tag: String,
) -> Result<RhaiListing, Box<EvalAltResult>> {
let mut lst = mem::take(listing);
lst = lst.add_tag(tag);
*listing = lst;
@ -532,44 +608,50 @@ mod listing_module {
}
#[rhai_fn(return_raw, global)]
pub fn add_bid(listing: &mut RhaiListing, bid: RhaiBid) -> Result<RhaiListing, Box<EvalAltResult>> {
pub fn add_bid(
listing: &mut RhaiListing,
bid: RhaiBid,
) -> Result<RhaiListing, Box<EvalAltResult>> {
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
Position::NONE,
)))
}
}
}
#[rhai_fn(return_raw, global)]
pub fn complete_sale(listing: &mut RhaiListing, buyer_id: INT) -> Result<RhaiListing, Box<EvalAltResult>> {
pub fn complete_sale(
listing: &mut RhaiListing,
buyer_id: INT,
) -> Result<RhaiListing, Box<EvalAltResult>> {
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
Position::NONE,
)))
}
}
@ -582,13 +664,13 @@ mod listing_module {
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
Position::NONE,
)))
}
}
@ -647,7 +729,10 @@ mod bid_module {
// Setters using builder pattern with proper mutability handling
#[rhai_fn(return_raw, global)]
pub fn listing_id(bid: &mut RhaiBid, listing_id: String) -> Result<RhaiBid, Box<EvalAltResult>> {
pub fn listing_id(
bid: &mut RhaiBid,
listing_id: String,
) -> Result<RhaiBid, Box<EvalAltResult>> {
let mut b = mem::take(bid);
b = b.listing_id(listing_id);
*bid = b;
@ -680,7 +765,10 @@ mod bid_module {
}
#[rhai_fn(return_raw, global)]
pub fn update_status(bid: &mut RhaiBid, status_str: String) -> Result<RhaiBid, Box<EvalAltResult>> {
pub fn update_status(
bid: &mut RhaiBid,
status_str: String,
) -> Result<RhaiBid, Box<EvalAltResult>> {
let status = string_to_bid_status(&status_str)?;
let mut b = mem::take(bid);
b = b.status(status);
@ -706,13 +794,21 @@ pub fn register_finance_rhai_module(engine: &mut Engine, db: Arc<OurDB>) {
engine.register_global_module(bid_module.into());
// --- 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("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()));
engine.register_fn("str_to_listing_status", |s: ImmutableString| {
self::string_to_listing_status(s.as_str())
});
engine.register_fn("listing_status_to_str", self::listing_status_to_string);
engine.register_fn("str_to_listing_type", |s: ImmutableString| self::string_to_listing_type(s.as_str()));
engine.register_fn("str_to_listing_type", |s: ImmutableString| {
self::string_to_listing_type(s.as_str())
});
engine.register_fn("listing_type_to_str", self::listing_type_to_string);
engine.register_fn("str_to_bid_status", |s: ImmutableString| self::string_to_bid_status(s.as_str()));
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);
// --- Database interaction functions (registered in a separate db_module) ---
@ -720,68 +816,158 @@ pub fn register_finance_rhai_module(engine: &mut Engine, db: Arc<OurDB>) {
// Account DB functions
let db_set_account = Arc::clone(&db);
db_module.set_native_fn("set_account", move |account: Account| -> Result<INT, Box<EvalAltResult>> {
let collection = db_set_account.collection::<Account>().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)))
});
db_module.set_native_fn(
"set_account",
move |account: Account| -> Result<INT, Box<EvalAltResult>> {
let collection = db_set_account.collection::<Account>().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 db_get_account = Arc::clone(&db);
db_module.set_native_fn("get_account_by_id", move |id_rhai: INT| -> Result<Dynamic, Box<EvalAltResult>> {
let id_u32 = id_from_i64_to_u32(id_rhai)?;
let collection = db_get_account.collection::<Account>().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)))
});
db_module.set_native_fn(
"get_account_by_id",
move |id_rhai: INT| -> Result<Dynamic, Box<EvalAltResult>> {
let id_u32 = id_from_i64_to_u32(id_rhai)?;
let collection = db_get_account.collection::<Account>().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,
))
})
},
);
// Asset DB functions
let db_set_asset = Arc::clone(&db);
db_module.set_native_fn("set_asset", move |asset: Asset| -> Result<INT, Box<EvalAltResult>> {
let collection = db_set_asset.collection::<Asset>().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)))
});
db_module.set_native_fn(
"set_asset",
move |asset: Asset| -> Result<INT, Box<EvalAltResult>> {
let collection = db_set_asset.collection::<Asset>().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 db_get_asset = Arc::clone(&db);
db_module.set_native_fn("get_asset_by_id", move |id_rhai: INT| -> Result<Dynamic, Box<EvalAltResult>> {
let id_u32 = id_from_i64_to_u32(id_rhai)?;
let collection = db_get_asset.collection::<Asset>().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)))
});
db_module.set_native_fn(
"get_asset_by_id",
move |id_rhai: INT| -> Result<Dynamic, Box<EvalAltResult>> {
let id_u32 = id_from_i64_to_u32(id_rhai)?;
let collection = db_get_asset.collection::<Asset>().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,
))
})
},
);
// Listing DB functions
let db_set_listing = Arc::clone(&db);
db_module.set_native_fn("set_listing", move |listing: Listing| -> Result<INT, Box<EvalAltResult>> {
let collection = db_set_listing.collection::<Listing>().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)))
});
db_module.set_native_fn(
"set_listing",
move |listing: Listing| -> Result<INT, Box<EvalAltResult>> {
let collection = db_set_listing.collection::<Listing>().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 db_get_listing = Arc::clone(&db);
db_module.set_native_fn("get_listing_by_id", move |id_rhai: INT| -> Result<Dynamic, Box<EvalAltResult>> {
let id_u32 = id_from_i64_to_u32(id_rhai)?;
let collection = db_get_listing.collection::<Listing>().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)))
});
db_module.set_native_fn(
"get_listing_by_id",
move |id_rhai: INT| -> Result<Dynamic, Box<EvalAltResult>> {
let id_u32 = id_from_i64_to_u32(id_rhai)?;
let collection = db_get_listing.collection::<Listing>().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.");
}

View File

@ -1,7 +1,7 @@
use super::flow_step::FlowStep;
use heromodels_core::BaseModelData;
use heromodels_derive::model;
use serde::{Deserialize, Serialize};
use super::flow_step::FlowStep;
/// Represents a signing flow.
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, Default)]
@ -33,7 +33,7 @@ impl Flow {
Self {
base_data: BaseModelData::new(),
flow_uuid: flow_uuid.to_string(),
name: String::new(), // Default name, to be set by builder
name: String::new(), // Default name, to be set by builder
status: String::from("Pending"), // Default status, to be set by builder
steps: Vec::new(),
}

View File

@ -1,11 +1,11 @@
// Export flow model submodules
pub mod flow;
pub mod flow_step;
pub mod signature_requirement;
pub mod rhai;
pub mod signature_requirement;
// Re-export key types for convenience
pub use flow::Flow;
pub use flow_step::FlowStep;
pub use signature_requirement::SignatureRequirement;
pub use rhai::register_flow_rhai_module;
pub use signature_requirement::SignatureRequirement;

View File

@ -1,7 +1,7 @@
use rhai::plugin::*;
use rhai::{Engine, EvalAltResult, Position, Module, INT, Dynamic, Array};
use std::sync::Arc;
use rhai::{Array, Dynamic, Engine, EvalAltResult, INT, Module, Position};
use std::mem;
use std::sync::Arc;
use super::flow::Flow;
use super::flow_step::FlowStep;
@ -9,18 +9,18 @@ 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 crate::db::hero::OurDB;
// Helper to convert i64 from Rhai to u32 for IDs
fn id_from_i64_to_u32(id_i64: i64) -> Result<u32, Box<EvalAltResult>> {
u32::try_from(id_i64).map_err(|_|
u32::try_from(id_i64).map_err(|_| {
Box::new(EvalAltResult::ErrorArithmetic(
format!("Failed to convert ID '{}' to u32", id_i64).into(),
Position::NONE
Position::NONE,
))
)
})
}
#[export_module]
@ -30,7 +30,7 @@ mod rhai_flow_module {
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<RhaiFlow, Box<EvalAltResult>> {
@ -38,41 +38,63 @@ mod rhai_flow_module {
*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<RhaiFlow, Box<EvalAltResult>> {
pub fn flow_status(
flow: &mut RhaiFlow,
status: String,
) -> Result<RhaiFlow, Box<EvalAltResult>> {
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<RhaiFlow, Box<EvalAltResult>> {
pub fn flow_add_step(
flow: &mut RhaiFlow,
step: RhaiFlowStep,
) -> Result<RhaiFlow, Box<EvalAltResult>> {
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 }
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 }
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 }
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() }
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() }
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() }
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::<Array>()
flow.steps
.iter()
.cloned()
.map(Dynamic::from)
.collect::<Array>()
}
// --- FlowStep Functions ---
#[rhai_fn(global)]
pub fn new_flow_step(step_order_i64: i64) -> Dynamic {
@ -81,34 +103,46 @@ mod rhai_flow_module {
let mut flow_step = FlowStep::default();
flow_step.step_order = step_order;
Dynamic::from(flow_step)
},
Err(err) => Dynamic::from(err.to_string())
}
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<RhaiFlowStep, Box<EvalAltResult>> {
pub fn flow_step_description(
step: &mut RhaiFlowStep,
description: String,
) -> Result<RhaiFlowStep, Box<EvalAltResult>> {
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<RhaiFlowStep, Box<EvalAltResult>> {
pub fn flow_step_status(
step: &mut RhaiFlowStep,
status: String,
) -> Result<RhaiFlowStep, Box<EvalAltResult>> {
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 }
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 }
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 }
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 {
@ -117,14 +151,22 @@ mod rhai_flow_module {
}
}
#[rhai_fn(get = "step_order", pure)]
pub fn get_step_order(step: &mut RhaiFlowStep) -> i64 { step.step_order as i64 }
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() }
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 {
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();
@ -132,48 +174,69 @@ mod rhai_flow_module {
signature_requirement.public_key = public_key;
signature_requirement.message = message;
Dynamic::from(signature_requirement)
},
Err(err) => Dynamic::from(err.to_string())
}
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<RhaiSignatureRequirement, Box<EvalAltResult>> {
pub fn signature_requirement_signed_by(
sr: &mut RhaiSignatureRequirement,
signed_by: String,
) -> Result<RhaiSignatureRequirement, Box<EvalAltResult>> {
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<RhaiSignatureRequirement, Box<EvalAltResult>> {
pub fn signature_requirement_signature(
sr: &mut RhaiSignatureRequirement,
signature: String,
) -> Result<RhaiSignatureRequirement, Box<EvalAltResult>> {
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<RhaiSignatureRequirement, Box<EvalAltResult>> {
pub fn signature_requirement_status(
sr: &mut RhaiSignatureRequirement,
status: String,
) -> Result<RhaiSignatureRequirement, Box<EvalAltResult>> {
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 }
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 }
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 }
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 }
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() }
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() }
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 {
@ -189,185 +252,284 @@ mod rhai_flow_module {
}
}
#[rhai_fn(get = "status", pure)]
pub fn get_sr_status(sr: &mut RhaiSignatureRequirement) -> String { sr.status.clone() }
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<OurDB>) {
// 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<Flow, Box<EvalAltResult>> {
// 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)
});
db_module.set_native_fn(
"save_flow",
move |flow: Flow| -> Result<Flow, Box<EvalAltResult>> {
// 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)
},
);
let db_clone = Arc::clone(&db);
db_module.set_native_fn("get_flow_by_id", move |id_i64: INT| -> Result<Flow, Box<EvalAltResult>> {
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)))
});
db_module.set_native_fn(
"get_flow_by_id",
move |id_i64: INT| -> Result<Flow, Box<EvalAltResult>> {
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,
))
})
},
);
let db_clone = Arc::clone(&db);
db_module.set_native_fn("delete_flow", move |id_i64: INT| -> Result<(), Box<EvalAltResult>> {
let id_u32 = id_from_i64_to_u32(id_i64)?;
// Use the Collection trait method directly
let collection = db_clone.collection::<Flow>()
.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
)))
});
db_module.set_native_fn(
"delete_flow",
move |id_i64: INT| -> Result<(), Box<EvalAltResult>> {
let id_u32 = id_from_i64_to_u32(id_i64)?;
// Use the Collection trait method directly
let collection = db_clone.collection::<Flow>().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 db_clone = Arc::clone(&db);
db_module.set_native_fn("list_flows", move || -> Result<Dynamic, Box<EvalAltResult>> {
let collection = db_clone.collection::<Flow>()
.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))
});
db_module.set_native_fn(
"list_flows",
move || -> Result<Dynamic, Box<EvalAltResult>> {
let collection = db_clone.collection::<Flow>().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<FlowStep, Box<EvalAltResult>> {
// 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)
});
db_module.set_native_fn(
"save_flow_step",
move |step: FlowStep| -> Result<FlowStep, Box<EvalAltResult>> {
// 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<FlowStep, Box<EvalAltResult>> {
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)))
});
db_module.set_native_fn(
"get_flow_step_by_id",
move |id_i64: INT| -> Result<FlowStep, Box<EvalAltResult>> {
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<EvalAltResult>> {
let id_u32 = id_from_i64_to_u32(id_i64)?;
// Use the Collection trait method directly
let collection = db_clone.collection::<FlowStep>()
.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
)))
});
db_module.set_native_fn(
"delete_flow_step",
move |id_i64: INT| -> Result<(), Box<EvalAltResult>> {
let id_u32 = id_from_i64_to_u32(id_i64)?;
// Use the Collection trait method directly
let collection = db_clone.collection::<FlowStep>().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<Dynamic, Box<EvalAltResult>> {
let collection = db_clone.collection::<FlowStep>()
.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))
});
db_module.set_native_fn(
"list_flow_steps",
move || -> Result<Dynamic, Box<EvalAltResult>> {
let collection = db_clone.collection::<FlowStep>().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<SignatureRequirement, Box<EvalAltResult>> {
// 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)
});
db_module.set_native_fn(
"save_signature_requirement",
move |sr: SignatureRequirement| -> Result<SignatureRequirement, Box<EvalAltResult>> {
// 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<SignatureRequirement, Box<EvalAltResult>> {
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)))
});
db_module.set_native_fn(
"get_signature_requirement_by_id",
move |id_i64: INT| -> Result<SignatureRequirement, Box<EvalAltResult>> {
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<EvalAltResult>> {
let id_u32 = id_from_i64_to_u32(id_i64)?;
// Use the Collection trait method directly
let collection = db_clone.collection::<SignatureRequirement>()
.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
)))
});
db_module.set_native_fn(
"delete_signature_requirement",
move |id_i64: INT| -> Result<(), Box<EvalAltResult>> {
let id_u32 = id_from_i64_to_u32(id_i64)?;
// Use the Collection trait method directly
let collection = db_clone.collection::<SignatureRequirement>().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<Dynamic, Box<EvalAltResult>> {
let collection = db_clone.collection::<SignatureRequirement>()
.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))
});
db_module.set_native_fn(
"list_signature_requirements",
move || -> Result<Dynamic, Box<EvalAltResult>> {
let collection = db_clone.collection::<SignatureRequirement>().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.");
}

View File

@ -32,7 +32,12 @@ pub struct SignatureRequirement {
impl SignatureRequirement {
/// Create a new signature requirement.
pub fn new(_id: u32, flow_step_id: u32, public_key: impl ToString, message: impl ToString) -> Self {
pub fn new(
_id: u32,
flow_step_id: u32,
public_key: impl ToString,
message: impl ToString,
) -> Self {
Self {
base_data: BaseModelData::new(),
flow_step_id,

View File

@ -4,5 +4,5 @@ pub mod proposal;
pub mod attached_file;
pub use self::proposal::{Proposal, Ballot, VoteOption, ProposalStatus, VoteEventStatus};
pub use self::attached_file::AttachedFile;
pub use self::attached_file::AttachedFile;
pub use self::proposal::{Ballot, Proposal, ProposalStatus, VoteEventStatus, VoteOption};

View File

@ -5,9 +5,9 @@ use heromodels_derive::model; // For #[model]
use rhai::{CustomType, TypeBuilder};
use serde::{Deserialize, Serialize};
use heromodels_core::BaseModelData;
use crate::models::core::Comment;
use super::AttachedFile;
use crate::models::core::Comment;
use heromodels_core::BaseModelData;
// --- Enums ---
@ -72,9 +72,9 @@ impl VoteOption {
#[model] // Has base.Base in V spec
pub struct Ballot {
pub base_data: BaseModelData,
pub user_id: u32, // The ID of the user who cast this ballot
pub vote_option_id: u8, // The 'id' of the VoteOption chosen
pub shares_count: i64, // Number of shares/tokens/voting power
pub user_id: u32, // The ID of the user who cast this ballot
pub vote_option_id: u8, // The 'id' of the VoteOption chosen
pub shares_count: i64, // Number of shares/tokens/voting power
pub comment: Option<String>, // Optional comment from the voter
}
@ -281,7 +281,7 @@ impl Proposal {
eprintln!("Voting is not open for proposal '{}'", self.title);
return self;
}
// Check if the option exists
if !self.options.iter().any(|opt| opt.id == chosen_option_id) {
eprintln!(
@ -290,7 +290,7 @@ impl Proposal {
);
return self;
}
// Check eligibility for private proposals
if let Some(group) = &self.private_group {
if !group.contains(&user_id) {
@ -301,14 +301,14 @@ impl Proposal {
return self;
}
}
// Create a new ballot with the comment
let mut new_ballot = Ballot::new(ballot_id, user_id, chosen_option_id, shares);
new_ballot.comment = Some(comment.to_string());
// Add the ballot to the proposal
self.ballots.push(new_ballot);
// Update the vote count for the chosen option
if let Some(option) = self
.options
@ -317,7 +317,7 @@ impl Proposal {
{
option.count += shares;
}
self
}
}

View File

@ -1,7 +1,7 @@
use heromodels_core::BaseModelData;
use heromodels_derive::model;
use std::fmt;
use serde::{Deserialize, Serialize};
use std::fmt;
// --- Enums ---
@ -109,7 +109,7 @@ impl ContractSigner {
self.signed_at = Some(signed_at);
self
}
pub fn clear_signed_at(mut self) -> Self {
self.signed_at = None;
self
@ -139,21 +139,21 @@ pub struct Contract {
pub title: String,
pub description: String,
#[index]
pub contract_type: String,
#[index]
pub status: crate::models::ContractStatus, // Use re-exported path for #[model] macro
pub created_by: String,
pub terms_and_conditions: String,
pub start_date: Option<u64>,
pub end_date: Option<u64>,
pub renewal_period_days: Option<i32>,
pub next_renewal_date: Option<u64>,
pub signers: Vec<ContractSigner>,
pub revisions: Vec<ContractRevision>,
pub current_version: u32,
@ -217,7 +217,7 @@ impl Contract {
self.start_date = Some(start_date);
self
}
pub fn clear_start_date(mut self) -> Self {
self.start_date = None;
self
@ -257,7 +257,7 @@ impl Contract {
self.signers.push(signer);
self
}
pub fn signers(mut self, signers: Vec<ContractSigner>) -> Self {
self.signers = signers;
self
@ -272,7 +272,7 @@ impl Contract {
self.revisions = revisions;
self
}
pub fn current_version(mut self, version: u32) -> Self {
self.current_version = version;
self
@ -287,7 +287,7 @@ impl Contract {
self.last_signed_date = None;
self
}
// Example methods for state changes
pub fn set_status(&mut self, status: crate::models::ContractStatus) {
self.status = status;

View File

@ -1,51 +1,60 @@
use rhai::{
Dynamic, Engine, EvalAltResult, NativeCallContext, Position, Module, Array,
};
use rhai::{Array, Dynamic, Engine, EvalAltResult, Module, NativeCallContext, Position};
use std::sync::Arc;
use crate::db::hero::OurDB; // Updated path based on compiler suggestion
// use heromodels_core::BaseModelData; // Removed as fields are accessed via contract.base_data directly
use crate::models::legal::{Contract, ContractRevision, ContractSigner, ContractStatus, SignerStatus};
use crate::models::legal::{
Contract, ContractRevision, ContractSigner, ContractStatus, SignerStatus,
};
use crate::db::Collection; // Import the Collection trait
// --- Helper Functions for ID and Timestamp Conversion ---
fn i64_to_u32(val: i64, context_pos: Position, field_name: &str, object_name: &str) -> Result<u32, Box<EvalAltResult>> {
fn i64_to_u32(
val: i64,
context_pos: Position,
field_name: &str,
object_name: &str,
) -> Result<u32, Box<EvalAltResult>> {
val.try_into().map_err(|_e| {
Box::new(EvalAltResult::ErrorArithmetic(
format!(
"Conversion error for field '{}' in object '{}': cannot convert i64 ({}) to u32",
field_name,
object_name,
val
field_name, object_name, val
),
context_pos,
))
})
}
fn i64_to_u64(val: i64, context_pos: Position, field_name: &str, object_name: &str) -> Result<u64, Box<EvalAltResult>> {
fn i64_to_u64(
val: i64,
context_pos: Position,
field_name: &str,
object_name: &str,
) -> Result<u64, Box<EvalAltResult>> {
val.try_into().map_err(|_e| {
Box::new(EvalAltResult::ErrorArithmetic(
format!(
"Conversion error for field '{}' in object '{}': cannot convert i64 ({}) to u64",
field_name,
object_name,
val
field_name, object_name, val
),
context_pos,
))
})
}
fn i64_to_i32(val: i64, context_pos: Position, field_name: &str, object_name: &str) -> Result<i32, Box<EvalAltResult>> {
fn i64_to_i32(
val: i64,
context_pos: Position,
field_name: &str,
object_name: &str,
) -> Result<i32, Box<EvalAltResult>> {
val.try_into().map_err(|_e| {
Box::new(EvalAltResult::ErrorArithmetic(
format!(
"Conversion error for field '{}' in object '{}': cannot convert i64 ({}) to i32",
field_name,
object_name,
val
field_name, object_name, val
),
context_pos,
))
@ -73,193 +82,532 @@ pub fn register_legal_rhai_module(engine: &mut Engine, db: Arc<OurDB>) {
engine.register_static_module("SignerStatusConstants", signer_status_module.into());
engine.register_type_with_name::<SignerStatus>("SignerStatus"); // Expose the type itself
// --- ContractRevision ---
// --- ContractRevision ---
engine.register_type_with_name::<ContractRevision>("ContractRevision");
engine.register_fn(
"new_contract_revision",
move |context: NativeCallContext, version_i64: i64, content: String, created_at_i64: i64, created_by: String| -> Result<ContractRevision, Box<EvalAltResult>> {
let version = i64_to_u32(version_i64, context.position(), "version", "new_contract_revision")?;
let created_at = i64_to_u64(created_at_i64, context.position(), "created_at", "new_contract_revision")?;
Ok(ContractRevision::new(version, content, created_at, created_by))
}
move |context: NativeCallContext,
version_i64: i64,
content: String,
created_at_i64: i64,
created_by: String|
-> Result<ContractRevision, Box<EvalAltResult>> {
let version = i64_to_u32(
version_i64,
context.call_position(),
"version",
"new_contract_revision",
)?;
let created_at = i64_to_u64(
created_at_i64,
context.call_position(),
"created_at",
"new_contract_revision",
)?;
Ok(ContractRevision::new(
version, content, created_at, created_by,
))
},
);
engine.register_fn(
"comments",
|mut revision: ContractRevision, comments: String| -> ContractRevision {
revision.comments = Some(comments);
revision
},
);
engine.register_get(
"version",
|revision: &mut ContractRevision| -> Result<i64, Box<EvalAltResult>> {
Ok(revision.version as i64)
},
);
engine.register_get(
"content",
|revision: &mut ContractRevision| -> Result<String, Box<EvalAltResult>> {
Ok(revision.content.clone())
},
);
engine.register_get(
"created_at",
|revision: &mut ContractRevision| -> Result<i64, Box<EvalAltResult>> {
Ok(revision.created_at as i64)
},
);
engine.register_get(
"created_by",
|revision: &mut ContractRevision| -> Result<String, Box<EvalAltResult>> {
Ok(revision.created_by.clone())
},
);
engine.register_get(
"comments",
|revision: &mut ContractRevision| -> Result<Dynamic, Box<EvalAltResult>> {
Ok(revision
.comments
.clone()
.map_or(Dynamic::UNIT, Dynamic::from))
},
);
engine.register_fn("comments", |mut revision: ContractRevision, comments: String| -> ContractRevision {
revision.comments = Some(comments);
revision
});
engine.register_get("version", |revision: &mut ContractRevision| -> Result<i64, Box<EvalAltResult>> { Ok(revision.version as i64) });
engine.register_get("content", |revision: &mut ContractRevision| -> Result<String, Box<EvalAltResult>> { Ok(revision.content.clone()) });
engine.register_get("created_at", |revision: &mut ContractRevision| -> Result<i64, Box<EvalAltResult>> { Ok(revision.created_at as i64) });
engine.register_get("created_by", |revision: &mut ContractRevision| -> Result<String, Box<EvalAltResult>> { Ok(revision.created_by.clone()) });
engine.register_get("comments", |revision: &mut ContractRevision| -> Result<Dynamic, Box<EvalAltResult>> {
Ok(revision.comments.clone().map_or(Dynamic::UNIT, Dynamic::from))
});
// --- ContractSigner ---
// --- ContractSigner ---
engine.register_type_with_name::<ContractSigner>("ContractSigner");
engine.register_fn(
"new_contract_signer",
|id: String, name: String, email: String| -> ContractSigner {
ContractSigner::new(id, name, email)
}
},
);
engine.register_fn(
"status",
|signer: ContractSigner, status: SignerStatus| -> ContractSigner { signer.status(status) },
);
engine.register_fn(
"signed_at",
|context: NativeCallContext,
signer: ContractSigner,
signed_at_i64: i64|
-> Result<ContractSigner, Box<EvalAltResult>> {
let signed_at_u64 = i64_to_u64(
signed_at_i64,
context.call_position(),
"signed_at",
"ContractSigner.signed_at",
)?;
Ok(signer.signed_at(signed_at_u64))
},
);
engine.register_fn(
"clear_signed_at",
|signer: ContractSigner| -> ContractSigner { signer.clear_signed_at() },
);
engine.register_fn(
"comments",
|signer: ContractSigner, comments: String| -> ContractSigner { signer.comments(comments) },
);
engine.register_fn(
"clear_comments",
|signer: ContractSigner| -> ContractSigner { signer.clear_comments() },
);
engine.register_fn("status", |signer: ContractSigner, status: SignerStatus| -> ContractSigner { signer.status(status) });
engine.register_fn("signed_at", |context: NativeCallContext, signer: ContractSigner, signed_at_i64: i64| -> Result<ContractSigner, Box<EvalAltResult>> {
let signed_at_u64 = i64_to_u64(signed_at_i64, context.position(), "signed_at", "ContractSigner.signed_at")?;
Ok(signer.signed_at(signed_at_u64))
});
engine.register_fn("clear_signed_at", |signer: ContractSigner| -> ContractSigner { signer.clear_signed_at() });
engine.register_fn("comments", |signer: ContractSigner, comments: String| -> ContractSigner { signer.comments(comments) });
engine.register_fn("clear_comments", |signer: ContractSigner| -> ContractSigner { signer.clear_comments() });
engine.register_get("id", |signer: &mut ContractSigner| -> Result<String, Box<EvalAltResult>> { Ok(signer.id.clone()) });
engine.register_get("name", |signer: &mut ContractSigner| -> Result<String, Box<EvalAltResult>> { Ok(signer.name.clone()) });
engine.register_get("email", |signer: &mut ContractSigner| -> Result<String, Box<EvalAltResult>> { Ok(signer.email.clone()) });
engine.register_get("status", |signer: &mut ContractSigner| -> Result<SignerStatus, Box<EvalAltResult>> { Ok(signer.status.clone()) });
engine.register_get("signed_at_ts", |signer: &mut ContractSigner| -> Result<Dynamic, Box<EvalAltResult>> {
Ok(signer.signed_at.map_or(Dynamic::UNIT, |ts| Dynamic::from(ts as i64)))
});
engine.register_get("comments", |signer: &mut ContractSigner| -> Result<Dynamic, Box<EvalAltResult>> {
Ok(signer.comments.clone().map_or(Dynamic::UNIT, Dynamic::from))
});
engine.register_get("signed_at", |signer: &mut ContractSigner| -> Result<Dynamic, Box<EvalAltResult>> {
Ok(signer.signed_at.map_or(Dynamic::UNIT, |ts| Dynamic::from(ts)))
});
engine.register_get(
"id",
|signer: &mut ContractSigner| -> Result<String, Box<EvalAltResult>> {
Ok(signer.id.clone())
},
);
engine.register_get(
"name",
|signer: &mut ContractSigner| -> Result<String, Box<EvalAltResult>> {
Ok(signer.name.clone())
},
);
engine.register_get(
"email",
|signer: &mut ContractSigner| -> Result<String, Box<EvalAltResult>> {
Ok(signer.email.clone())
},
);
engine.register_get(
"status",
|signer: &mut ContractSigner| -> Result<SignerStatus, Box<EvalAltResult>> {
Ok(signer.status.clone())
},
);
engine.register_get(
"signed_at_ts",
|signer: &mut ContractSigner| -> Result<Dynamic, Box<EvalAltResult>> {
Ok(signer
.signed_at
.map_or(Dynamic::UNIT, |ts| Dynamic::from(ts as i64)))
},
);
engine.register_get(
"comments",
|signer: &mut ContractSigner| -> Result<Dynamic, Box<EvalAltResult>> {
Ok(signer.comments.clone().map_or(Dynamic::UNIT, Dynamic::from))
},
);
engine.register_get(
"signed_at",
|signer: &mut ContractSigner| -> Result<Dynamic, Box<EvalAltResult>> {
Ok(signer
.signed_at
.map_or(Dynamic::UNIT, |ts| Dynamic::from(ts)))
},
);
// --- Contract ---
// --- Contract ---
engine.register_type_with_name::<Contract>("Contract");
engine.register_fn(
"new_contract",
move |context: NativeCallContext, base_id_i64: i64, contract_id: String| -> Result<Contract, Box<EvalAltResult>> {
let base_id = i64_to_u32(base_id_i64, context.position(), "base_id", "new_contract")?;
move |context: NativeCallContext,
base_id_i64: i64,
contract_id: String|
-> Result<Contract, Box<EvalAltResult>> {
let base_id = i64_to_u32(
base_id_i64,
context.call_position(),
"base_id",
"new_contract",
)?;
Ok(Contract::new(base_id, contract_id))
}
},
);
// Builder methods
engine.register_fn("title", |contract: Contract, title: String| -> Contract { contract.title(title) });
engine.register_fn("description", |contract: Contract, description: String| -> Contract { contract.description(description) });
engine.register_fn("contract_type", |contract: Contract, contract_type: String| -> Contract { contract.contract_type(contract_type) });
engine.register_fn("status", |contract: Contract, status: ContractStatus| -> Contract { contract.status(status) });
engine.register_fn("created_by", |contract: Contract, created_by: String| -> Contract { contract.created_by(created_by) });
engine.register_fn("terms_and_conditions", |contract: Contract, terms: String| -> Contract { contract.terms_and_conditions(terms) });
engine.register_fn("start_date", |context: NativeCallContext, contract: Contract, start_date_i64: i64| -> Result<Contract, Box<EvalAltResult>> {
let start_date_u64 = i64_to_u64(start_date_i64, context.position(), "start_date", "Contract.start_date")?;
Ok(contract.start_date(start_date_u64))
engine.register_fn("title", |contract: Contract, title: String| -> Contract {
contract.title(title)
});
engine.register_fn("clear_start_date", |contract: Contract| -> Contract { contract.clear_start_date() });
engine.register_fn(
"description",
|contract: Contract, description: String| -> Contract { contract.description(description) },
);
engine.register_fn(
"contract_type",
|contract: Contract, contract_type: String| -> Contract {
contract.contract_type(contract_type)
},
);
engine.register_fn(
"status",
|contract: Contract, status: ContractStatus| -> Contract { contract.status(status) },
);
engine.register_fn(
"created_by",
|contract: Contract, created_by: String| -> Contract { contract.created_by(created_by) },
);
engine.register_fn(
"terms_and_conditions",
|contract: Contract, terms: String| -> Contract { contract.terms_and_conditions(terms) },
);
engine.register_fn("end_date", |context: NativeCallContext, contract: Contract, end_date_i64: i64| -> Result<Contract, Box<EvalAltResult>> {
let end_date_u64 = i64_to_u64(end_date_i64, context.position(), "end_date", "Contract.end_date")?;
Ok(contract.end_date(end_date_u64))
});
engine.register_fn("clear_end_date", |contract: Contract| -> Contract { contract.clear_end_date() });
engine.register_fn("renewal_period_days", |context: NativeCallContext, contract: Contract, days_i64: i64| -> Result<Contract, Box<EvalAltResult>> {
let days_i32 = i64_to_i32(days_i64, context.position(), "renewal_period_days", "Contract.renewal_period_days")?;
Ok(contract.renewal_period_days(days_i32))
});
engine.register_fn("clear_renewal_period_days", |contract: Contract| -> Contract { contract.clear_renewal_period_days() });
engine.register_fn("next_renewal_date", |context: NativeCallContext, contract: Contract, date_i64: i64| -> Result<Contract, Box<EvalAltResult>> {
let date_u64 = i64_to_u64(date_i64, context.position(), "next_renewal_date", "Contract.next_renewal_date")?;
Ok(contract.next_renewal_date(date_u64))
});
engine.register_fn("clear_next_renewal_date", |contract: Contract| -> Contract { contract.clear_next_renewal_date() });
engine.register_fn("add_signer", |contract: Contract, signer: ContractSigner| -> Contract { contract.add_signer(signer) });
engine.register_fn("signers", |contract: Contract, signers_array: Array| -> Contract {
let signers_vec = signers_array.into_iter().filter_map(|s| s.try_cast::<ContractSigner>()).collect();
contract.signers(signers_vec)
engine.register_fn(
"start_date",
|context: NativeCallContext,
contract: Contract,
start_date_i64: i64|
-> Result<Contract, Box<EvalAltResult>> {
let start_date_u64 = i64_to_u64(
start_date_i64,
context.call_position(),
"start_date",
"Contract.start_date",
)?;
Ok(contract.start_date(start_date_u64))
},
);
engine.register_fn("clear_start_date", |contract: Contract| -> Contract {
contract.clear_start_date()
});
engine.register_fn("add_revision", |contract: Contract, revision: ContractRevision| -> Contract { contract.add_revision(revision) });
engine.register_fn("revisions", |contract: Contract, revisions_array: Array| -> Contract {
let revisions_vec = revisions_array.into_iter().filter_map(|r| r.try_cast::<ContractRevision>()).collect();
contract.revisions(revisions_vec)
engine.register_fn(
"end_date",
|context: NativeCallContext,
contract: Contract,
end_date_i64: i64|
-> Result<Contract, Box<EvalAltResult>> {
let end_date_u64 = i64_to_u64(
end_date_i64,
context.call_position(),
"end_date",
"Contract.end_date",
)?;
Ok(contract.end_date(end_date_u64))
},
);
engine.register_fn("clear_end_date", |contract: Contract| -> Contract {
contract.clear_end_date()
});
engine.register_fn("current_version", |context: NativeCallContext, contract: Contract, version_i64: i64| -> Result<Contract, Box<EvalAltResult>> {
let version_u32 = i64_to_u32(version_i64, context.position(), "current_version", "Contract.current_version")?;
Ok(contract.current_version(version_u32))
});
engine.register_fn(
"renewal_period_days",
|context: NativeCallContext,
contract: Contract,
days_i64: i64|
-> Result<Contract, Box<EvalAltResult>> {
let days_i32 = i64_to_i32(
days_i64,
context.call_position(),
"renewal_period_days",
"Contract.renewal_period_days",
)?;
Ok(contract.renewal_period_days(days_i32))
},
);
engine.register_fn(
"clear_renewal_period_days",
|contract: Contract| -> Contract { contract.clear_renewal_period_days() },
);
engine.register_fn("last_signed_date", |context: NativeCallContext, contract: Contract, date_i64: i64| -> Result<Contract, Box<EvalAltResult>> {
let date_u64 = i64_to_u64(date_i64, context.position(), "last_signed_date", "Contract.last_signed_date")?;
Ok(contract.last_signed_date(date_u64))
engine.register_fn(
"next_renewal_date",
|context: NativeCallContext,
contract: Contract,
date_i64: i64|
-> Result<Contract, Box<EvalAltResult>> {
let date_u64 = i64_to_u64(
date_i64,
context.call_position(),
"next_renewal_date",
"Contract.next_renewal_date",
)?;
Ok(contract.next_renewal_date(date_u64))
},
);
engine.register_fn(
"clear_next_renewal_date",
|contract: Contract| -> Contract { contract.clear_next_renewal_date() },
);
engine.register_fn(
"add_signer",
|contract: Contract, signer: ContractSigner| -> Contract { contract.add_signer(signer) },
);
engine.register_fn(
"signers",
|contract: Contract, signers_array: Array| -> Contract {
let signers_vec = signers_array
.into_iter()
.filter_map(|s| s.try_cast::<ContractSigner>())
.collect();
contract.signers(signers_vec)
},
);
engine.register_fn(
"add_revision",
|contract: Contract, revision: ContractRevision| -> Contract {
contract.add_revision(revision)
},
);
engine.register_fn(
"revisions",
|contract: Contract, revisions_array: Array| -> Contract {
let revisions_vec = revisions_array
.into_iter()
.filter_map(|r| r.try_cast::<ContractRevision>())
.collect();
contract.revisions(revisions_vec)
},
);
engine.register_fn(
"current_version",
|context: NativeCallContext,
contract: Contract,
version_i64: i64|
-> Result<Contract, Box<EvalAltResult>> {
let version_u32 = i64_to_u32(
version_i64,
context.call_position(),
"current_version",
"Contract.current_version",
)?;
Ok(contract.current_version(version_u32))
},
);
engine.register_fn(
"last_signed_date",
|context: NativeCallContext,
contract: Contract,
date_i64: i64|
-> Result<Contract, Box<EvalAltResult>> {
let date_u64 = i64_to_u64(
date_i64,
context.call_position(),
"last_signed_date",
"Contract.last_signed_date",
)?;
Ok(contract.last_signed_date(date_u64))
},
);
engine.register_fn("clear_last_signed_date", |contract: Contract| -> Contract {
contract.clear_last_signed_date()
});
engine.register_fn("clear_last_signed_date", |contract: Contract| -> Contract { contract.clear_last_signed_date() });
// Getters for Contract
engine.register_get("id", |contract: &mut Contract| -> Result<i64, Box<EvalAltResult>> { Ok(contract.base_data.id as i64) });
engine.register_get("created_at_ts", |contract: &mut Contract| -> Result<i64, Box<EvalAltResult>> { Ok(contract.base_data.created_at as i64) });
engine.register_get("updated_at_ts", |contract: &mut Contract| -> Result<i64, Box<EvalAltResult>> { Ok(contract.base_data.modified_at as i64) });
engine.register_get("contract_id", |contract: &mut Contract| -> Result<String, Box<EvalAltResult>> { Ok(contract.contract_id.clone()) });
engine.register_get("title", |contract: &mut Contract| -> Result<String, Box<EvalAltResult>> { Ok(contract.title.clone()) });
engine.register_get("description", |contract: &mut Contract| -> Result<String, Box<EvalAltResult>> { Ok(contract.description.clone()) });
engine.register_get("contract_type", |contract: &mut Contract| -> Result<String, Box<EvalAltResult>> { Ok(contract.contract_type.clone()) });
engine.register_get("status", |contract: &mut Contract| -> Result<ContractStatus, Box<EvalAltResult>> { Ok(contract.status.clone()) });
engine.register_get("created_by", |contract: &mut Contract| -> Result<String, Box<EvalAltResult>> { Ok(contract.created_by.clone()) });
engine.register_get("terms_and_conditions", |contract: &mut Contract| -> Result<String, Box<EvalAltResult>> { Ok(contract.terms_and_conditions.clone()) });
engine.register_get("start_date", |contract: &mut Contract| -> Result<Dynamic, Box<EvalAltResult>> {
Ok(contract.start_date.map_or(Dynamic::UNIT, |ts| Dynamic::from(ts as i64)))
});
engine.register_get("end_date", |contract: &mut Contract| -> Result<Dynamic, Box<EvalAltResult>> {
Ok(contract.end_date.map_or(Dynamic::UNIT, |ts| Dynamic::from(ts as i64)))
});
engine.register_get("renewal_period_days", |contract: &mut Contract| -> Result<Dynamic, Box<EvalAltResult>> {
Ok(contract.renewal_period_days.map_or(Dynamic::UNIT, |days| Dynamic::from(days as i64)))
});
engine.register_get("next_renewal_date", |contract: &mut Contract| -> Result<Dynamic, Box<EvalAltResult>> {
Ok(contract.next_renewal_date.map_or(Dynamic::UNIT, |ts| Dynamic::from(ts as i64)))
});
engine.register_get("last_signed_date", |contract: &mut Contract| -> Result<Dynamic, Box<EvalAltResult>> {
Ok(contract.last_signed_date.map_or(Dynamic::UNIT, |ts| Dynamic::from(ts as i64)))
});
engine.register_get(
"id",
|contract: &mut Contract| -> Result<i64, Box<EvalAltResult>> {
Ok(contract.base_data.id as i64)
},
);
engine.register_get(
"created_at_ts",
|contract: &mut Contract| -> Result<i64, Box<EvalAltResult>> {
Ok(contract.base_data.created_at as i64)
},
);
engine.register_get(
"updated_at_ts",
|contract: &mut Contract| -> Result<i64, Box<EvalAltResult>> {
Ok(contract.base_data.modified_at as i64)
},
);
engine.register_get(
"contract_id",
|contract: &mut Contract| -> Result<String, Box<EvalAltResult>> {
Ok(contract.contract_id.clone())
},
);
engine.register_get(
"title",
|contract: &mut Contract| -> Result<String, Box<EvalAltResult>> {
Ok(contract.title.clone())
},
);
engine.register_get(
"description",
|contract: &mut Contract| -> Result<String, Box<EvalAltResult>> {
Ok(contract.description.clone())
},
);
engine.register_get(
"contract_type",
|contract: &mut Contract| -> Result<String, Box<EvalAltResult>> {
Ok(contract.contract_type.clone())
},
);
engine.register_get(
"status",
|contract: &mut Contract| -> Result<ContractStatus, Box<EvalAltResult>> {
Ok(contract.status.clone())
},
);
engine.register_get(
"created_by",
|contract: &mut Contract| -> Result<String, Box<EvalAltResult>> {
Ok(contract.created_by.clone())
},
);
engine.register_get(
"terms_and_conditions",
|contract: &mut Contract| -> Result<String, Box<EvalAltResult>> {
Ok(contract.terms_and_conditions.clone())
},
);
engine.register_get("current_version", |contract: &mut Contract| -> Result<i64, Box<EvalAltResult>> { Ok(contract.current_version as i64) });
engine.register_get(
"start_date",
|contract: &mut Contract| -> Result<Dynamic, Box<EvalAltResult>> {
Ok(contract
.start_date
.map_or(Dynamic::UNIT, |ts| Dynamic::from(ts as i64)))
},
);
engine.register_get(
"end_date",
|contract: &mut Contract| -> Result<Dynamic, Box<EvalAltResult>> {
Ok(contract
.end_date
.map_or(Dynamic::UNIT, |ts| Dynamic::from(ts as i64)))
},
);
engine.register_get(
"renewal_period_days",
|contract: &mut Contract| -> Result<Dynamic, Box<EvalAltResult>> {
Ok(contract
.renewal_period_days
.map_or(Dynamic::UNIT, |days| Dynamic::from(days as i64)))
},
);
engine.register_get(
"next_renewal_date",
|contract: &mut Contract| -> Result<Dynamic, Box<EvalAltResult>> {
Ok(contract
.next_renewal_date
.map_or(Dynamic::UNIT, |ts| Dynamic::from(ts as i64)))
},
);
engine.register_get(
"last_signed_date",
|contract: &mut Contract| -> Result<Dynamic, Box<EvalAltResult>> {
Ok(contract
.last_signed_date
.map_or(Dynamic::UNIT, |ts| Dynamic::from(ts as i64)))
},
);
engine.register_get("signers", |contract: &mut Contract| -> Result<Array, Box<EvalAltResult>> {
let rhai_array = contract.signers.iter().cloned().map(Dynamic::from).collect::<Array>();
Ok(rhai_array)
});
engine.register_get("revisions", |contract: &mut Contract| -> Result<Array, Box<EvalAltResult>> {
let rhai_array = contract.revisions.iter().cloned().map(Dynamic::from).collect::<Array>();
Ok(rhai_array)
});
engine.register_get(
"current_version",
|contract: &mut Contract| -> Result<i64, Box<EvalAltResult>> {
Ok(contract.current_version as i64)
},
);
engine.register_get(
"signers",
|contract: &mut Contract| -> Result<Array, Box<EvalAltResult>> {
let rhai_array = contract
.signers
.iter()
.cloned()
.map(Dynamic::from)
.collect::<Array>();
Ok(rhai_array)
},
);
engine.register_get(
"revisions",
|contract: &mut Contract| -> Result<Array, Box<EvalAltResult>> {
let rhai_array = contract
.revisions
.iter()
.cloned()
.map(Dynamic::from)
.collect::<Array>();
Ok(rhai_array)
},
);
// Method set_status
engine.register_fn("set_contract_status", |contract: &mut Contract, status: ContractStatus| {
contract.set_status(status);
});
engine.register_fn(
"set_contract_status",
|contract: &mut Contract, status: ContractStatus| {
contract.set_status(status);
},
);
// --- Database Interaction ---
let captured_db_for_set = Arc::clone(&db);
engine.register_fn("set_contract",
engine.register_fn(
"set_contract",
move |contract: Contract| -> Result<(), Box<EvalAltResult>> {
captured_db_for_set.set(&contract).map(|_| ()).map_err(|e| {
Box::new(EvalAltResult::ErrorRuntime(
format!("Failed to set Contract (ID: {}): {}", contract.base_data.id, e).into(),
format!(
"Failed to set Contract (ID: {}): {}",
contract.base_data.id, e
)
.into(),
Position::NONE,
))
})
});
},
);
let captured_db_for_get = Arc::clone(&db);
engine.register_fn("get_contract_by_id",
engine.register_fn(
"get_contract_by_id",
move |context: NativeCallContext, id_i64: i64| -> Result<Contract, Box<EvalAltResult>> {
let id_u32 = i64_to_u32(id_i64, context.position(), "id", "get_contract_by_id")?;
captured_db_for_get.get_by_id(id_u32)
.map_err(|e| Box::new(EvalAltResult::ErrorRuntime(
format!("Error getting Contract (ID: {}): {}", id_u32, e).into(),
Position::NONE,
)))?
.ok_or_else(|| Box::new(EvalAltResult::ErrorRuntime(
format!("Contract with ID {} not found", id_u32).into(),
Position::NONE,
)))
});
let id_u32 = i64_to_u32(id_i64, context.call_position(), "id", "get_contract_by_id")?;
captured_db_for_get
.get_by_id(id_u32)
.map_err(|e| {
Box::new(EvalAltResult::ErrorRuntime(
format!("Error getting Contract (ID: {}): {}", id_u32, e).into(),
Position::NONE,
))
})?
.ok_or_else(|| {
Box::new(EvalAltResult::ErrorRuntime(
format!("Contract with ID {} not found", id_u32).into(),
Position::NONE,
))
})
},
);
}

View File

@ -1,7 +1,7 @@
use heromodels_core::BaseModelData;
use heromodels_derive::model;
use serde::{Deserialize, Serialize};
use rhai::{CustomType, TypeBuilder};
use serde::{Deserialize, Serialize};
/// Represents a collection of library items.
#[model]

File diff suppressed because it is too large Load Diff

View File

@ -3,41 +3,41 @@ pub mod core;
pub mod userexample;
// pub mod productexample; // Temporarily remove as files are missing
pub mod access;
pub mod calendar;
pub mod contact;
pub mod circle;
pub mod governance;
pub mod finance;
pub mod library;
pub mod legal;
pub mod flow;
pub mod biz;
pub mod calendar;
pub mod circle;
pub mod contact;
pub mod finance;
pub mod flow;
pub mod governance;
pub mod legal;
pub mod library;
pub mod projects;
// Re-export key types for convenience
pub use core::Comment;
pub use userexample::User;
// pub use productexample::Product; // Temporarily remove
pub use calendar::{Calendar, Event, Attendee, AttendanceStatus};
pub use circle::{Circle};
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};
pub use flow::{Flow, FlowStep, SignatureRequirement};
pub use biz::{Sale, SaleItem, SaleStatus};
pub use library::items::{Image, Pdf, Markdown};
pub use calendar::{AttendanceStatus, Attendee, Calendar, Event};
pub use circle::Circle;
pub use finance::marketplace::{Bid, BidStatus, Listing, ListingStatus, ListingType};
pub use finance::{Account, Asset, AssetType};
pub use flow::{Flow, FlowStep, SignatureRequirement};
pub use governance::{AttachedFile, Ballot, Proposal, ProposalStatus, VoteEventStatus, VoteOption};
pub use legal::{Contract, ContractRevision, ContractSigner, ContractStatus, SignerStatus};
pub use library::collection::Collection;
pub use library::items::{Image, Markdown, Pdf};
pub use flow::register_flow_rhai_module;
#[cfg(feature = "rhai")]
pub use biz::register_biz_rhai_module;
#[cfg(feature = "rhai")]
pub use calendar::register_calendar_rhai_module;
#[cfg(feature = "rhai")]
pub use circle::register_circle_rhai_module;
pub use flow::register_flow_rhai_module;
pub use legal::register_legal_rhai_module;
#[cfg(feature = "rhai")]
pub use biz::register_biz_rhai_module;
pub use library::register_library_rhai_module;
#[cfg(feature = "rhai")]
pub use projects::register_projects_rhai_module;
#[cfg(feature = "rhai")]
pub use library::register_library_rhai_module;

View File

@ -1,8 +1,8 @@
// heromodels/src/models/projects/base.rs
use serde::{Deserialize, Serialize};
use heromodels_core::{BaseModelData, Model, BaseModelDataOps};
use heromodels_core::{BaseModelData, BaseModelDataOps, Model};
#[cfg(feature = "rhai")]
use rhai::{CustomType, TypeBuilder};
use serde::{Deserialize, Serialize};
use strum_macros::Display; // Made unconditional as Display derive is used on non-rhai-gated enums
// --- Enums ---
@ -50,7 +50,7 @@ pub enum ItemType {
impl Default for ItemType {
fn default() -> Self {
ItemType::Task
}
}
}
// --- Structs ---
@ -178,7 +178,6 @@ impl Project {
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[cfg_attr(feature = "rhai", derive(CustomType))]
pub struct Label {
@ -226,4 +225,3 @@ impl Label {
self
}
}

View File

@ -3,8 +3,8 @@
use chrono::{DateTime, Utc};
use heromodels_core::BaseModelData;
use heromodels_derive::model;
use serde::{Deserialize, Serialize};
use rhai::{CustomType, TypeBuilder};
use serde::{Deserialize, Serialize};
use super::base::Status as ProjectStatus; // Using the generic project status for now
@ -15,17 +15,17 @@ pub struct Epic {
pub name: String,
pub description: Option<String>,
pub status: ProjectStatus, // Or a new EpicStatus enum if needed
pub project_id: Option<u32>, // Link to a general project/board
pub start_date: Option<DateTime<Utc>>,
pub due_date: Option<DateTime<Utc>>,
pub tags: Vec<String>,
// 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<u32>,
pub child_task_ids: Vec<u32>,
}
impl Epic {

View File

@ -1,11 +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 sprint_enums;
pub mod task;
pub mod task_enums;
// pub mod epic;
// pub mod issue;
// pub mod kanban;
@ -13,11 +13,11 @@ pub mod sprint;
// 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 sprint_enums::*;
pub use task::*;
pub use task_enums::*;
// pub use epic::*;
// pub use issue::*;
// pub use kanban::*;
@ -25,7 +25,7 @@ pub use sprint::*;
// pub use story::*;
#[cfg(feature = "rhai")]
pub mod rhai;
pub mod rhai;
#[cfg(feature = "rhai")]
pub use rhai::register_projects_rhai_module;

View File

@ -1,14 +1,13 @@
// heromodels/src/models/projects/rhai.rs
use rhai::{Engine, EvalAltResult, Dynamic, Position};
use std::sync::Arc;
use crate::db::hero::OurDB;
use heromodels_core::{Model, BaseModelDataOps};
use crate::db::{Db, Collection};
use crate::db::{Collection, Db};
use heromodels_core::{BaseModelDataOps, Model};
use rhai::{Dynamic, Engine, EvalAltResult, Position};
use std::sync::Arc;
// Import models from the projects::base module
use super::base::{Project, /* Label, */ Priority, Status, ItemType}; // Label commented out as it's unused for now
use super::base::{ItemType, /* Label, */ Priority, Project, Status}; // Label commented out as it's unused for now
// Helper function for ID conversion (if needed, similar to other rhai.rs files)
fn id_from_i64(val: i64) -> Result<u32, Box<EvalAltResult>> {
@ -78,172 +77,325 @@ pub fn register_projects_rhai_module(engine: &mut Engine, db: Arc<OurDB>) {
});
// Multi-argument constructor (renamed)
engine.register_fn("new_project_with_details", |id_i64: i64, name: String, description: String, owner_id_i64: i64| -> Result<Project, Box<EvalAltResult>> {
Ok(Project::new(id_from_i64(id_i64)?, name, description, id_from_i64(owner_id_i64)?))
});
engine.register_fn(
"new_project_with_details",
|id_i64: i64,
name: String,
description: String,
owner_id_i64: i64|
-> Result<Project, Box<EvalAltResult>> {
Ok(Project::new(
id_from_i64(id_i64)?,
name,
description,
id_from_i64(owner_id_i64)?,
))
},
);
// Getters for Project
engine.register_get("id", |p: &mut Project| -> Result<i64, Box<EvalAltResult>> { Ok(p.get_id() as i64) });
engine.register_get("name", |p: &mut Project| -> Result<String, Box<EvalAltResult>> { Ok(p.name.clone()) });
engine.register_get("description", |p: &mut Project| -> Result<String, Box<EvalAltResult>> { Ok(p.description.clone()) });
engine.register_get("owner_id", |p: &mut Project| -> Result<i64, Box<EvalAltResult>> { Ok(p.owner_id as i64) });
engine.register_get("member_ids", |p: &mut Project| -> Result<rhai::Array, Box<EvalAltResult>> {
Ok(p.member_ids.iter().map(|&id| rhai::Dynamic::from(id as i64)).collect())
engine.register_get("id", |p: &mut Project| -> Result<i64, Box<EvalAltResult>> {
Ok(p.get_id() as i64)
});
engine.register_get("board_ids", |p: &mut Project| -> Result<rhai::Array, Box<EvalAltResult>> {
Ok(p.board_ids.iter().map(|&id| rhai::Dynamic::from(id as i64)).collect())
});
engine.register_get("sprint_ids", |p: &mut Project| -> Result<rhai::Array, Box<EvalAltResult>> {
Ok(p.sprint_ids.iter().map(|&id| rhai::Dynamic::from(id as i64)).collect())
});
engine.register_get("epic_ids", |p: &mut Project| -> Result<rhai::Array, Box<EvalAltResult>> {
Ok(p.epic_ids.iter().map(|&id| rhai::Dynamic::from(id as i64)).collect())
});
engine.register_get("tags", |p: &mut Project| -> Result<rhai::Array, Box<EvalAltResult>> {
Ok(p.tags.iter().map(|tag| rhai::Dynamic::from(tag.clone())).collect())
});
engine.register_get("created_at", |p: &mut Project| -> Result<i64, Box<EvalAltResult>> { Ok(p.base_data.created_at) });
engine.register_get("modified_at", |p: &mut Project| -> Result<i64, Box<EvalAltResult>> { Ok(p.base_data.modified_at) });
engine.register_get("comments", |p: &mut Project| -> Result<rhai::Array, Box<EvalAltResult>> {
Ok(p.base_data.comments.iter().map(|&id| rhai::Dynamic::from(id as i64)).collect())
});
engine.register_get("status", |p: &mut Project| -> Result<Status, Box<EvalAltResult>> { Ok(p.status.clone()) });
engine.register_get("priority", |p: &mut Project| -> Result<Priority, Box<EvalAltResult>> { Ok(p.priority.clone()) });
engine.register_get("item_type", |p: &mut Project| -> Result<ItemType, Box<EvalAltResult>> { Ok(p.item_type.clone()) });
engine.register_get(
"name",
|p: &mut Project| -> Result<String, Box<EvalAltResult>> { Ok(p.name.clone()) },
);
engine.register_get(
"description",
|p: &mut Project| -> Result<String, Box<EvalAltResult>> { Ok(p.description.clone()) },
);
engine.register_get(
"owner_id",
|p: &mut Project| -> Result<i64, Box<EvalAltResult>> { Ok(p.owner_id as i64) },
);
engine.register_get(
"member_ids",
|p: &mut Project| -> Result<rhai::Array, Box<EvalAltResult>> {
Ok(p.member_ids
.iter()
.map(|&id| rhai::Dynamic::from(id as i64))
.collect())
},
);
engine.register_get(
"board_ids",
|p: &mut Project| -> Result<rhai::Array, Box<EvalAltResult>> {
Ok(p.board_ids
.iter()
.map(|&id| rhai::Dynamic::from(id as i64))
.collect())
},
);
engine.register_get(
"sprint_ids",
|p: &mut Project| -> Result<rhai::Array, Box<EvalAltResult>> {
Ok(p.sprint_ids
.iter()
.map(|&id| rhai::Dynamic::from(id as i64))
.collect())
},
);
engine.register_get(
"epic_ids",
|p: &mut Project| -> Result<rhai::Array, Box<EvalAltResult>> {
Ok(p.epic_ids
.iter()
.map(|&id| rhai::Dynamic::from(id as i64))
.collect())
},
);
engine.register_get(
"tags",
|p: &mut Project| -> Result<rhai::Array, Box<EvalAltResult>> {
Ok(p.tags
.iter()
.map(|tag| rhai::Dynamic::from(tag.clone()))
.collect())
},
);
engine.register_get(
"created_at",
|p: &mut Project| -> Result<i64, Box<EvalAltResult>> { Ok(p.base_data.created_at) },
);
engine.register_get(
"modified_at",
|p: &mut Project| -> Result<i64, Box<EvalAltResult>> { Ok(p.base_data.modified_at) },
);
engine.register_get(
"comments",
|p: &mut Project| -> Result<rhai::Array, Box<EvalAltResult>> {
Ok(p.base_data
.comments
.iter()
.map(|&id| rhai::Dynamic::from(id as i64))
.collect())
},
);
engine.register_get(
"status",
|p: &mut Project| -> Result<Status, Box<EvalAltResult>> { Ok(p.status.clone()) },
);
engine.register_get(
"priority",
|p: &mut Project| -> Result<Priority, Box<EvalAltResult>> { Ok(p.priority.clone()) },
);
engine.register_get(
"item_type",
|p: &mut Project| -> Result<ItemType, Box<EvalAltResult>> { Ok(p.item_type.clone()) },
);
// Builder methods for Project
// let db_clone = db.clone(); // This was unused
engine.register_fn("name", |p: Project, name: String| -> Result<Project, Box<EvalAltResult>> { Ok(p.name(name)) });
engine.register_fn("description", |p: Project, description: String| -> Result<Project, Box<EvalAltResult>> { Ok(p.description(description)) });
engine.register_fn("owner_id", |p: Project, owner_id_i64: i64| -> Result<Project, Box<EvalAltResult>> { Ok(p.owner_id(id_from_i64(owner_id_i64)?)) });
engine.register_fn("add_member_id", |p: Project, member_id_i64: i64| -> Result<Project, Box<EvalAltResult>> { Ok(p.add_member_id(id_from_i64(member_id_i64)?)) });
engine.register_fn("member_ids", |p: Project, member_ids_i64: rhai::Array| -> Result<Project, Box<EvalAltResult>> {
let ids = member_ids_i64
.into_iter()
.map(|id_dyn: Dynamic| {
let val_i64 = id_dyn.clone().try_cast::<i64>().ok_or_else(|| {
Box::new(EvalAltResult::ErrorMismatchDataType(
"Expected integer for ID".to_string(),
id_dyn.type_name().to_string(),
Position::NONE,
))
})?;
id_from_i64(val_i64)
})
.collect::<Result<Vec<u32>, Box<EvalAltResult>>>()?;
Ok(p.member_ids(ids))
});
engine.register_fn("add_board_id", |p: Project, board_id_i64: i64| -> Result<Project, Box<EvalAltResult>> { Ok(p.add_board_id(id_from_i64(board_id_i64)?)) });
engine.register_fn("board_ids", |p: Project, board_ids_i64: rhai::Array| -> Result<Project, Box<EvalAltResult>> {
let ids = board_ids_i64
.into_iter()
.map(|id_dyn: Dynamic| {
let val_i64 = id_dyn.clone().try_cast::<i64>().ok_or_else(|| {
Box::new(EvalAltResult::ErrorMismatchDataType(
"Expected integer for ID".to_string(),
id_dyn.type_name().to_string(),
Position::NONE,
))
})?;
id_from_i64(val_i64)
})
.collect::<Result<Vec<u32>, Box<EvalAltResult>>>()?;
Ok(p.board_ids(ids))
});
engine.register_fn("add_sprint_id", |p: Project, sprint_id_i64: i64| -> Result<Project, Box<EvalAltResult>> { Ok(p.add_sprint_id(id_from_i64(sprint_id_i64)?)) });
engine.register_fn("sprint_ids", |p: Project, sprint_ids_i64: rhai::Array| -> Result<Project, Box<EvalAltResult>> {
let ids = sprint_ids_i64
.into_iter()
.map(|id_dyn: Dynamic| {
let val_i64 = id_dyn.clone().try_cast::<i64>().ok_or_else(|| {
Box::new(EvalAltResult::ErrorMismatchDataType(
"Expected integer for ID".to_string(),
id_dyn.type_name().to_string(),
Position::NONE,
))
})?;
id_from_i64(val_i64)
})
.collect::<Result<Vec<u32>, Box<EvalAltResult>>>()?;
Ok(p.sprint_ids(ids))
});
engine.register_fn("add_epic_id", |p: Project, epic_id_i64: i64| -> Result<Project, Box<EvalAltResult>> { Ok(p.add_epic_id(id_from_i64(epic_id_i64)?)) });
engine.register_fn("epic_ids", |p: Project, epic_ids_i64: rhai::Array| -> Result<Project, Box<EvalAltResult>> {
let ids = epic_ids_i64
.into_iter()
.map(|id_dyn: Dynamic| {
let val_i64 = id_dyn.clone().try_cast::<i64>().ok_or_else(|| {
Box::new(EvalAltResult::ErrorMismatchDataType(
"Expected integer for ID".to_string(),
id_dyn.type_name().to_string(),
Position::NONE,
))
})?;
id_from_i64(val_i64)
})
.collect::<Result<Vec<u32>, Box<EvalAltResult>>>()?;
Ok(p.epic_ids(ids))
});
engine.register_fn("add_tag", |p: Project, tag: String| -> Result<Project, Box<EvalAltResult>> { Ok(p.add_tag(tag)) });
engine.register_fn("tags", |p: Project, tags_dyn: rhai::Array| -> Result<Project, Box<EvalAltResult>> {
let tags_vec = tags_dyn
.into_iter()
.map(|tag_dyn: Dynamic| {
tag_dyn.clone().into_string().map_err(|_err| { // _err is Rhai's internal error, we create a new one
Box::new(EvalAltResult::ErrorMismatchDataType(
"Expected string for tag".to_string(),
tag_dyn.type_name().to_string(),
Position::NONE,
))
engine.register_fn(
"name",
|p: Project, name: String| -> Result<Project, Box<EvalAltResult>> { Ok(p.name(name)) },
);
engine.register_fn(
"description",
|p: Project, description: String| -> Result<Project, Box<EvalAltResult>> {
Ok(p.description(description))
},
);
engine.register_fn(
"owner_id",
|p: Project, owner_id_i64: i64| -> Result<Project, Box<EvalAltResult>> {
Ok(p.owner_id(id_from_i64(owner_id_i64)?))
},
);
engine.register_fn(
"add_member_id",
|p: Project, member_id_i64: i64| -> Result<Project, Box<EvalAltResult>> {
Ok(p.add_member_id(id_from_i64(member_id_i64)?))
},
);
engine.register_fn(
"member_ids",
|p: Project, member_ids_i64: rhai::Array| -> Result<Project, Box<EvalAltResult>> {
let ids = member_ids_i64
.into_iter()
.map(|id_dyn: Dynamic| {
let val_i64 = id_dyn.clone().try_cast::<i64>().ok_or_else(|| {
Box::new(EvalAltResult::ErrorMismatchDataType(
"Expected integer for ID".to_string(),
id_dyn.type_name().to_string(),
Position::NONE,
))
})?;
id_from_i64(val_i64)
})
})
.collect::<Result<Vec<String>, Box<EvalAltResult>>>()?;
Ok(p.tags(tags_vec))
});
.collect::<Result<Vec<u32>, Box<EvalAltResult>>>()?;
Ok(p.member_ids(ids))
},
);
engine.register_fn(
"add_board_id",
|p: Project, board_id_i64: i64| -> Result<Project, Box<EvalAltResult>> {
Ok(p.add_board_id(id_from_i64(board_id_i64)?))
},
);
engine.register_fn(
"board_ids",
|p: Project, board_ids_i64: rhai::Array| -> Result<Project, Box<EvalAltResult>> {
let ids = board_ids_i64
.into_iter()
.map(|id_dyn: Dynamic| {
let val_i64 = id_dyn.clone().try_cast::<i64>().ok_or_else(|| {
Box::new(EvalAltResult::ErrorMismatchDataType(
"Expected integer for ID".to_string(),
id_dyn.type_name().to_string(),
Position::NONE,
))
})?;
id_from_i64(val_i64)
})
.collect::<Result<Vec<u32>, Box<EvalAltResult>>>()?;
Ok(p.board_ids(ids))
},
);
engine.register_fn(
"add_sprint_id",
|p: Project, sprint_id_i64: i64| -> Result<Project, Box<EvalAltResult>> {
Ok(p.add_sprint_id(id_from_i64(sprint_id_i64)?))
},
);
engine.register_fn(
"sprint_ids",
|p: Project, sprint_ids_i64: rhai::Array| -> Result<Project, Box<EvalAltResult>> {
let ids = sprint_ids_i64
.into_iter()
.map(|id_dyn: Dynamic| {
let val_i64 = id_dyn.clone().try_cast::<i64>().ok_or_else(|| {
Box::new(EvalAltResult::ErrorMismatchDataType(
"Expected integer for ID".to_string(),
id_dyn.type_name().to_string(),
Position::NONE,
))
})?;
id_from_i64(val_i64)
})
.collect::<Result<Vec<u32>, Box<EvalAltResult>>>()?;
Ok(p.sprint_ids(ids))
},
);
engine.register_fn(
"add_epic_id",
|p: Project, epic_id_i64: i64| -> Result<Project, Box<EvalAltResult>> {
Ok(p.add_epic_id(id_from_i64(epic_id_i64)?))
},
);
engine.register_fn(
"epic_ids",
|p: Project, epic_ids_i64: rhai::Array| -> Result<Project, Box<EvalAltResult>> {
let ids = epic_ids_i64
.into_iter()
.map(|id_dyn: Dynamic| {
let val_i64 = id_dyn.clone().try_cast::<i64>().ok_or_else(|| {
Box::new(EvalAltResult::ErrorMismatchDataType(
"Expected integer for ID".to_string(),
id_dyn.type_name().to_string(),
Position::NONE,
))
})?;
id_from_i64(val_i64)
})
.collect::<Result<Vec<u32>, Box<EvalAltResult>>>()?;
Ok(p.epic_ids(ids))
},
);
engine.register_fn(
"add_tag",
|p: Project, tag: String| -> Result<Project, Box<EvalAltResult>> { Ok(p.add_tag(tag)) },
);
engine.register_fn(
"tags",
|p: Project, tags_dyn: rhai::Array| -> Result<Project, Box<EvalAltResult>> {
let tags_vec = tags_dyn
.into_iter()
.map(|tag_dyn: Dynamic| {
tag_dyn.clone().into_string().map_err(|_err| {
// _err is Rhai's internal error, we create a new one
Box::new(EvalAltResult::ErrorMismatchDataType(
"Expected string for tag".to_string(),
tag_dyn.type_name().to_string(),
Position::NONE,
))
})
})
.collect::<Result<Vec<String>, Box<EvalAltResult>>>()?;
Ok(p.tags(tags_vec))
},
);
engine.register_fn("status", |p: Project, status: Status| -> Result<Project, Box<EvalAltResult>> { Ok(p.status(status)) });
engine.register_fn("priority", |p: Project, priority: Priority| -> Result<Project, Box<EvalAltResult>> { Ok(p.priority(priority)) });
engine.register_fn("item_type", |p: Project, item_type: ItemType| -> Result<Project, Box<EvalAltResult>> { Ok(p.item_type(item_type)) });
engine.register_fn(
"status",
|p: Project, status: Status| -> Result<Project, Box<EvalAltResult>> {
Ok(p.status(status))
},
);
engine.register_fn(
"priority",
|p: Project, priority: Priority| -> Result<Project, Box<EvalAltResult>> {
Ok(p.priority(priority))
},
);
engine.register_fn(
"item_type",
|p: Project, item_type: ItemType| -> Result<Project, Box<EvalAltResult>> {
Ok(p.item_type(item_type))
},
);
// Base ModelData builders
engine.register_fn("add_base_comment", |p: Project, comment_id_i64: i64| -> Result<Project, Box<EvalAltResult>> { Ok(p.add_base_comment(id_from_i64(comment_id_i64)?)) });
engine.register_fn(
"add_base_comment",
|p: Project, comment_id_i64: i64| -> Result<Project, Box<EvalAltResult>> {
Ok(p.add_base_comment(id_from_i64(comment_id_i64)?))
},
);
// --- Database Interaction Functions ---
let db_clone_set = db.clone();
engine.register_fn("set_project", move |project: Project| -> Result<(), Box<EvalAltResult>> {
let collection = db_clone_set.collection::<Project>().map_err(|e| {
Box::new(EvalAltResult::ErrorSystem(
"Failed to access project collection".to_string(),
format!("DB operation failed: {:?}", e).into(),
))
})?;
engine.register_fn(
"set_project",
move |project: Project| -> Result<(), Box<EvalAltResult>> {
let collection = db_clone_set.collection::<Project>().map_err(|e| {
Box::new(EvalAltResult::ErrorSystem(
"Failed to access project collection".to_string(),
format!("DB operation failed: {:?}", e).into(),
))
})?;
collection.set(&project).map(|_| ()).map_err(|e| {
Box::new(EvalAltResult::ErrorSystem(
"Failed to save project".to_string(),
format!("DB operation failed: {:?}", e).into(),
))
})
});
collection.set(&project).map(|_| ()).map_err(|e| {
Box::new(EvalAltResult::ErrorSystem(
"Failed to save project".to_string(),
format!("DB operation failed: {:?}", e).into(),
))
})
},
);
let db_clone_get = db.clone();
engine.register_fn("get_project_by_id", move |id_i64: i64| -> Result<Dynamic, Box<EvalAltResult>> {
let id = id_from_i64(id_i64)?;
let collection = db_clone_get.collection::<Project>().map_err(|e| {
Box::new(EvalAltResult::ErrorSystem(
"Failed to access project collection".to_string(),
format!("DB operation failed: {:?}", e).into(),
))
})?;
engine.register_fn(
"get_project_by_id",
move |id_i64: i64| -> Result<Dynamic, Box<EvalAltResult>> {
let id = id_from_i64(id_i64)?;
let collection = db_clone_get.collection::<Project>().map_err(|e| {
Box::new(EvalAltResult::ErrorSystem(
"Failed to access project collection".to_string(),
format!("DB operation failed: {:?}", e).into(),
))
})?;
match collection.get_by_id(id) {
Ok(Some(project)) => Ok(Dynamic::from(project)),
Ok(None) => Ok(Dynamic::UNIT), // Represents '()' in Rhai
Err(e) => Err(Box::new(EvalAltResult::ErrorSystem(
"Failed to retrieve project by ID".to_string(),
format!("DB operation failed: {:?}", e).into(),
))),
}
});
match collection.get_by_id(id) {
Ok(Some(project)) => Ok(Dynamic::from(project)),
Ok(None) => Ok(Dynamic::UNIT), // Represents '()' in Rhai
Err(e) => Err(Box::new(EvalAltResult::ErrorSystem(
"Failed to retrieve project by ID".to_string(),
format!("DB operation failed: {:?}", e).into(),
))),
}
},
);
// TODO: Register Rhai bindings for the `Label` model if needed, or remove unused import.
// Register Label type and its methods/getters

View File

@ -3,8 +3,8 @@
use chrono::{DateTime, Utc};
use heromodels_core::BaseModelData;
use heromodels_derive::model;
use serde::{Deserialize, Serialize};
use rhai::{CustomType, TypeBuilder};
use serde::{Deserialize, Serialize};
use super::sprint_enums::SprintStatus; // Import our new enum
@ -16,14 +16,14 @@ pub struct Sprint {
pub description: Option<String>,
pub status: SprintStatus,
pub goal: Option<String>, // Sprint goal
pub project_id: Option<u32>, // Link to a general project/board
pub start_date: Option<DateTime<Utc>>,
pub end_date: Option<DateTime<Utc>>, // Changed from due_date for sprints
// Explicitly list task IDs belonging to this sprint
pub task_ids: Vec<u32>,
pub task_ids: Vec<u32>,
}
impl Sprint {

View File

@ -3,10 +3,10 @@
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 rhai::{CustomType, TypeBuilder};
use serde::{Deserialize, Serialize}; // Assuming rhai might be used
use super::task_enums::{TaskStatus, TaskPriority}; // Import our new enums
use super::task_enums::{TaskPriority, TaskStatus}; // Import our new enums
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, CustomType)]
#[model] // This will provide id, created_at, updated_at via base_data
@ -16,10 +16,10 @@ pub struct Task {
pub description: Option<String>,
pub status: TaskStatus,
pub priority: TaskPriority,
pub assignee_id: Option<u32>, // User ID
pub reporter_id: Option<u32>, // User ID
pub parent_task_id: Option<u32>, // For subtasks
pub epic_id: Option<u32>,
pub sprint_id: Option<u32>,

View File

@ -2,4 +2,4 @@
pub mod user;
// Re-export User for convenience
pub use user::User;
pub use user::User;

View File

@ -15,9 +15,9 @@ rand = "0.8.5"
criterion = "0.5.1"
tempfile = "3.8.0"
[[bench]]
name = "ourdb_benchmarks"
harness = false
# [[bench]]
# name = "ourdb_benchmarks"
# harness = false
[[example]]
name = "basic_usage"

View File

@ -6,18 +6,18 @@ fn main() -> Result<(), ourdb::Error> {
// Create a temporary directory for the database
let db_path = std::env::temp_dir().join("ourdb_advanced_example");
std::fs::create_dir_all(&db_path)?;
println!("Creating database at: {}", db_path.display());
// Demonstrate key-value mode (non-incremental)
key_value_mode_example(&db_path)?;
// Demonstrate incremental mode
incremental_mode_example(&db_path)?;
// Demonstrate performance benchmarking
performance_benchmark(&db_path)?;
// Clean up (optional)
if std::env::var("KEEP_DB").is_err() {
std::fs::remove_dir_all(&db_path)?;
@ -25,16 +25,16 @@ fn main() -> Result<(), ourdb::Error> {
} else {
println!("Database kept at: {}", db_path.display());
}
Ok(())
}
fn key_value_mode_example(base_path: &PathBuf) -> Result<(), ourdb::Error> {
println!("\n=== Key-Value Mode Example ===");
let db_path = base_path.join("key_value");
std::fs::create_dir_all(&db_path)?;
// Create a new database with key-value mode (non-incremental)
let config = OurDBConfig {
path: db_path,
@ -43,52 +43,62 @@ fn key_value_mode_example(base_path: &PathBuf) -> Result<(), ourdb::Error> {
keysize: Some(2), // Small key size for demonstration
reset: None, // Don't reset existing database
};
let mut db = OurDB::new(config)?;
// In key-value mode, we must provide IDs explicitly
let custom_ids = [100, 200, 300, 400, 500];
// Store data with custom IDs
for (i, &id) in custom_ids.iter().enumerate() {
let data = format!("Record with custom ID {}", id);
db.set(OurDBSetArgs { id: Some(id), data: data.as_bytes() })?;
println!("Stored record {} with custom ID: {}", i+1, id);
db.set(OurDBSetArgs {
id: Some(id),
data: data.as_bytes(),
})?;
println!("Stored record {} with custom ID: {}", i + 1, id);
}
// Retrieve data by custom IDs
for &id in &custom_ids {
let retrieved = db.get(id)?;
println!("Retrieved ID {}: {}", id, String::from_utf8_lossy(&retrieved));
println!(
"Retrieved ID {}: {}",
id,
String::from_utf8_lossy(&retrieved)
);
}
// Update and track history
let id_to_update = custom_ids[2]; // ID 300
for i in 1..=3 {
let updated_data = format!("Updated record {} (version {})", id_to_update, i);
db.set(OurDBSetArgs { id: Some(id_to_update), data: updated_data.as_bytes() })?;
db.set(OurDBSetArgs {
id: Some(id_to_update),
data: updated_data.as_bytes(),
})?;
println!("Updated ID {} (version {})", id_to_update, i);
}
// Get history for the updated record
let history = db.get_history(id_to_update, 5)?;
println!("History for ID {} (most recent first):", id_to_update);
for (i, entry) in history.iter().enumerate() {
println!(" Version {}: {}", i, String::from_utf8_lossy(entry));
}
db.close()?;
println!("Key-value mode example completed");
Ok(())
}
fn incremental_mode_example(base_path: &PathBuf) -> Result<(), ourdb::Error> {
println!("\n=== Incremental Mode Example ===");
let db_path = base_path.join("incremental");
std::fs::create_dir_all(&db_path)?;
// Create a new database with incremental mode
let config = OurDBConfig {
path: db_path,
@ -97,42 +107,49 @@ fn incremental_mode_example(base_path: &PathBuf) -> Result<(), ourdb::Error> {
keysize: Some(3), // 3-byte keys
reset: None, // Don't reset existing database
};
let mut db = OurDB::new(config)?;
// In incremental mode, IDs are auto-generated
let mut assigned_ids = Vec::new();
// Store multiple records and collect assigned IDs
for i in 1..=5 {
let data = format!("Auto-increment record {}", i);
let id = db.set(OurDBSetArgs { id: None, data: data.as_bytes() })?;
let id = db.set(OurDBSetArgs {
id: None,
data: data.as_bytes(),
})?;
assigned_ids.push(id);
println!("Stored record {} with auto-assigned ID: {}", i, id);
}
// Check next ID
let next_id = db.get_next_id()?;
println!("Next ID to be assigned: {}", next_id);
// Retrieve all records
for &id in &assigned_ids {
let retrieved = db.get(id)?;
println!("Retrieved ID {}: {}", id, String::from_utf8_lossy(&retrieved));
println!(
"Retrieved ID {}: {}",
id,
String::from_utf8_lossy(&retrieved)
);
}
db.close()?;
println!("Incremental mode example completed");
Ok(())
}
fn performance_benchmark(base_path: &PathBuf) -> Result<(), ourdb::Error> {
println!("\n=== Performance Benchmark ===");
let db_path = base_path.join("benchmark");
std::fs::create_dir_all(&db_path)?;
// Create a new database
let config = OurDBConfig {
path: db_path,
@ -141,62 +158,74 @@ fn performance_benchmark(base_path: &PathBuf) -> Result<(), ourdb::Error> {
keysize: Some(4), // 4-byte keys
reset: None, // Don't reset existing database
};
let mut db = OurDB::new(config)?;
// Number of operations for the benchmark
let num_operations = 1000;
let data_size = 100; // bytes per record
// Prepare test data
let test_data = vec![b'A'; data_size];
// Benchmark write operations
println!("Benchmarking {} write operations...", num_operations);
let start = Instant::now();
let mut ids = Vec::with_capacity(num_operations);
for _ in 0..num_operations {
let id = db.set(OurDBSetArgs { id: None, data: &test_data })?;
let id = db.set(OurDBSetArgs {
id: None,
data: &test_data,
})?;
ids.push(id);
}
let write_duration = start.elapsed();
let writes_per_second = num_operations as f64 / write_duration.as_secs_f64();
println!("Write performance: {:.2} ops/sec ({:.2} ms/op)",
writes_per_second,
write_duration.as_secs_f64() * 1000.0 / num_operations as f64);
println!(
"Write performance: {:.2} ops/sec ({:.2} ms/op)",
writes_per_second,
write_duration.as_secs_f64() * 1000.0 / num_operations as f64
);
// Benchmark read operations
println!("Benchmarking {} read operations...", num_operations);
let start = Instant::now();
for &id in &ids {
let _ = db.get(id)?;
}
let read_duration = start.elapsed();
let reads_per_second = num_operations as f64 / read_duration.as_secs_f64();
println!("Read performance: {:.2} ops/sec ({:.2} ms/op)",
reads_per_second,
read_duration.as_secs_f64() * 1000.0 / num_operations as f64);
println!(
"Read performance: {:.2} ops/sec ({:.2} ms/op)",
reads_per_second,
read_duration.as_secs_f64() * 1000.0 / num_operations as f64
);
// Benchmark update operations
println!("Benchmarking {} update operations...", num_operations);
let start = Instant::now();
for &id in &ids {
db.set(OurDBSetArgs { id: Some(id), data: &test_data })?;
db.set(OurDBSetArgs {
id: Some(id),
data: &test_data,
})?;
}
let update_duration = start.elapsed();
let updates_per_second = num_operations as f64 / update_duration.as_secs_f64();
println!("Update performance: {:.2} ops/sec ({:.2} ms/op)",
updates_per_second,
update_duration.as_secs_f64() * 1000.0 / num_operations as f64);
println!(
"Update performance: {:.2} ops/sec ({:.2} ms/op)",
updates_per_second,
update_duration.as_secs_f64() * 1000.0 / num_operations as f64
);
db.close()?;
println!("Performance benchmark completed");
Ok(())
}

View File

@ -4,9 +4,9 @@ fn main() -> Result<(), ourdb::Error> {
// Create a temporary directory for the database
let db_path = std::env::temp_dir().join("ourdb_example");
std::fs::create_dir_all(&db_path)?;
println!("Creating database at: {}", db_path.display());
// Create a new database with incremental mode enabled
let config = OurDBConfig {
path: db_path.clone(),
@ -15,51 +15,68 @@ fn main() -> Result<(), ourdb::Error> {
keysize: None, // Use default (4 bytes)
reset: None, // Don't reset existing database
};
let mut db = OurDB::new(config)?;
// Store some data with auto-generated IDs
let data1 = b"First record";
let id1 = db.set(OurDBSetArgs { id: None, data: data1 })?;
let id1 = db.set(OurDBSetArgs {
id: None,
data: data1,
})?;
println!("Stored first record with ID: {}", id1);
let data2 = b"Second record";
let id2 = db.set(OurDBSetArgs { id: None, data: data2 })?;
let id2 = db.set(OurDBSetArgs {
id: None,
data: data2,
})?;
println!("Stored second record with ID: {}", id2);
// Retrieve and print the data
let retrieved1 = db.get(id1)?;
println!("Retrieved ID {}: {}", id1, String::from_utf8_lossy(&retrieved1));
println!(
"Retrieved ID {}: {}",
id1,
String::from_utf8_lossy(&retrieved1)
);
let retrieved2 = db.get(id2)?;
println!("Retrieved ID {}: {}", id2, String::from_utf8_lossy(&retrieved2));
println!(
"Retrieved ID {}: {}",
id2,
String::from_utf8_lossy(&retrieved2)
);
// Update a record to demonstrate history tracking
let updated_data = b"Updated first record";
db.set(OurDBSetArgs { id: Some(id1), data: updated_data })?;
db.set(OurDBSetArgs {
id: Some(id1),
data: updated_data,
})?;
println!("Updated record with ID: {}", id1);
// Get history for the updated record
let history = db.get_history(id1, 2)?;
println!("History for ID {}:", id1);
for (i, entry) in history.iter().enumerate() {
println!(" Version {}: {}", i, String::from_utf8_lossy(entry));
}
// Delete a record
db.delete(id2)?;
println!("Deleted record with ID: {}", id2);
// Verify deletion
match db.get(id2) {
Ok(_) => println!("Record still exists (unexpected)"),
Err(e) => println!("Verified deletion: {}", e),
}
// Close the database
db.close()?;
println!("Database closed successfully");
// Clean up (optional)
if std::env::var("KEEP_DB").is_err() {
std::fs::remove_dir_all(&db_path)?;
@ -67,6 +84,6 @@ fn main() -> Result<(), ourdb::Error> {
} else {
println!("Database kept at: {}", db_path.display());
}
Ok(())
}

View File

@ -4,12 +4,12 @@ use std::time::Instant;
fn main() -> Result<(), ourdb::Error> {
// Parse command-line arguments
let args: Vec<String> = std::env::args().collect();
// Default values
let mut incremental_mode = true;
let mut keysize: u8 = 4;
let mut num_operations = 10000;
// Parse arguments
for i in 1..args.len() {
if args[i] == "--no-incremental" {
@ -20,13 +20,13 @@ fn main() -> Result<(), ourdb::Error> {
num_operations = args[i + 1].parse().unwrap_or(10000);
}
}
// Create a temporary directory for the database
let db_path = std::env::temp_dir().join("ourdb_benchmark");
std::fs::create_dir_all(&db_path)?;
println!("Database path: {}", db_path.display());
// Create a new database
let config = OurDBConfig {
path: db_path.clone(),
@ -35,73 +35,90 @@ fn main() -> Result<(), ourdb::Error> {
keysize: Some(keysize),
reset: Some(true), // Reset the database for benchmarking
};
let mut db = OurDB::new(config)?;
// Prepare test data (100 bytes per record)
let test_data = vec![b'A'; 100];
// Benchmark write operations
println!("Benchmarking {} write operations (incremental: {}, keysize: {})...",
num_operations, incremental_mode, keysize);
println!(
"Benchmarking {} write operations (incremental: {}, keysize: {})...",
num_operations, incremental_mode, keysize
);
let start = Instant::now();
let mut ids = Vec::with_capacity(num_operations);
for _ in 0..num_operations {
let id = if incremental_mode {
db.set(OurDBSetArgs { id: None, data: &test_data })?
db.set(OurDBSetArgs {
id: None,
data: &test_data,
})?
} else {
// In non-incremental mode, we need to provide IDs
let id = ids.len() as u32 + 1;
db.set(OurDBSetArgs { id: Some(id), data: &test_data })?;
db.set(OurDBSetArgs {
id: Some(id),
data: &test_data,
})?;
id
};
ids.push(id);
}
let write_duration = start.elapsed();
let writes_per_second = num_operations as f64 / write_duration.as_secs_f64();
println!("Write performance: {:.2} ops/sec ({:.2} ms/op)",
writes_per_second,
write_duration.as_secs_f64() * 1000.0 / num_operations as f64);
println!(
"Write performance: {:.2} ops/sec ({:.2} ms/op)",
writes_per_second,
write_duration.as_secs_f64() * 1000.0 / num_operations as f64
);
// Benchmark read operations
println!("Benchmarking {} read operations...", num_operations);
let start = Instant::now();
for &id in &ids {
let _ = db.get(id)?;
}
let read_duration = start.elapsed();
let reads_per_second = num_operations as f64 / read_duration.as_secs_f64();
println!("Read performance: {:.2} ops/sec ({:.2} ms/op)",
reads_per_second,
read_duration.as_secs_f64() * 1000.0 / num_operations as f64);
println!(
"Read performance: {:.2} ops/sec ({:.2} ms/op)",
reads_per_second,
read_duration.as_secs_f64() * 1000.0 / num_operations as f64
);
// Benchmark update operations
println!("Benchmarking {} update operations...", num_operations);
let start = Instant::now();
for &id in &ids {
db.set(OurDBSetArgs { id: Some(id), data: &test_data })?;
db.set(OurDBSetArgs {
id: Some(id),
data: &test_data,
})?;
}
let update_duration = start.elapsed();
let updates_per_second = num_operations as f64 / update_duration.as_secs_f64();
println!("Update performance: {:.2} ops/sec ({:.2} ms/op)",
updates_per_second,
update_duration.as_secs_f64() * 1000.0 / num_operations as f64);
println!(
"Update performance: {:.2} ops/sec ({:.2} ms/op)",
updates_per_second,
update_duration.as_secs_f64() * 1000.0 / num_operations as f64
);
// Clean up
db.close()?;
std::fs::remove_dir_all(&db_path)?;
Ok(())
}

View File

@ -13,9 +13,9 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
.as_secs();
let db_path = temp_dir().join(format!("ourdb_example_{}", timestamp));
std::fs::create_dir_all(&db_path)?;
println!("Creating database at: {}", db_path.display());
// Create a new OurDB instance
let config = OurDBConfig {
path: db_path.clone(),
@ -24,51 +24,60 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
keysize: None,
reset: Some(false),
};
let mut db = OurDB::new(config)?;
println!("Database created successfully");
// Store some data
let test_data = b"Hello, OurDB!";
let id = db.set(OurDBSetArgs { id: None, data: test_data })?;
let id = db.set(OurDBSetArgs {
id: None,
data: test_data,
})?;
println!("\nStored data with ID: {}", id);
// Retrieve the data
let retrieved = db.get(id)?;
println!("Retrieved data: {}", String::from_utf8_lossy(&retrieved));
// Update the data
let updated_data = b"Updated data in OurDB!";
db.set(OurDBSetArgs { id: Some(id), data: updated_data })?;
db.set(OurDBSetArgs {
id: Some(id),
data: updated_data,
})?;
println!("\nUpdated data with ID: {}", id);
// Retrieve the updated data
let retrieved = db.get(id)?;
println!("Retrieved updated data: {}", String::from_utf8_lossy(&retrieved));
println!(
"Retrieved updated data: {}",
String::from_utf8_lossy(&retrieved)
);
// Get history
let history = db.get_history(id, 2)?;
println!("\nHistory for ID {}:", id);
for (i, data) in history.iter().enumerate() {
println!(" Version {}: {}", i + 1, String::from_utf8_lossy(data));
}
// Delete the data
db.delete(id)?;
println!("\nDeleted data with ID: {}", id);
// Try to retrieve the deleted data (should fail)
match db.get(id) {
Ok(_) => println!("Data still exists (unexpected)"),
Err(e) => println!("Verified deletion: {}", e),
}
println!("\nExample completed successfully!");
// Clean up
db.close()?;
std::fs::remove_dir_all(&db_path)?;
println!("Cleaned up database directory");
Ok(())
}

View File

@ -1,7 +1,6 @@
use std::fs::{self, File, OpenOptions};
use std::io::{Read, Seek, SeekFrom, Write};
use crc32fast::Hasher;
use crate::error::Error;
@ -27,11 +26,8 @@ impl OurDB {
}
// Open the file fresh
let file = OpenOptions::new()
.read(true)
.write(true)
.open(&path)?;
let file = OpenOptions::new().read(true).write(true).open(&path)?;
self.file = Some(file);
self.file_nr = file_nr;
@ -42,10 +38,10 @@ impl OurDB {
pub(crate) fn create_new_db_file(&mut self, file_nr: u16) -> Result<(), Error> {
let new_file_path = self.path.join(format!("{}.db", file_nr));
let mut file = File::create(&new_file_path)?;
// Write a single byte to make all positions start from 1
file.write_all(&[0u8])?;
Ok(())
}
@ -54,17 +50,17 @@ impl OurDB {
// For keysize 2, 3, or 4, we can only use file_nr 0
if self.lookup.keysize() <= 4 {
let path = self.path.join("0.db");
if !path.exists() {
self.create_new_db_file(0)?;
}
return Ok(0);
}
// For keysize 6, we can use multiple files
let path = self.path.join(format!("{}.db", self.last_used_file_nr));
if !path.exists() {
self.create_new_db_file(self.last_used_file_nr)?;
return Ok(self.last_used_file_nr);
@ -80,30 +76,36 @@ impl OurDB {
}
/// Stores data at the specified ID with history tracking
pub(crate) fn set_(&mut self, id: u32, old_location: Location, data: &[u8]) -> Result<(), Error> {
pub(crate) fn set_(
&mut self,
id: u32,
old_location: Location,
data: &[u8],
) -> Result<(), Error> {
// Validate data size - maximum is u16::MAX (65535 bytes or ~64KB)
if data.len() > u16::MAX as usize {
return Err(Error::InvalidOperation(
format!("Data size exceeds maximum allowed size of {} bytes", u16::MAX)
));
return Err(Error::InvalidOperation(format!(
"Data size exceeds maximum allowed size of {} bytes",
u16::MAX
)));
}
// Get file number to use
let file_nr = self.get_file_nr()?;
// Select the file
self.db_file_select(file_nr)?;
// Get current file position for lookup
let file = self.file.as_mut().ok_or_else(|| Error::Other("No file open".to_string()))?;
let file = self
.file
.as_mut()
.ok_or_else(|| Error::Other("No file open".to_string()))?;
file.seek(SeekFrom::End(0))?;
let position = file.stream_position()? as u32;
// Create new location
let new_location = Location {
file_nr,
position,
};
let new_location = Location { file_nr, position };
// Calculate CRC of data
let crc = calculate_crc(data);
@ -144,13 +146,19 @@ impl OurDB {
/// Retrieves data at the specified location
pub(crate) fn get_(&mut self, location: Location) -> Result<Vec<u8>, Error> {
if location.position == 0 {
return Err(Error::NotFound(format!("Record not found, location: {:?}", location)));
return Err(Error::NotFound(format!(
"Record not found, location: {:?}",
location
)));
}
// Select the file
self.db_file_select(location.file_nr)?;
let file = self.file.as_mut().ok_or_else(|| Error::Other("No file open".to_string()))?;
let file = self
.file
.as_mut()
.ok_or_else(|| Error::Other("No file open".to_string()))?;
// Read header
file.seek(SeekFrom::Start(location.position as u64))?;
@ -161,10 +169,10 @@ impl OurDB {
let size = u16::from(header[0]) | (u16::from(header[1]) << 8);
// Parse CRC (4 bytes)
let stored_crc = u32::from(header[2])
| (u32::from(header[3]) << 8)
| (u32::from(header[4]) << 16)
| (u32::from(header[5]) << 24);
let stored_crc = u32::from(header[2])
| (u32::from(header[3]) << 8)
| (u32::from(header[4]) << 16)
| (u32::from(header[5]) << 24);
// Read data
let mut data = vec![0u8; size as usize];
@ -173,7 +181,9 @@ impl OurDB {
// Verify CRC
let calculated_crc = calculate_crc(&data);
if calculated_crc != stored_crc {
return Err(Error::DataCorruption("CRC mismatch: data corruption detected".to_string()));
return Err(Error::DataCorruption(
"CRC mismatch: data corruption detected".to_string(),
));
}
Ok(data)
@ -188,7 +198,10 @@ impl OurDB {
// Select the file
self.db_file_select(location.file_nr)?;
let file = self.file.as_mut().ok_or_else(|| Error::Other("No file open".to_string()))?;
let file = self
.file
.as_mut()
.ok_or_else(|| Error::Other("No file open".to_string()))?;
// Skip size and CRC (6 bytes)
file.seek(SeekFrom::Start(location.position as u64 + 6))?;
@ -210,7 +223,10 @@ impl OurDB {
// Select the file
self.db_file_select(location.file_nr)?;
let file = self.file.as_mut().ok_or_else(|| Error::Other("No file open".to_string()))?;
let file = self
.file
.as_mut()
.ok_or_else(|| Error::Other("No file open".to_string()))?;
// Read size first
file.seek(SeekFrom::Start(location.position as u64))?;
@ -240,7 +256,7 @@ impl OurDB {
for entry in fs::read_dir(&self.path)? {
let entry = entry?;
let path = entry.path();
if path.is_file() && path.extension().map_or(false, |ext| ext == "db") {
if let Some(stem) = path.file_stem() {
if let Ok(file_nr) = stem.to_string_lossy().parse::<u16>() {
@ -254,42 +270,42 @@ impl OurDB {
for file_nr in file_numbers {
let src_path = self.path.join(format!("{}.db", file_nr));
let temp_file_path = temp_path.join(format!("{}.db", file_nr));
// Create new file
let mut temp_file = File::create(&temp_file_path)?;
temp_file.write_all(&[0u8])?; // Initialize with a byte
// Open source file
let mut src_file = File::open(&src_path)?;
// Read and process records
let mut buffer = vec![0u8; 1024]; // Read in chunks
let mut _position = 0;
while let Ok(bytes_read) = src_file.read(&mut buffer) {
if bytes_read == 0 {
break;
}
// Process the chunk
// This is a simplified version - in a real implementation,
// you would need to handle records that span chunk boundaries
_position += bytes_read;
}
// TODO: Implement proper record copying and position updating
// This would involve:
// 1. Reading each record from the source file
// 2. If not deleted (all zeros), copy to temp file
// 3. Update lookup table with new positions
}
// TODO: Replace original files with temp files
// Clean up
fs::remove_dir_all(&temp_path)?;
Ok(())
}
}
@ -304,7 +320,7 @@ fn calculate_crc(data: &[u8]) -> u32 {
#[cfg(test)]
mod tests {
use std::path::PathBuf;
use crate::{OurDB, OurDBConfig, OurDBSetArgs};
use std::env::temp_dir;
use std::time::{SystemTime, UNIX_EPOCH};
@ -320,26 +336,30 @@ mod tests {
#[test]
fn test_backend_operations() {
let temp_dir = get_temp_dir();
let config = OurDBConfig {
path: temp_dir.clone(),
incremental_mode: false,
file_size: None,
keysize: None,
reset: None, // Don't reset existing database
reset: None, // Don't reset existing database
};
let mut db = OurDB::new(config).unwrap();
// Test set and get
let test_data = b"Test data for backend operations";
let id = 1;
db.set(OurDBSetArgs { id: Some(id), data: test_data }).unwrap();
db.set(OurDBSetArgs {
id: Some(id),
data: test_data,
})
.unwrap();
let retrieved = db.get(id).unwrap();
assert_eq!(retrieved, test_data);
// Clean up
db.destroy().unwrap();
}

View File

@ -6,23 +6,23 @@ pub enum Error {
/// IO errors from file operations
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
/// Data corruption errors
#[error("Data corruption: {0}")]
DataCorruption(String),
/// Invalid operation errors
#[error("Invalid operation: {0}")]
InvalidOperation(String),
/// Lookup table errors
#[error("Lookup error: {0}")]
LookupError(String),
/// Record not found errors
#[error("Record not found: {0}")]
NotFound(String),
/// Other errors
#[error("Error: {0}")]
Other(String),

View File

@ -1,7 +1,7 @@
mod backend;
mod error;
mod location;
mod lookup;
mod backend;
pub use error::Error;
pub use location::Location;
@ -62,7 +62,7 @@ impl OurDB {
if config.reset.unwrap_or(false) && config.path.exists() {
std::fs::remove_dir_all(&config.path)?;
}
// Create directory if it doesn't exist
std::fs::create_dir_all(&config.path)?;
@ -96,11 +96,11 @@ impl OurDB {
}
/// Sets a value in the database
///
///
/// In incremental mode:
/// - If ID is provided, it updates an existing record
/// - If ID is not provided, it creates a new record with auto-generated ID
///
///
/// In key-value mode:
/// - ID must be provided
pub fn set(&mut self, args: OurDBSetArgs) -> Result<u32, Error> {
@ -110,7 +110,7 @@ impl OurDB {
let location = self.lookup.get(id)?;
if location.position == 0 {
return Err(Error::InvalidOperation(
"Cannot set ID for insertions when incremental mode is enabled".to_string()
"Cannot set ID for insertions when incremental mode is enabled".to_string(),
));
}
@ -124,10 +124,12 @@ impl OurDB {
}
} else {
// Using key-value mode
let id = args.id.ok_or_else(|| Error::InvalidOperation(
"ID must be provided when incremental is disabled".to_string()
))?;
let id = args.id.ok_or_else(|| {
Error::InvalidOperation(
"ID must be provided when incremental is disabled".to_string(),
)
})?;
let location = self.lookup.get(id)?;
self.set_(id, location, args.data)?;
Ok(id)
@ -141,7 +143,7 @@ impl OurDB {
}
/// Retrieves a list of previous values for the specified key
///
///
/// The depth parameter controls how many historical values to retrieve (maximum)
pub fn get_history(&mut self, id: u32, depth: u8) -> Result<Vec<Vec<u8>>, Error> {
let mut result = Vec::new();
@ -179,7 +181,9 @@ impl OurDB {
/// Returns the next ID which will be used when storing in incremental mode
pub fn get_next_id(&mut self) -> Result<u32, Error> {
if !self.incremental_mode {
return Err(Error::InvalidOperation("Incremental mode is not enabled".to_string()));
return Err(Error::InvalidOperation(
"Incremental mode is not enabled".to_string(),
));
}
self.lookup.get_next_id()
}
@ -212,7 +216,8 @@ impl OurDB {
}
fn save(&mut self) -> Result<(), Error> {
self.lookup.export_sparse(&self.lookup_dump_path().to_string_lossy())?;
self.lookup
.export_sparse(&self.lookup_dump_path().to_string_lossy())?;
Ok(())
}
@ -238,41 +243,50 @@ mod tests {
#[test]
fn test_basic_operations() {
let temp_dir = get_temp_dir();
let config = OurDBConfig {
path: temp_dir.clone(),
incremental_mode: true,
file_size: None,
keysize: None,
reset: None, // Don't reset existing database
reset: None, // Don't reset existing database
};
let mut db = OurDB::new(config).unwrap();
// Test set and get
let test_data = b"Hello, OurDB!";
let id = db.set(OurDBSetArgs { id: None, data: test_data }).unwrap();
let id = db
.set(OurDBSetArgs {
id: None,
data: test_data,
})
.unwrap();
let retrieved = db.get(id).unwrap();
assert_eq!(retrieved, test_data);
// Test update
let updated_data = b"Updated data";
db.set(OurDBSetArgs { id: Some(id), data: updated_data }).unwrap();
db.set(OurDBSetArgs {
id: Some(id),
data: updated_data,
})
.unwrap();
let retrieved = db.get(id).unwrap();
assert_eq!(retrieved, updated_data);
// Test history
let history = db.get_history(id, 2).unwrap();
assert_eq!(history.len(), 2);
assert_eq!(history[0], updated_data);
assert_eq!(history[1], test_data);
// Test delete
db.delete(id).unwrap();
assert!(db.get(id).is_err());
// Clean up
db.destroy().unwrap();
}

View File

@ -1,7 +1,7 @@
use crate::error::Error;
/// Location represents a physical position in a database file
///
///
/// It consists of a file number and a position within that file.
/// This allows OurDB to span multiple files for large datasets.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
@ -14,7 +14,7 @@ pub struct Location {
impl Location {
/// Creates a new Location from bytes based on keysize
///
///
/// - keysize = 2: Only position (2 bytes), file_nr = 0
/// - keysize = 3: Only position (3 bytes), file_nr = 0
/// - keysize = 4: Only position (4 bytes), file_nr = 0
@ -22,13 +22,18 @@ impl Location {
pub fn from_bytes(bytes: &[u8], keysize: u8) -> Result<Self, Error> {
// Validate keysize
if ![2, 3, 4, 6].contains(&keysize) {
return Err(Error::InvalidOperation(format!("Invalid keysize: {}", keysize)));
return Err(Error::InvalidOperation(format!(
"Invalid keysize: {}",
keysize
)));
}
// Create padded bytes
let mut padded = vec![0u8; keysize as usize];
if bytes.len() > keysize as usize {
return Err(Error::InvalidOperation("Input bytes exceed keysize".to_string()));
return Err(Error::InvalidOperation(
"Input bytes exceed keysize".to_string(),
));
}
let start_idx = keysize as usize - bytes.len();
@ -49,34 +54,39 @@ impl Location {
// Verify limits
if location.position > 0xFFFF {
return Err(Error::InvalidOperation(
"Position exceeds max value for keysize=2 (max 65535)".to_string()
"Position exceeds max value for keysize=2 (max 65535)".to_string(),
));
}
},
}
3 => {
// Only position, 3 bytes big endian
location.position = u32::from(padded[0]) << 16 | u32::from(padded[1]) << 8 | u32::from(padded[2]);
location.position =
u32::from(padded[0]) << 16 | u32::from(padded[1]) << 8 | u32::from(padded[2]);
location.file_nr = 0;
// Verify limits
if location.position > 0xFFFFFF {
return Err(Error::InvalidOperation(
"Position exceeds max value for keysize=3 (max 16777215)".to_string()
"Position exceeds max value for keysize=3 (max 16777215)".to_string(),
));
}
},
}
4 => {
// Only position, 4 bytes big endian
location.position = u32::from(padded[0]) << 24 | u32::from(padded[1]) << 16
| u32::from(padded[2]) << 8 | u32::from(padded[3]);
location.position = u32::from(padded[0]) << 24
| u32::from(padded[1]) << 16
| u32::from(padded[2]) << 8
| u32::from(padded[3]);
location.file_nr = 0;
},
}
6 => {
// 2 bytes file_nr + 4 bytes position, all big endian
location.file_nr = u16::from(padded[0]) << 8 | u16::from(padded[1]);
location.position = u32::from(padded[2]) << 24 | u32::from(padded[3]) << 16
| u32::from(padded[4]) << 8 | u32::from(padded[5]);
},
location.position = u32::from(padded[2]) << 24
| u32::from(padded[3]) << 16
| u32::from(padded[4]) << 8
| u32::from(padded[5]);
}
_ => unreachable!(),
}
@ -84,26 +94,26 @@ impl Location {
}
/// Converts the location to bytes (always 6 bytes)
///
///
/// Format: [file_nr (2 bytes)][position (4 bytes)]
pub fn to_bytes(&self) -> Vec<u8> {
let mut bytes = Vec::with_capacity(6);
// Put file_nr first (2 bytes)
bytes.push((self.file_nr >> 8) as u8);
bytes.push(self.file_nr as u8);
// Put position next (4 bytes)
bytes.push((self.position >> 24) as u8);
bytes.push((self.position >> 16) as u8);
bytes.push((self.position >> 8) as u8);
bytes.push(self.position as u8);
bytes
}
/// Converts the location to a u64 value
///
///
/// The file_nr is stored in the most significant bits
pub fn to_u64(&self) -> u64 {
(u64::from(self.file_nr) << 32) | u64::from(self.position)

View File

@ -16,7 +16,7 @@ pub struct LookupConfig {
/// - 2: For databases with < 65,536 records (single file)
/// - 3: For databases with < 16,777,216 records (single file)
/// - 4: For databases with < 4,294,967,296 records (single file)
/// - 6: For large databases requiring multiple files
/// - 6: For large databases requiring multiple files
pub keysize: u8,
/// Path for disk-based lookup
pub lookuppath: String,
@ -46,7 +46,10 @@ impl LookupTable {
pub fn new(config: LookupConfig) -> Result<Self, Error> {
// Verify keysize is valid
if ![2, 3, 4, 6].contains(&config.keysize) {
return Err(Error::InvalidOperation(format!("Invalid keysize: {}", config.keysize)));
return Err(Error::InvalidOperation(format!(
"Invalid keysize: {}",
config.keysize
)));
}
let incremental = if config.incremental_mode {
@ -90,7 +93,7 @@ impl LookupTable {
if !self.lookuppath.is_empty() {
// Disk-based lookup
let data_path = Path::new(&self.lookuppath).join(DATA_FILE_NAME);
// Check file size first
let file_size = fs::metadata(&data_path)?.len();
let start_pos = id as u64 * entry_size as u64;
@ -98,7 +101,9 @@ impl LookupTable {
if start_pos + entry_size as u64 > file_size {
return Err(Error::LookupError(format!(
"Invalid read for get in lut: {}: {} would exceed file size {}",
self.lookuppath, start_pos + entry_size as u64, file_size
self.lookuppath,
start_pos + entry_size as u64,
file_size
)));
}
@ -108,14 +113,14 @@ impl LookupTable {
let mut data = vec![0u8; entry_size];
let bytes_read = file.read(&mut data)?;
if bytes_read < entry_size {
return Err(Error::LookupError(format!(
"Incomplete read: expected {} bytes but got {}",
entry_size, bytes_read
)));
}
return Location::from_bytes(&data, self.keysize);
}
@ -126,7 +131,7 @@ impl LookupTable {
let start = (id * self.keysize as u32) as usize;
let end = start + entry_size;
Location::from_bytes(&self.data[start..end], self.keysize)
}
@ -142,7 +147,7 @@ impl LookupTable {
if id > incremental {
return Err(Error::InvalidOperation(
"Cannot set ID for insertions when incremental mode is enabled".to_string()
"Cannot set ID for insertions when incremental mode is enabled".to_string(),
));
}
}
@ -151,53 +156,64 @@ impl LookupTable {
let location_bytes = match self.keysize {
2 => {
if location.file_nr != 0 {
return Err(Error::InvalidOperation("file_nr must be 0 for keysize=2".to_string()));
return Err(Error::InvalidOperation(
"file_nr must be 0 for keysize=2".to_string(),
));
}
if location.position > 0xFFFF {
return Err(Error::InvalidOperation(
"position exceeds max value for keysize=2 (max 65535)".to_string()
"position exceeds max value for keysize=2 (max 65535)".to_string(),
));
}
vec![(location.position >> 8) as u8, location.position as u8]
},
}
3 => {
if location.file_nr != 0 {
return Err(Error::InvalidOperation("file_nr must be 0 for keysize=3".to_string()));
return Err(Error::InvalidOperation(
"file_nr must be 0 for keysize=3".to_string(),
));
}
if location.position > 0xFFFFFF {
return Err(Error::InvalidOperation(
"position exceeds max value for keysize=3 (max 16777215)".to_string()
"position exceeds max value for keysize=3 (max 16777215)".to_string(),
));
}
vec![
(location.position >> 16) as u8,
(location.position >> 8) as u8,
location.position as u8
location.position as u8,
]
},
}
4 => {
if location.file_nr != 0 {
return Err(Error::InvalidOperation("file_nr must be 0 for keysize=4".to_string()));
return Err(Error::InvalidOperation(
"file_nr must be 0 for keysize=4".to_string(),
));
}
vec![
(location.position >> 24) as u8,
(location.position >> 16) as u8,
(location.position >> 8) as u8,
location.position as u8
location.position as u8,
]
},
}
6 => {
// Full location with file_nr and position
location.to_bytes()
},
_ => return Err(Error::InvalidOperation(format!("Invalid keysize: {}", self.keysize))),
}
_ => {
return Err(Error::InvalidOperation(format!(
"Invalid keysize: {}",
self.keysize
)))
}
};
if !self.lookuppath.is_empty() {
// Disk-based lookup
let data_path = Path::new(&self.lookuppath).join(DATA_FILE_NAME);
let mut file = OpenOptions::new().write(true).open(data_path)?;
let start_pos = id as u64 * entry_size as u64;
file.seek(SeekFrom::Start(start_pos))?;
file.write_all(&location_bytes)?;
@ -207,7 +223,7 @@ impl LookupTable {
if start + entry_size > self.data.len() {
return Err(Error::LookupError("Index out of bounds".to_string()));
}
for (i, &byte) in location_bytes.iter().enumerate() {
self.data[start + i] = byte;
}
@ -224,9 +240,9 @@ impl LookupTable {
/// Gets the next available ID in incremental mode
pub fn get_next_id(&self) -> Result<u32, Error> {
let incremental = self.incremental.ok_or_else(||
let incremental = self.incremental.ok_or_else(|| {
Error::InvalidOperation("Lookup table not in incremental mode".to_string())
)?;
})?;
let table_size = if !self.lookuppath.is_empty() {
let data_path = Path::new(&self.lookuppath).join(DATA_FILE_NAME);
@ -244,9 +260,9 @@ impl LookupTable {
/// Increments the index in incremental mode
pub fn increment_index(&mut self) -> Result<(), Error> {
let mut incremental = self.incremental.ok_or_else(||
let mut incremental = self.incremental.ok_or_else(|| {
Error::InvalidOperation("Lookup table not in incremental mode".to_string())
)?;
})?;
incremental += 1;
self.incremental = Some(incremental);
@ -299,10 +315,10 @@ impl LookupTable {
for id in 0..max_entries {
file.seek(SeekFrom::Start(id * entry_size as u64))?;
let mut buffer = vec![0u8; entry_size];
let bytes_read = file.read(&mut buffer)?;
if bytes_read < entry_size {
break;
}
@ -317,11 +333,11 @@ impl LookupTable {
} else {
// For memory-based lookup
let max_entries = self.data.len() / entry_size;
for id in 0..max_entries {
let start = id * entry_size;
let entry = &self.data[start..start + entry_size];
// Check if entry is non-zero
if entry.iter().any(|&b| b != 0) {
// Write ID (4 bytes) + entry
@ -344,7 +360,7 @@ impl LookupTable {
if data.len() % record_size != 0 {
return Err(Error::DataCorruption(
"Invalid sparse data format: size mismatch".to_string()
"Invalid sparse data format: size mismatch".to_string(),
));
}
@ -359,10 +375,10 @@ impl LookupTable {
// Extract entry
let entry = &data[chunk_start + 4..chunk_start + record_size];
// Create location from entry
let location = Location::from_bytes(entry, self.keysize)?;
// Set the entry
self.set(id, location)?;
}
@ -380,13 +396,13 @@ impl LookupTable {
let data_path = Path::new(&self.lookuppath).join(DATA_FILE_NAME);
let mut file = File::open(&data_path)?;
let file_size = fs::metadata(&data_path)?.len();
let mut buffer = vec![0u8; entry_size];
let mut pos = 0u32;
while (pos as u64 * entry_size as u64) < file_size {
file.seek(SeekFrom::Start(pos as u64 * entry_size as u64))?;
let bytes_read = file.read(&mut buffer)?;
if bytes_read == 0 || bytes_read < entry_size {
break;
@ -396,7 +412,7 @@ impl LookupTable {
if location.position != 0 || location.file_nr != 0 {
last_id = pos;
}
pos += 1;
}
} else {
@ -422,7 +438,7 @@ fn get_incremental_info(config: &LookupConfig) -> Result<u32, Error> {
if !config.lookuppath.is_empty() {
let inc_path = Path::new(&config.lookuppath).join(INCREMENTAL_FILE_NAME);
if !inc_path.exists() {
// Create a separate file for storing the incremental value
fs::write(&inc_path, "1")?;
@ -437,7 +453,7 @@ fn get_incremental_info(config: &LookupConfig) -> Result<u32, Error> {
1
}
};
Ok(incremental)
} else {
// For memory-based lookup, start with 1
@ -447,9 +463,9 @@ fn get_incremental_info(config: &LookupConfig) -> Result<u32, Error> {
#[cfg(test)]
mod tests {
use std::path::PathBuf;
use super::*;
use std::env::temp_dir;
use std::path::PathBuf;
use std::time::{SystemTime, UNIX_EPOCH};
fn get_temp_dir() -> PathBuf {
@ -468,25 +484,25 @@ mod tests {
lookuppath: String::new(),
incremental_mode: true,
};
let mut lookup = LookupTable::new(config).unwrap();
// Test set and get
let location = Location {
file_nr: 0,
position: 12345,
};
lookup.set(1, location).unwrap();
let retrieved = lookup.get(1).unwrap();
assert_eq!(retrieved.file_nr, location.file_nr);
assert_eq!(retrieved.position, location.position);
// Test incremental mode
let next_id = lookup.get_next_id().unwrap();
assert_eq!(next_id, 2);
lookup.increment_index().unwrap();
let next_id = lookup.get_next_id().unwrap();
assert_eq!(next_id, 3);
@ -496,28 +512,28 @@ mod tests {
fn test_disk_lookup() {
let temp_dir = get_temp_dir();
fs::create_dir_all(&temp_dir).unwrap();
let config = LookupConfig {
size: 1000,
keysize: 4,
lookuppath: temp_dir.to_string_lossy().to_string(),
incremental_mode: true,
};
let mut lookup = LookupTable::new(config).unwrap();
// Test set and get
let location = Location {
file_nr: 0,
position: 12345,
};
lookup.set(1, location).unwrap();
let retrieved = lookup.get(1).unwrap();
assert_eq!(retrieved.file_nr, location.file_nr);
assert_eq!(retrieved.position, location.position);
// Clean up
fs::remove_dir_all(temp_dir).unwrap();
}

View File

@ -1,9 +1,9 @@
use ourdb::{OurDB, OurDBConfig, OurDBSetArgs};
use rand;
use std::env::temp_dir;
use std::fs;
use std::path::PathBuf;
use std::time::{SystemTime, UNIX_EPOCH};
use rand;
// Helper function to create a unique temporary directory for tests
fn get_temp_dir() -> PathBuf {
@ -13,56 +13,64 @@ fn get_temp_dir() -> PathBuf {
.as_nanos();
let random_part = rand::random::<u32>();
let dir = temp_dir().join(format!("ourdb_test_{}_{}", timestamp, random_part));
// Ensure the directory exists and is empty
if dir.exists() {
std::fs::remove_dir_all(&dir).unwrap();
}
std::fs::create_dir_all(&dir).unwrap();
dir
}
#[test]
fn test_basic_operations() {
let temp_dir = get_temp_dir();
// Create a new database with incremental mode
let config = OurDBConfig {
path: temp_dir.clone(),
incremental_mode: true,
file_size: None,
keysize: None,
reset: None
reset: None,
};
let mut db = OurDB::new(config).unwrap();
// Test set and get
let test_data = b"Hello, OurDB!";
let id = db.set(OurDBSetArgs { id: None, data: test_data }).unwrap();
let id = db
.set(OurDBSetArgs {
id: None,
data: test_data,
})
.unwrap();
let retrieved = db.get(id).unwrap();
assert_eq!(retrieved, test_data);
// Test update
let updated_data = b"Updated data";
db.set(OurDBSetArgs { id: Some(id), data: updated_data }).unwrap();
db.set(OurDBSetArgs {
id: Some(id),
data: updated_data,
})
.unwrap();
let retrieved = db.get(id).unwrap();
assert_eq!(retrieved, updated_data);
// Test history
let history = db.get_history(id, 2).unwrap();
assert_eq!(history.len(), 2);
assert_eq!(history[0], updated_data);
assert_eq!(history[1], test_data);
// Test delete
db.delete(id).unwrap();
assert!(db.get(id).is_err());
// Clean up
db.destroy().unwrap();
}
@ -70,30 +78,33 @@ fn test_basic_operations() {
#[test]
fn test_key_value_mode() {
let temp_dir = get_temp_dir();
// Create a new database with key-value mode
let config = OurDBConfig {
path: temp_dir.clone(),
incremental_mode: false,
file_size: None,
keysize: None,
reset: None
reset: None,
};
let mut db = OurDB::new(config).unwrap();
// Test set with explicit ID
let test_data = b"Key-value data";
let id = 42;
db.set(OurDBSetArgs { id: Some(id), data: test_data }).unwrap();
db.set(OurDBSetArgs {
id: Some(id),
data: test_data,
})
.unwrap();
let retrieved = db.get(id).unwrap();
assert_eq!(retrieved, test_data);
// Verify next_id fails in key-value mode
assert!(db.get_next_id().is_err());
// Clean up
db.destroy().unwrap();
}
@ -101,33 +112,42 @@ fn test_key_value_mode() {
#[test]
fn test_incremental_mode() {
let temp_dir = get_temp_dir();
// Create a new database with incremental mode
let config = OurDBConfig {
path: temp_dir.clone(),
incremental_mode: true,
file_size: None,
keysize: None,
reset: None
reset: None,
};
let mut db = OurDB::new(config).unwrap();
// Test auto-increment IDs
let data1 = b"First record";
let id1 = db.set(OurDBSetArgs { id: None, data: data1 }).unwrap();
let id1 = db
.set(OurDBSetArgs {
id: None,
data: data1,
})
.unwrap();
let data2 = b"Second record";
let id2 = db.set(OurDBSetArgs { id: None, data: data2 }).unwrap();
let id2 = db
.set(OurDBSetArgs {
id: None,
data: data2,
})
.unwrap();
// IDs should be sequential
assert_eq!(id2, id1 + 1);
// Verify get_next_id works
let next_id = db.get_next_id().unwrap();
assert_eq!(next_id, id2 + 1);
// Clean up
db.destroy().unwrap();
}
@ -135,8 +155,7 @@ fn test_incremental_mode() {
#[test]
fn test_persistence() {
let temp_dir = get_temp_dir();
// Create data in a new database
{
let config = OurDBConfig {
@ -144,21 +163,26 @@ fn test_persistence() {
incremental_mode: true,
file_size: None,
keysize: None,
reset: None
reset: None,
};
let mut db = OurDB::new(config).unwrap();
let test_data = b"Persistent data";
let id = db.set(OurDBSetArgs { id: None, data: test_data }).unwrap();
let id = db
.set(OurDBSetArgs {
id: None,
data: test_data,
})
.unwrap();
// Explicitly close the database
db.close().unwrap();
// ID should be 1 in a new database
assert_eq!(id, 1);
}
// Reopen the database and verify data persists
{
let config = OurDBConfig {
@ -166,19 +190,19 @@ fn test_persistence() {
incremental_mode: true,
file_size: None,
keysize: None,
reset: None
reset: None,
};
let mut db = OurDB::new(config).unwrap();
// Verify data is still there
let retrieved = db.get(1).unwrap();
assert_eq!(retrieved, b"Persistent data");
// Verify incremental counter persisted
let next_id = db.get_next_id().unwrap();
assert_eq!(next_id, 2);
// Clean up
db.destroy().unwrap();
}
@ -188,28 +212,33 @@ fn test_persistence() {
fn test_different_keysizes() {
for keysize in [2, 3, 4, 6].iter() {
let temp_dir = get_temp_dir();
// Ensure the directory exists
std::fs::create_dir_all(&temp_dir).unwrap();
// Create a new database with specified keysize
let config = OurDBConfig {
path: temp_dir.clone(),
incremental_mode: true,
file_size: None,
keysize: Some(*keysize),
reset: None
reset: None,
};
let mut db = OurDB::new(config).unwrap();
// Test basic operations
let test_data = b"Keysize test data";
let id = db.set(OurDBSetArgs { id: None, data: test_data }).unwrap();
let id = db
.set(OurDBSetArgs {
id: None,
data: test_data,
})
.unwrap();
let retrieved = db.get(id).unwrap();
assert_eq!(retrieved, test_data);
// Clean up
db.destroy().unwrap();
}
@ -218,28 +247,33 @@ fn test_different_keysizes() {
#[test]
fn test_large_data() {
let temp_dir = get_temp_dir();
// Create a new database
let config = OurDBConfig {
path: temp_dir.clone(),
incremental_mode: true,
file_size: None,
keysize: None,
reset: None
reset: None,
};
let mut db = OurDB::new(config).unwrap();
// Create a large data set (60KB - within the 64KB limit)
let large_data = vec![b'X'; 60 * 1024];
// Store and retrieve large data
let id = db.set(OurDBSetArgs { id: None, data: &large_data }).unwrap();
let id = db
.set(OurDBSetArgs {
id: None,
data: &large_data,
})
.unwrap();
let retrieved = db.get(id).unwrap();
assert_eq!(retrieved.len(), large_data.len());
assert_eq!(retrieved, large_data);
// Clean up
db.destroy().unwrap();
}
@ -247,27 +281,33 @@ fn test_large_data() {
#[test]
fn test_exceed_size_limit() {
let temp_dir = get_temp_dir();
// Create a new database
let config = OurDBConfig {
path: temp_dir.clone(),
incremental_mode: true,
file_size: None,
keysize: None,
reset: None
reset: None,
};
let mut db = OurDB::new(config).unwrap();
// Create data larger than the 64KB limit (70KB)
let oversized_data = vec![b'X'; 70 * 1024];
// Attempt to store data that exceeds the size limit
let result = db.set(OurDBSetArgs { id: None, data: &oversized_data });
let result = db.set(OurDBSetArgs {
id: None,
data: &oversized_data,
});
// Verify that an error is returned
assert!(result.is_err(), "Expected an error when storing data larger than 64KB");
assert!(
result.is_err(),
"Expected an error when storing data larger than 64KB"
);
// Clean up
db.destroy().unwrap();
}
@ -275,46 +315,55 @@ fn test_exceed_size_limit() {
#[test]
fn test_multiple_files() {
let temp_dir = get_temp_dir();
// Create a new database with small file size to force multiple files
let config = OurDBConfig {
path: temp_dir.clone(),
incremental_mode: true,
file_size: Some(1024), // Very small file size (1KB)
keysize: Some(6), // 6-byte keysize for multiple files
reset: None
reset: None,
};
let mut db = OurDB::new(config).unwrap();
// Store enough data to span multiple files
let data_size = 500; // bytes per record
let test_data = vec![b'A'; data_size];
let mut ids = Vec::new();
for _ in 0..10 {
let id = db.set(OurDBSetArgs { id: None, data: &test_data }).unwrap();
let id = db
.set(OurDBSetArgs {
id: None,
data: &test_data,
})
.unwrap();
ids.push(id);
}
// Verify all data can be retrieved
for &id in &ids {
let retrieved = db.get(id).unwrap();
assert_eq!(retrieved.len(), data_size);
}
// Verify multiple files were created
let files = fs::read_dir(&temp_dir).unwrap()
let files = fs::read_dir(&temp_dir)
.unwrap()
.filter_map(Result::ok)
.filter(|entry| {
let path = entry.path();
path.is_file() && path.extension().map_or(false, |ext| ext == "db")
})
.count();
assert!(files > 1, "Expected multiple database files, found {}", files);
assert!(
files > 1,
"Expected multiple database files, found {}",
files
);
// Clean up
db.destroy().unwrap();
}

View File

@ -1,23 +1,23 @@
use proc_macro::TokenStream;
use quote::{quote, format_ident};
use syn::{parse_macro_input, ItemFn, FnArg, Pat, PatType, ReturnType, parse_quote};
use quote::{format_ident, quote};
use syn::{parse_macro_input, parse_quote, FnArg, ItemFn, Pat, PatType, ReturnType};
/// Procedural macro that generates a Rhai client function for a Rust function.
///
///
/// When applied to a Rust function, it generates a corresponding function with a '_rhai_client' suffix
/// that calls the original function through the Rhai engine.
///
///
/// # Example
///
///
/// ```rust
/// #[rhai]
/// fn hello(name: String) -> String {
/// format!("Hello, {}!", name)
/// }
/// ```
///
///
/// This will generate:
///
///
/// ```rust
/// fn hello_rhai_client(engine: &rhai::Engine, name: String) -> String {
/// let script = format!("hello(\"{}\")", name.replace("\"", "\\\""));
@ -27,7 +27,7 @@ use syn::{parse_macro_input, ItemFn, FnArg, Pat, PatType, ReturnType, parse_quot
/// })
/// }
/// ```
///
///
/// Note: The macro handles type conversions between Rust and Rhai types,
/// particularly for integer types (Rhai uses i64 internally).
#[proc_macro_attribute]
@ -36,15 +36,15 @@ pub fn rhai(_attr: TokenStream, item: TokenStream) -> TokenStream {
let input_fn = parse_macro_input!(item as ItemFn);
let fn_name = &input_fn.sig.ident;
let fn_name_str = fn_name.to_string();
// Create the client function name (original + _rhai_client)
let client_fn_name = format_ident!("{}_rhai_client", fn_name);
// Extract function parameters
let mut param_names = Vec::new();
let mut param_types = Vec::new();
let mut param_declarations = Vec::new();
for arg in &input_fn.sig.inputs {
match arg {
FnArg::Typed(PatType { pat, ty, .. }) => {
@ -61,48 +61,55 @@ pub fn rhai(_attr: TokenStream, item: TokenStream) -> TokenStream {
}
}
}
// Determine return type
let return_type = match &input_fn.sig.output {
ReturnType::Default => parse_quote!(()),
ReturnType::Type(_, ty) => ty.clone(),
};
// Generate parameter formatting for the Rhai script
let param_format_strings = param_names.iter().zip(param_types.iter()).map(|(name, ty)| {
let type_str = quote! { #ty }.to_string();
// Handle different parameter types
if type_str.contains("String") {
quote! {
format!("\"{}\"" , #name.replace("\"", "\\\""))
let param_format_strings = param_names
.iter()
.zip(param_types.iter())
.map(|(name, ty)| {
let type_str = quote! { #ty }.to_string();
// Handle different parameter types
if type_str.contains("String") {
quote! {
format!("\"{}\"" , #name.replace("\"", "\\\""))
}
} else if type_str.contains("bool") {
quote! {
format!("{}", #name)
}
} else if type_str.contains("i32") || type_str.contains("u32") {
// Convert smaller integer types to i64 for Rhai
quote! {
format!("{}", #name as i64)
}
} else if type_str.contains("i64")
|| type_str.contains("u64")
|| type_str.contains("f32")
|| type_str.contains("f64")
{
// Other numeric types
quote! {
format!("{}", #name)
}
} else {
// For complex types, just pass the variable name
// The Rhai engine will handle the conversion
quote! {
#name.to_string()
}
}
} else if type_str.contains("bool") {
quote! {
format!("{}", #name)
}
} else if type_str.contains("i32") || type_str.contains("u32") {
// Convert smaller integer types to i64 for Rhai
quote! {
format!("{}", #name as i64)
}
} else if type_str.contains("i64") || type_str.contains("u64") || type_str.contains("f32") || type_str.contains("f64") {
// Other numeric types
quote! {
format!("{}", #name)
}
} else {
// For complex types, just pass the variable name
// The Rhai engine will handle the conversion
quote! {
#name.to_string()
}
}
});
});
// Determine if the return type needs conversion
let return_type_str = quote! { #return_type }.to_string();
// Generate the client function with appropriate type conversions
let client_fn = if return_type_str.contains("i32") || return_type_str.contains("u32") {
// For integer return types that need conversion
@ -113,7 +120,7 @@ pub fn rhai(_attr: TokenStream, item: TokenStream) -> TokenStream {
#fn_name_str,
&[#(#param_format_strings),*].join(", ")
);
match engine.eval::<i64>(&script) {
Ok(result) => result as #return_type,
Err(err) => {
@ -132,7 +139,7 @@ pub fn rhai(_attr: TokenStream, item: TokenStream) -> TokenStream {
#fn_name_str,
&[#(#param_format_strings),*].join(", ")
);
match engine.eval::<#return_type>(&script) {
Ok(result) => result,
Err(err) => {
@ -151,7 +158,7 @@ pub fn rhai(_attr: TokenStream, item: TokenStream) -> TokenStream {
#fn_name_str,
&[#(#param_format_strings),*].join(", ")
);
match engine.eval::<#return_type>(&script) {
Ok(result) => result,
Err(err) => {
@ -170,7 +177,7 @@ pub fn rhai(_attr: TokenStream, item: TokenStream) -> TokenStream {
#fn_name_str,
&[#(#param_format_strings),*].join(", ")
);
match engine.eval::<#return_type>(&script) {
Ok(result) => result,
Err(err) => {
@ -181,19 +188,19 @@ pub fn rhai(_attr: TokenStream, item: TokenStream) -> TokenStream {
}
}
};
// Combine the original function and the generated client function
let output = quote! {
#input_fn
#client_fn
};
output.into()
}
/// A more advanced version of the rhai macro that handles different parameter types better.
///
///
/// This version properly escapes strings and handles different parameter types more accurately.
/// It's recommended to use this version for more complex functions.
#[proc_macro_attribute]
@ -202,15 +209,15 @@ pub fn rhai_advanced(_attr: TokenStream, item: TokenStream) -> TokenStream {
let input_fn = parse_macro_input!(item as ItemFn);
let fn_name = &input_fn.sig.ident;
let fn_name_str = fn_name.to_string();
// Create the client function name (original + _rhai_client)
let client_fn_name = format_ident!("{}_rhai_client", fn_name);
// Extract function parameters
let mut param_names = Vec::new();
let mut param_types = Vec::new();
let mut param_declarations = Vec::new();
for arg in &input_fn.sig.inputs {
match arg {
FnArg::Typed(PatType { pat, ty, .. }) => {
@ -227,48 +234,53 @@ pub fn rhai_advanced(_attr: TokenStream, item: TokenStream) -> TokenStream {
}
}
}
// Determine return type
let return_type = match &input_fn.sig.output {
ReturnType::Default => parse_quote!(()),
ReturnType::Type(_, ty) => ty.clone(),
};
// Generate parameter formatting for the Rhai script
let param_format_expressions = param_names.iter().zip(param_types.iter()).map(|(name, ty)| {
let type_str = quote! { #ty }.to_string();
// Handle different parameter types
if type_str.contains("String") {
quote! {
format!("\"{}\"", #name.replace("\"", "\\\""))
let param_format_expressions = param_names
.iter()
.zip(param_types.iter())
.map(|(name, ty)| {
let type_str = quote! { #ty }.to_string();
// Handle different parameter types
if type_str.contains("String") {
quote! {
format!("\"{}\"", #name.replace("\"", "\\\""))
}
} else if type_str.contains("bool") {
quote! {
format!("{}", #name)
}
} else if type_str.contains("i32") || type_str.contains("u32") {
// Convert smaller integer types to i64 for Rhai
quote! {
format!("{}", #name as i64)
}
} else if type_str.contains("i") || type_str.contains("u") || type_str.contains("f") {
// Other numeric types
quote! {
format!("{}", #name)
}
} else {
// Default for other types
quote! {
format!("{:?}", #name)
}
}
} else if type_str.contains("bool") {
quote! {
format!("{}", #name)
}
} else if type_str.contains("i32") || type_str.contains("u32") {
// Convert smaller integer types to i64 for Rhai
quote! {
format!("{}", #name as i64)
}
} else if type_str.contains("i") || type_str.contains("u") || type_str.contains("f") {
// Other numeric types
quote! {
format!("{}", #name)
}
} else {
// Default for other types
quote! {
format!("{:?}", #name)
}
}
}).collect::<Vec<_>>();
})
.collect::<Vec<_>>();
// Determine if the return type needs conversion
let return_type_str = quote! { #return_type }.to_string();
let needs_return_conversion = return_type_str.contains("i32") || return_type_str.contains("u32");
let needs_return_conversion =
return_type_str.contains("i32") || return_type_str.contains("u32");
// Generate the client function with appropriate type conversions
let client_fn = if needs_return_conversion {
quote! {
@ -278,7 +290,7 @@ pub fn rhai_advanced(_attr: TokenStream, item: TokenStream) -> TokenStream {
#fn_name_str,
&[#(#param_format_expressions),*].join(", ")
);
match engine.eval::<i64>(&script) {
Ok(result) => result as #return_type,
Err(err) => {
@ -296,7 +308,7 @@ pub fn rhai_advanced(_attr: TokenStream, item: TokenStream) -> TokenStream {
#fn_name_str,
&[#(#param_format_expressions),*].join(", ")
);
match engine.eval::<#return_type>(&script) {
Ok(result) => result,
Err(err) => {
@ -307,19 +319,19 @@ pub fn rhai_advanced(_attr: TokenStream, item: TokenStream) -> TokenStream {
}
}
};
// Combine the original function and the generated client function
let output = quote! {
#input_fn
#client_fn
};
output.into()
}
/// Macro that generates a module with Rhai client functions for all functions in scope.
///
///
/// This macro should be used at the module level to generate Rhai client functions for all
/// functions marked with the #[rhai] attribute.
#[proc_macro]
@ -329,20 +341,20 @@ pub fn generate_rhai_module(_item: TokenStream) -> TokenStream {
// client functions for all of them.
//
// For simplicity, we'll just return a placeholder implementation
let output = quote! {
/// Register all functions marked with #[rhai] in this module with the Rhai engine.
///
///
/// This function handles type conversions between Rust and Rhai types automatically.
/// For example, it converts between Rust's i32 and Rhai's i64 types.
pub fn register_rhai_functions(engine: &mut rhai::Engine) {
// This would be generated based on the functions in the module
println!("Registering Rhai functions...");
// In a real implementation, this would iterate through all functions
// marked with #[rhai] and register them with the engine.
}
};
output.into()
}

View File

@ -1,16 +1,16 @@
use tst::TST;
use std::time::Instant;
use tst::TST;
fn main() -> Result<(), tst::Error> {
// Create a temporary directory for the database
let db_path = std::env::temp_dir().join("tst_example");
std::fs::create_dir_all(&db_path)?;
println!("Creating ternary search tree at: {}", db_path.display());
// Create a new TST
let mut tree = TST::new(db_path.to_str().unwrap(), true)?;
// Store some data
println!("Inserting data...");
tree.set("hello", b"world".to_vec())?;
@ -19,50 +19,50 @@ fn main() -> Result<(), tst::Error> {
tree.set("apple", b"fruit".to_vec())?;
tree.set("application", b"software".to_vec())?;
tree.set("banana", b"yellow".to_vec())?;
// Retrieve and print the data
let value = tree.get("hello")?;
println!("hello: {}", String::from_utf8_lossy(&value));
// List keys with prefix
println!("\nListing keys with prefix 'hel':");
let start = Instant::now();
let keys = tree.list("hel")?;
let duration = start.elapsed();
for key in &keys {
println!(" {}", key);
}
println!("Found {} keys in {:?}", keys.len(), duration);
// Get all values with prefix
println!("\nGetting all values with prefix 'app':");
let start = Instant::now();
let values = tree.getall("app")?;
let duration = start.elapsed();
for (i, value) in values.iter().enumerate() {
println!(" Value {}: {}", i + 1, String::from_utf8_lossy(value));
}
println!("Found {} values in {:?}", values.len(), duration);
// Delete a key
println!("\nDeleting 'help'...");
tree.delete("help")?;
// Verify deletion
println!("Listing keys with prefix 'hel' after deletion:");
let keys_after = tree.list("hel")?;
for key in &keys_after {
println!(" {}", key);
}
// Try to get a deleted key
match tree.get("help") {
Ok(_) => println!("Unexpectedly found 'help' after deletion!"),
Err(e) => println!("As expected, 'help' was not found: {}", e),
}
// Clean up (optional)
if std::env::var("KEEP_DB").is_err() {
std::fs::remove_dir_all(&db_path)?;
@ -70,6 +70,6 @@ fn main() -> Result<(), tst::Error> {
} else {
println!("\nDatabase kept at: {}", db_path.display());
}
Ok(())
}
}

View File

@ -1,20 +1,20 @@
use tst::TST;
use std::time::{Duration, Instant};
use std::io::{self, Write};
use std::time::{Duration, Instant};
use tst::TST;
// Function to generate a test value of specified size
fn generate_test_value(index: usize, size: usize) -> Vec<u8> {
let base_value = format!("val{:08}", index);
let mut value = Vec::with_capacity(size);
// Fill with repeating pattern to reach desired size
while value.len() < size {
value.extend_from_slice(base_value.as_bytes());
}
// Truncate to exact size
value.truncate(size);
value
}
@ -28,39 +28,39 @@ const PERFORMANCE_SAMPLE_SIZE: usize = 100;
fn main() -> Result<(), tst::Error> {
// Create a temporary directory for the database
let db_path = std::env::temp_dir().join("tst_performance_test");
// Completely remove and recreate the directory to ensure a clean start
if db_path.exists() {
std::fs::remove_dir_all(&db_path)?;
}
std::fs::create_dir_all(&db_path)?;
println!("Creating ternary search tree at: {}", db_path.display());
println!("Will insert {} records and show progress...", TOTAL_RECORDS);
// Create a new TST
let mut tree = TST::new(db_path.to_str().unwrap(), true)?;
// Track overall time
let start_time = Instant::now();
// Track performance metrics
let mut insertion_times = Vec::with_capacity(TOTAL_RECORDS / PROGRESS_INTERVAL);
let mut last_batch_time = Instant::now();
let mut last_batch_records = 0;
// Insert records and track progress
for i in 0..TOTAL_RECORDS {
let key = format!("key:{:08}", i);
// Generate a 100-byte value
let value = generate_test_value(i, 100);
// Time the insertion of every Nth record for performance sampling
if i % PERFORMANCE_SAMPLE_SIZE == 0 {
let insert_start = Instant::now();
tree.set(&key, value)?;
let insert_duration = insert_start.elapsed();
// Only print detailed timing for specific samples to avoid flooding output
if i % (PERFORMANCE_SAMPLE_SIZE * 10) == 0 {
println!("Record {}: Insertion took {:?}", i, insert_duration);
@ -68,76 +68,93 @@ fn main() -> Result<(), tst::Error> {
} else {
tree.set(&key, value)?;
}
// Show progress at intervals
if (i + 1) % PROGRESS_INTERVAL == 0 || i == TOTAL_RECORDS - 1 {
let records_in_batch = i + 1 - last_batch_records;
let batch_duration = last_batch_time.elapsed();
let records_per_second = records_in_batch as f64 / batch_duration.as_secs_f64();
insertion_times.push((i + 1, batch_duration));
print!("\rProgress: {}/{} records ({:.2}%) - {:.2} records/sec",
i + 1, TOTAL_RECORDS,
(i + 1) as f64 / TOTAL_RECORDS as f64 * 100.0,
records_per_second);
print!(
"\rProgress: {}/{} records ({:.2}%) - {:.2} records/sec",
i + 1,
TOTAL_RECORDS,
(i + 1) as f64 / TOTAL_RECORDS as f64 * 100.0,
records_per_second
);
io::stdout().flush().unwrap();
last_batch_time = Instant::now();
last_batch_records = i + 1;
}
}
let total_duration = start_time.elapsed();
println!("\n\nPerformance Summary:");
println!("Total time to insert {} records: {:?}", TOTAL_RECORDS, total_duration);
println!("Average insertion rate: {:.2} records/second",
TOTAL_RECORDS as f64 / total_duration.as_secs_f64());
println!(
"Total time to insert {} records: {:?}",
TOTAL_RECORDS, total_duration
);
println!(
"Average insertion rate: {:.2} records/second",
TOTAL_RECORDS as f64 / total_duration.as_secs_f64()
);
// Show performance trend
println!("\nPerformance Trend (records inserted vs. time per batch):");
for (i, (record_count, duration)) in insertion_times.iter().enumerate() {
if i % 10 == 0 || i == insertion_times.len() - 1 { // Only show every 10th point to avoid too much output
println!(" After {} records: {:?} for {} records ({:.2} records/sec)",
record_count,
duration,
PROGRESS_INTERVAL,
PROGRESS_INTERVAL as f64 / duration.as_secs_f64());
if i % 10 == 0 || i == insertion_times.len() - 1 {
// Only show every 10th point to avoid too much output
println!(
" After {} records: {:?} for {} records ({:.2} records/sec)",
record_count,
duration,
PROGRESS_INTERVAL,
PROGRESS_INTERVAL as f64 / duration.as_secs_f64()
);
}
}
// Test access performance with distributed samples
println!("\nTesting access performance with distributed samples...");
let mut total_get_time = Duration::new(0, 0);
let num_samples = 1000;
// Use a simple distribution pattern instead of random
for i in 0..num_samples {
// Distribute samples across the entire range
let sample_id = (i * (TOTAL_RECORDS / num_samples)) % TOTAL_RECORDS;
let key = format!("key:{:08}", sample_id);
let get_start = Instant::now();
let _ = tree.get(&key)?;
total_get_time += get_start.elapsed();
}
println!("Average time to retrieve a record: {:?}",
total_get_time / num_samples as u32);
println!(
"Average time to retrieve a record: {:?}",
total_get_time / num_samples as u32
);
// Test prefix search performance
println!("\nTesting prefix search performance...");
let prefixes = ["key:0", "key:1", "key:5", "key:9"];
for prefix in &prefixes {
let list_start = Instant::now();
let keys = tree.list(prefix)?;
let list_duration = list_start.elapsed();
println!("Found {} keys with prefix '{}' in {:?}",
keys.len(), prefix, list_duration);
println!(
"Found {} keys with prefix '{}' in {:?}",
keys.len(),
prefix,
list_duration
);
}
// Clean up (optional)
if std::env::var("KEEP_DB").is_err() {
std::fs::remove_dir_all(&db_path)?;
@ -145,6 +162,6 @@ fn main() -> Result<(), tst::Error> {
} else {
println!("\nDatabase kept at: {}", db_path.display());
}
Ok(())
}
}

View File

@ -1,82 +1,137 @@
use tst::TST;
use std::time::Instant;
use tst::TST;
fn main() -> Result<(), tst::Error> {
// Create a temporary directory for the database
let db_path = std::env::temp_dir().join("tst_prefix_example");
std::fs::create_dir_all(&db_path)?;
println!("Creating ternary search tree at: {}", db_path.display());
// Create a new TST
let mut tree = TST::new(db_path.to_str().unwrap(), true)?;
// Insert a variety of keys with different prefixes
println!("Inserting data with various prefixes...");
// Names
let names = [
"Alice", "Alexander", "Amanda", "Andrew", "Amy",
"Bob", "Barbara", "Benjamin", "Brenda", "Brian",
"Charlie", "Catherine", "Christopher", "Cynthia", "Carl",
"David", "Diana", "Daniel", "Deborah", "Donald",
"Edward", "Elizabeth", "Eric", "Emily", "Ethan"
"Alice",
"Alexander",
"Amanda",
"Andrew",
"Amy",
"Bob",
"Barbara",
"Benjamin",
"Brenda",
"Brian",
"Charlie",
"Catherine",
"Christopher",
"Cynthia",
"Carl",
"David",
"Diana",
"Daniel",
"Deborah",
"Donald",
"Edward",
"Elizabeth",
"Eric",
"Emily",
"Ethan",
];
for (i, name) in names.iter().enumerate() {
let value = format!("person-{}", i).into_bytes();
tree.set(name, value)?;
}
// Cities
let cities = [
"New York", "Los Angeles", "Chicago", "Houston", "Phoenix",
"Philadelphia", "San Antonio", "San Diego", "Dallas", "San Jose",
"Austin", "Jacksonville", "Fort Worth", "Columbus", "San Francisco",
"Charlotte", "Indianapolis", "Seattle", "Denver", "Washington"
"New York",
"Los Angeles",
"Chicago",
"Houston",
"Phoenix",
"Philadelphia",
"San Antonio",
"San Diego",
"Dallas",
"San Jose",
"Austin",
"Jacksonville",
"Fort Worth",
"Columbus",
"San Francisco",
"Charlotte",
"Indianapolis",
"Seattle",
"Denver",
"Washington",
];
for (i, city) in cities.iter().enumerate() {
let value = format!("city-{}", i).into_bytes();
tree.set(city, value)?;
}
// Countries
let countries = [
"United States", "Canada", "Mexico", "Brazil", "Argentina",
"United Kingdom", "France", "Germany", "Italy", "Spain",
"China", "Japan", "India", "Australia", "Russia"
"United States",
"Canada",
"Mexico",
"Brazil",
"Argentina",
"United Kingdom",
"France",
"Germany",
"Italy",
"Spain",
"China",
"Japan",
"India",
"Australia",
"Russia",
];
for (i, country) in countries.iter().enumerate() {
let value = format!("country-{}", i).into_bytes();
tree.set(country, value)?;
}
println!("Total items inserted: {}", names.len() + cities.len() + countries.len());
println!(
"Total items inserted: {}",
names.len() + cities.len() + countries.len()
);
// Test prefix operations
test_prefix(&mut tree, "A")?;
test_prefix(&mut tree, "B")?;
test_prefix(&mut tree, "C")?;
test_prefix(&mut tree, "San")?;
test_prefix(&mut tree, "United")?;
// Test non-existent prefix
test_prefix(&mut tree, "Z")?;
// Test empty prefix (should return all keys)
println!("\nTesting empty prefix (should return all keys):");
let start = Instant::now();
let all_keys = tree.list("")?;
let duration = start.elapsed();
println!("Found {} keys with empty prefix in {:?}", all_keys.len(), duration);
println!(
"Found {} keys with empty prefix in {:?}",
all_keys.len(),
duration
);
println!("First 5 keys (alphabetically):");
for key in all_keys.iter().take(5) {
println!(" {}", key);
}
// Clean up (optional)
if std::env::var("KEEP_DB").is_err() {
std::fs::remove_dir_all(&db_path)?;
@ -84,39 +139,46 @@ fn main() -> Result<(), tst::Error> {
} else {
println!("\nDatabase kept at: {}", db_path.display());
}
Ok(())
}
fn test_prefix(tree: &mut TST, prefix: &str) -> Result<(), tst::Error> {
println!("\nTesting prefix '{}':", prefix);
// Test list operation
let start = Instant::now();
let keys = tree.list(prefix)?;
let list_duration = start.elapsed();
println!("Found {} keys with prefix '{}' in {:?}", keys.len(), prefix, list_duration);
println!(
"Found {} keys with prefix '{}' in {:?}",
keys.len(),
prefix,
list_duration
);
if !keys.is_empty() {
println!("Keys:");
for key in &keys {
println!(" {}", key);
}
// Test getall operation
let start = Instant::now();
let values = tree.getall(prefix)?;
let getall_duration = start.elapsed();
println!("Retrieved {} values in {:?}", values.len(), getall_duration);
println!("First value: {}",
if !values.is_empty() {
String::from_utf8_lossy(&values[0])
} else {
"None".into()
});
println!(
"First value: {}",
if !values.is_empty() {
String::from_utf8_lossy(&values[0])
} else {
"None".into()
}
);
}
Ok(())
}
}

View File

@ -1,7 +1,7 @@
//! Error types for the TST module.
use thiserror::Error;
use std::io;
use thiserror::Error;
/// Error type for TST operations.
#[derive(Debug, Error)]
@ -9,28 +9,28 @@ pub enum Error {
/// Error from OurDB operations.
#[error("OurDB error: {0}")]
OurDB(#[from] ourdb::Error),
/// Error when a key is not found.
#[error("Key not found: {0}")]
KeyNotFound(String),
/// Error when a prefix is not found.
#[error("Prefix not found: {0}")]
PrefixNotFound(String),
/// Error during serialization.
#[error("Serialization error: {0}")]
Serialization(String),
/// Error during deserialization.
#[error("Deserialization error: {0}")]
Deserialization(String),
/// Error for invalid operations.
#[error("Invalid operation: {0}")]
InvalidOperation(String),
/// IO error.
#[error("IO error: {0}")]
IO(#[from] io::Error),
}
}

View File

@ -18,7 +18,7 @@ use ourdb::OurDB;
pub struct TST {
/// Database for persistent storage
db: OurDB,
/// Database ID of the root node
root_id: Option<u32>,
}
@ -119,4 +119,4 @@ impl TST {
pub fn getall(&mut self, prefix: &str) -> Result<Vec<Vec<u8>>, Error> {
operations::getall(self, prefix)
}
}
}

View File

@ -5,19 +5,19 @@
pub struct TSTNode {
/// The character stored at this node.
pub character: char,
/// Value stored at this node (empty if not end of key).
pub value: Vec<u8>,
/// Whether this node represents the end of a key.
pub is_end_of_key: bool,
/// Reference to the left child node (for characters < current character).
pub left_id: Option<u32>,
/// Reference to the middle child node (for next character in key).
pub middle_id: Option<u32>,
/// Reference to the right child node (for characters > current character).
pub right_id: Option<u32>,
}
@ -34,7 +34,7 @@ impl TSTNode {
right_id: None,
}
}
/// Creates a new root node.
pub fn new_root() -> Self {
Self {
@ -46,4 +46,4 @@ impl TSTNode {
right_id: None,
}
}
}
}

View File

@ -9,19 +9,19 @@ use std::path::PathBuf;
/// Creates a new TST with the specified database path.
pub fn new_tst(path: &str, reset: bool) -> Result<TST, Error> {
let path_buf = PathBuf::from(path);
// Create the configuration for OurDB with reset parameter
let config = OurDBConfig {
path: path_buf.clone(),
incremental_mode: true,
file_size: Some(1024 * 1024), // 1MB file size for better performance with large datasets
keysize: Some(4), // Use keysize=4 (default)
reset: Some(reset), // Use the reset parameter
keysize: Some(4), // Use keysize=4 (default)
reset: Some(reset), // Use the reset parameter
};
// Create a new OurDB instance (it will handle reset internally)
let mut db = OurDB::new(config)?;
let root_id = if db.get_next_id()? == 1 || reset {
// Create a new root node
let root = TSTNode::new_root();
@ -29,17 +29,14 @@ pub fn new_tst(path: &str, reset: bool) -> Result<TST, Error> {
id: None,
data: &root.serialize(),
})?;
Some(root_id)
} else {
// Use existing root node
Some(1) // Root node always has ID 1
};
Ok(TST {
db,
root_id,
})
Ok(TST { db, root_id })
}
/// Sets a key-value pair in the tree.
@ -47,45 +44,51 @@ pub fn set(tree: &mut TST, key: &str, value: Vec<u8>) -> Result<(), Error> {
if key.is_empty() {
return Err(Error::InvalidOperation("Empty key not allowed".to_string()));
}
let root_id = match tree.root_id {
Some(id) => id,
None => return Err(Error::InvalidOperation("Tree not initialized".to_string())),
};
let chars: Vec<char> = key.chars().collect();
set_recursive(tree, root_id, &chars, 0, value)?;
Ok(())
}
/// Recursive helper function for setting a key-value pair.
fn set_recursive(tree: &mut TST, node_id: u32, chars: &[char], pos: usize, value: Vec<u8>) -> Result<u32, Error> {
fn set_recursive(
tree: &mut TST,
node_id: u32,
chars: &[char],
pos: usize,
value: Vec<u8>,
) -> Result<u32, Error> {
let mut node = tree.get_node(node_id)?;
if pos >= chars.len() {
// We've reached the end of the key
node.is_end_of_key = true;
node.value = value;
return tree.save_node(Some(node_id), &node);
}
let current_char = chars[pos];
if node.character == '\0' {
// Root node or empty node, set the character
node.character = current_char;
let node_id = tree.save_node(Some(node_id), &node)?;
// Continue with the next character
if pos + 1 < chars.len() {
let new_node = TSTNode::new(chars[pos + 1], Vec::new(), false);
let new_id = tree.save_node(None, &new_node)?;
let mut updated_node = tree.get_node(node_id)?;
updated_node.middle_id = Some(new_id);
tree.save_node(Some(node_id), &updated_node)?;
return set_recursive(tree, new_id, chars, pos + 1, value);
} else {
// This is the last character
@ -95,7 +98,7 @@ fn set_recursive(tree: &mut TST, node_id: u32, chars: &[char], pos: usize, value
return tree.save_node(Some(node_id), &updated_node);
}
}
if current_char < node.character {
// Go left
if let Some(left_id) = node.left_id {
@ -104,11 +107,11 @@ fn set_recursive(tree: &mut TST, node_id: u32, chars: &[char], pos: usize, value
// Create new left node
let new_node = TSTNode::new(current_char, Vec::new(), false);
let new_id = tree.save_node(None, &new_node)?;
// Update current node
node.left_id = Some(new_id);
tree.save_node(Some(node_id), &node)?;
return set_recursive(tree, new_id, chars, pos, value);
}
} else if current_char > node.character {
@ -119,11 +122,11 @@ fn set_recursive(tree: &mut TST, node_id: u32, chars: &[char], pos: usize, value
// Create new right node
let new_node = TSTNode::new(current_char, Vec::new(), false);
let new_id = tree.save_node(None, &new_node)?;
// Update current node
node.right_id = Some(new_id);
tree.save_node(Some(node_id), &node)?;
return set_recursive(tree, new_id, chars, pos, value);
}
} else {
@ -134,18 +137,18 @@ fn set_recursive(tree: &mut TST, node_id: u32, chars: &[char], pos: usize, value
node.value = value;
return tree.save_node(Some(node_id), &node);
}
if let Some(middle_id) = node.middle_id {
return set_recursive(tree, middle_id, chars, pos + 1, value);
} else {
// Create new middle node
let new_node = TSTNode::new(chars[pos + 1], Vec::new(), false);
let new_id = tree.save_node(None, &new_node)?;
// Update current node
node.middle_id = Some(new_id);
tree.save_node(Some(node_id), &node)?;
return set_recursive(tree, new_id, chars, pos + 1, value);
}
}
@ -156,15 +159,15 @@ pub fn get(tree: &mut TST, key: &str) -> Result<Vec<u8>, Error> {
if key.is_empty() {
return Err(Error::InvalidOperation("Empty key not allowed".to_string()));
}
let root_id = match tree.root_id {
Some(id) => id,
None => return Err(Error::InvalidOperation("Tree not initialized".to_string())),
};
let chars: Vec<char> = key.chars().collect();
let node_id = find_node(tree, root_id, &chars, 0)?;
let node = tree.get_node(node_id)?;
if node.is_end_of_key {
Ok(node.value.clone())
@ -176,13 +179,13 @@ pub fn get(tree: &mut TST, key: &str) -> Result<Vec<u8>, Error> {
/// Finds a node by key.
fn find_node(tree: &mut TST, node_id: u32, chars: &[char], pos: usize) -> Result<u32, Error> {
let node = tree.get_node(node_id)?;
if pos >= chars.len() {
return Ok(node_id);
}
let current_char = chars[pos];
if current_char < node.character {
// Go left
if let Some(left_id) = node.left_id {
@ -216,21 +219,21 @@ pub fn delete(tree: &mut TST, key: &str) -> Result<(), Error> {
if key.is_empty() {
return Err(Error::InvalidOperation("Empty key not allowed".to_string()));
}
let root_id = match tree.root_id {
Some(id) => id,
None => return Err(Error::InvalidOperation("Tree not initialized".to_string())),
};
let chars: Vec<char> = key.chars().collect();
let node_id = find_node(tree, root_id, &chars, 0)?;
let mut node = tree.get_node(node_id)?;
if !node.is_end_of_key {
return Err(Error::KeyNotFound(key.to_string()));
}
// If the node has a middle child, just mark it as not end of key
if node.middle_id.is_some() || node.left_id.is_some() || node.right_id.is_some() {
node.is_end_of_key = false;
@ -238,14 +241,14 @@ pub fn delete(tree: &mut TST, key: &str) -> Result<(), Error> {
tree.save_node(Some(node_id), &node)?;
return Ok(());
}
// Otherwise, we need to remove the node and update its parent
// This is more complex and would require tracking the path to the node
// For simplicity, we'll just mark it as not end of key for now
node.is_end_of_key = false;
node.value = Vec::new();
tree.save_node(Some(node_id), &node)?;
Ok(())
}
@ -255,46 +258,51 @@ pub fn list(tree: &mut TST, prefix: &str) -> Result<Vec<String>, Error> {
Some(id) => id,
None => return Err(Error::InvalidOperation("Tree not initialized".to_string())),
};
let mut result = Vec::new();
// Handle empty prefix case - will return all keys
if prefix.is_empty() {
collect_all_keys(tree, root_id, String::new(), &mut result)?;
return Ok(result);
}
// Find the node corresponding to the prefix
let chars: Vec<char> = prefix.chars().collect();
let node_id = match find_prefix_node(tree, root_id, &chars, 0) {
Ok(id) => id,
Err(_) => return Ok(Vec::new()), // Prefix not found, return empty list
};
// For empty prefix, we start with an empty string
// For non-empty prefix, we start with the prefix minus the last character
// (since the last character is in the node we found)
let prefix_base = if chars.len() > 1 {
chars[0..chars.len()-1].iter().collect()
chars[0..chars.len() - 1].iter().collect()
} else {
String::new()
};
// Collect all keys from the subtree
collect_keys_with_prefix(tree, node_id, prefix_base, &mut result)?;
Ok(result)
}
/// Finds the node corresponding to a prefix.
fn find_prefix_node(tree: &mut TST, node_id: u32, chars: &[char], pos: usize) -> Result<u32, Error> {
fn find_prefix_node(
tree: &mut TST,
node_id: u32,
chars: &[char],
pos: usize,
) -> Result<u32, Error> {
if pos >= chars.len() {
return Ok(node_id);
}
let node = tree.get_node(node_id)?;
let current_char = chars[pos];
if current_char < node.character {
// Go left
if let Some(left_id) = node.left_id {
@ -331,32 +339,32 @@ fn collect_keys_with_prefix(
result: &mut Vec<String>,
) -> Result<(), Error> {
let node = tree.get_node(node_id)?;
let mut new_path = current_path.clone();
// For non-root nodes, add the character to the path
if node.character != '\0' {
new_path.push(node.character);
}
// If this node is an end of key, add it to the result
if node.is_end_of_key {
result.push(new_path.clone());
}
// Recursively collect keys from all children
if let Some(left_id) = node.left_id {
collect_keys_with_prefix(tree, left_id, current_path.clone(), result)?;
}
if let Some(middle_id) = node.middle_id {
collect_keys_with_prefix(tree, middle_id, new_path.clone(), result)?;
}
if let Some(right_id) = node.right_id {
collect_keys_with_prefix(tree, right_id, current_path.clone(), result)?;
}
Ok(())
}
@ -368,32 +376,32 @@ fn collect_all_keys(
result: &mut Vec<String>,
) -> Result<(), Error> {
let node = tree.get_node(node_id)?;
let mut new_path = current_path.clone();
// Skip adding the character for the root node
if node.character != '\0' {
new_path.push(node.character);
}
// If this node is an end of key, add it to the result
if node.is_end_of_key {
result.push(new_path.clone());
}
// Recursively collect keys from all children
if let Some(left_id) = node.left_id {
collect_all_keys(tree, left_id, current_path.clone(), result)?;
}
if let Some(middle_id) = node.middle_id {
collect_all_keys(tree, middle_id, new_path.clone(), result)?;
}
if let Some(right_id) = node.right_id {
collect_all_keys(tree, right_id, current_path.clone(), result)?;
}
Ok(())
}
@ -401,23 +409,23 @@ fn collect_all_keys(
pub fn getall(tree: &mut TST, prefix: &str) -> Result<Vec<Vec<u8>>, Error> {
// Get all matching keys
let keys = list(tree, prefix)?;
// Get values for each key
let mut values = Vec::new();
let mut errors = Vec::new();
for key in keys {
match get(tree, &key) {
Ok(value) => values.push(value),
Err(e) => errors.push(format!("Error getting value for key '{}': {:?}", key, e))
Err(e) => errors.push(format!("Error getting value for key '{}': {:?}", key, e)),
}
}
// If we couldn't get any values but had keys, return the first error
if values.is_empty() && !errors.is_empty() {
return Err(Error::InvalidOperation(errors.join("; ")));
}
Ok(values)
}
@ -442,4 +450,4 @@ impl TST {
Err(err) => Err(Error::OurDB(err)),
}
}
}
}

View File

@ -10,17 +10,17 @@ impl TSTNode {
/// Serializes a node to bytes for storage.
pub fn serialize(&self) -> Vec<u8> {
let mut buffer = Vec::new();
// Version
buffer.push(VERSION);
// Character (as UTF-32)
let char_bytes = (self.character as u32).to_le_bytes();
buffer.extend_from_slice(&char_bytes);
// Is end of key
buffer.push(if self.is_end_of_key { 1 } else { 0 });
// Value (only if is_end_of_key)
if self.is_end_of_key {
let value_len = (self.value.len() as u32).to_le_bytes();
@ -30,88 +30,100 @@ impl TSTNode {
// Zero length
buffer.extend_from_slice(&[0, 0, 0, 0]);
}
// Child pointers
let left_id = self.left_id.unwrap_or(0).to_le_bytes();
buffer.extend_from_slice(&left_id);
let middle_id = self.middle_id.unwrap_or(0).to_le_bytes();
buffer.extend_from_slice(&middle_id);
let right_id = self.right_id.unwrap_or(0).to_le_bytes();
buffer.extend_from_slice(&right_id);
buffer
}
/// Deserializes bytes to a node.
pub fn deserialize(data: &[u8]) -> Result<Self, Error> {
if data.len() < 14 { // Minimum size: version + char + is_end + value_len + 3 child IDs
if data.len() < 14 {
// Minimum size: version + char + is_end + value_len + 3 child IDs
return Err(Error::Deserialization("Data too short".to_string()));
}
let mut pos = 0;
// Version
let version = data[pos];
pos += 1;
if version != VERSION {
return Err(Error::Deserialization(format!("Unsupported version: {}", version)));
return Err(Error::Deserialization(format!(
"Unsupported version: {}",
version
)));
}
// Character
let char_bytes = [data[pos], data[pos+1], data[pos+2], data[pos+3]];
let char_bytes = [data[pos], data[pos + 1], data[pos + 2], data[pos + 3]];
let char_code = u32::from_le_bytes(char_bytes);
let character = char::from_u32(char_code)
.ok_or_else(|| Error::Deserialization("Invalid character".to_string()))?;
pos += 4;
// Is end of key
let is_end_of_key = data[pos] != 0;
pos += 1;
// Value length
let value_len_bytes = [data[pos], data[pos+1], data[pos+2], data[pos+3]];
let value_len_bytes = [data[pos], data[pos + 1], data[pos + 2], data[pos + 3]];
let value_len = u32::from_le_bytes(value_len_bytes) as usize;
pos += 4;
// Value
let value = if value_len > 0 {
if pos + value_len > data.len() {
return Err(Error::Deserialization("Value length exceeds data".to_string()));
return Err(Error::Deserialization(
"Value length exceeds data".to_string(),
));
}
data[pos..pos+value_len].to_vec()
data[pos..pos + value_len].to_vec()
} else {
Vec::new()
};
pos += value_len;
// Child pointers
if pos + 12 > data.len() {
return Err(Error::Deserialization("Data too short for child pointers".to_string()));
return Err(Error::Deserialization(
"Data too short for child pointers".to_string(),
));
}
let left_id_bytes = [data[pos], data[pos+1], data[pos+2], data[pos+3]];
let left_id_bytes = [data[pos], data[pos + 1], data[pos + 2], data[pos + 3]];
let left_id = u32::from_le_bytes(left_id_bytes);
pos += 4;
let middle_id_bytes = [data[pos], data[pos+1], data[pos+2], data[pos+3]];
let middle_id_bytes = [data[pos], data[pos + 1], data[pos + 2], data[pos + 3]];
let middle_id = u32::from_le_bytes(middle_id_bytes);
pos += 4;
let right_id_bytes = [data[pos], data[pos+1], data[pos+2], data[pos+3]];
let right_id_bytes = [data[pos], data[pos + 1], data[pos + 2], data[pos + 3]];
let right_id = u32::from_le_bytes(right_id_bytes);
Ok(TSTNode {
character,
value,
is_end_of_key,
left_id: if left_id == 0 { None } else { Some(left_id) },
middle_id: if middle_id == 0 { None } else { Some(middle_id) },
middle_id: if middle_id == 0 {
None
} else {
Some(middle_id)
},
right_id: if right_id == 0 { None } else { Some(right_id) },
})
}
}
// Function removed as it was unused
// Function removed as it was unused

View File

@ -1,24 +1,24 @@
use tst::TST;
use std::env::temp_dir;
use std::fs;
use std::time::SystemTime;
use tst::TST;
fn get_test_db_path() -> String {
let timestamp = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap()
.as_nanos();
let path = temp_dir().join(format!("tst_test_{}", timestamp));
// If the path exists, remove it first
if path.exists() {
let _ = fs::remove_dir_all(&path);
}
// Create the directory
fs::create_dir_all(&path).unwrap();
path.to_string_lossy().to_string()
}
@ -30,44 +30,44 @@ fn cleanup_test_db(path: &str) {
#[test]
fn test_create_tst() {
let path = get_test_db_path();
let result = TST::new(&path, true);
match &result {
Ok(_) => (),
Err(e) => println!("Error creating TST: {:?}", e),
}
assert!(result.is_ok());
if let Ok(mut tst) = result {
// Make sure we can perform a basic operation
let set_result = tst.set("test_key", b"test_value".to_vec());
assert!(set_result.is_ok());
}
cleanup_test_db(&path);
}
#[test]
fn test_set_and_get() {
let path = get_test_db_path();
// Create a new TST with reset=true to ensure a clean state
let result = TST::new(&path, true);
assert!(result.is_ok());
let mut tree = result.unwrap();
// Test setting and getting a key
let key = "test_key";
let value = b"test_value".to_vec();
let set_result = tree.set(key, value.clone());
assert!(set_result.is_ok());
let get_result = tree.get(key);
assert!(get_result.is_ok());
assert_eq!(get_result.unwrap(), value);
// Make sure to clean up properly
cleanup_test_db(&path);
}
@ -75,45 +75,45 @@ fn test_set_and_get() {
#[test]
fn test_get_nonexistent_key() {
let path = get_test_db_path();
let mut tree = TST::new(&path, true).unwrap();
// Test getting a key that doesn't exist
let get_result = tree.get("nonexistent_key");
assert!(get_result.is_err());
cleanup_test_db(&path);
}
#[test]
fn test_delete() {
let path = get_test_db_path();
// Create a new TST with reset=true to ensure a clean state
let result = TST::new(&path, true);
assert!(result.is_ok());
let mut tree = result.unwrap();
// Set a key
let key = "delete_test";
let value = b"to_be_deleted".to_vec();
let set_result = tree.set(key, value);
assert!(set_result.is_ok());
// Verify it exists
let get_result = tree.get(key);
assert!(get_result.is_ok());
// Delete it
let delete_result = tree.delete(key);
assert!(delete_result.is_ok());
// Verify it's gone
let get_after_delete = tree.get(key);
assert!(get_after_delete.is_err());
// Make sure to clean up properly
cleanup_test_db(&path);
}
@ -121,28 +121,28 @@ fn test_delete() {
#[test]
fn test_multiple_keys() {
let path = get_test_db_path();
// Create a new TST with reset=true to ensure a clean state
let result = TST::new(&path, true);
assert!(result.is_ok());
let mut tree = result.unwrap();
// Insert multiple keys - use fewer keys to avoid filling the lookup table
let keys = ["apple", "banana", "cherry"];
for (i, key) in keys.iter().enumerate() {
let value = format!("value_{}", i).into_bytes();
let set_result = tree.set(key, value);
// Print error if set fails
if set_result.is_err() {
println!("Error setting key '{}': {:?}", key, set_result);
}
assert!(set_result.is_ok());
}
// Verify all keys exist
for (i, key) in keys.iter().enumerate() {
let expected_value = format!("value_{}", i).into_bytes();
@ -150,7 +150,7 @@ fn test_multiple_keys() {
assert!(get_result.is_ok());
assert_eq!(get_result.unwrap(), expected_value);
}
// Make sure to clean up properly
cleanup_test_db(&path);
}
@ -158,56 +158,53 @@ fn test_multiple_keys() {
#[test]
fn test_list_prefix() {
let path = get_test_db_path();
// Create a new TST with reset=true to ensure a clean state
let result = TST::new(&path, true);
assert!(result.is_ok());
let mut tree = result.unwrap();
// Insert keys with common prefixes - use fewer keys to avoid filling the lookup table
let keys = [
"apple", "application", "append",
"banana", "bandana"
];
let keys = ["apple", "application", "append", "banana", "bandana"];
for key in &keys {
let set_result = tree.set(key, key.as_bytes().to_vec());
assert!(set_result.is_ok());
}
// Test prefix "app"
let list_result = tree.list("app");
assert!(list_result.is_ok());
let app_keys = list_result.unwrap();
// Print the keys for debugging
println!("Keys with prefix 'app':");
for key in &app_keys {
println!(" {}", key);
}
// Check that each key is present
assert!(app_keys.contains(&"apple".to_string()));
assert!(app_keys.contains(&"application".to_string()));
assert!(app_keys.contains(&"append".to_string()));
// Test prefix "ban"
let list_result = tree.list("ban");
assert!(list_result.is_ok());
let ban_keys = list_result.unwrap();
assert!(ban_keys.contains(&"banana".to_string()));
assert!(ban_keys.contains(&"bandana".to_string()));
// Test non-existent prefix
let list_result = tree.list("z");
assert!(list_result.is_ok());
let z_keys = list_result.unwrap();
assert_eq!(z_keys.len(), 0);
// Make sure to clean up properly
cleanup_test_db(&path);
}
@ -215,46 +212,44 @@ fn test_list_prefix() {
#[test]
fn test_getall_prefix() {
let path = get_test_db_path();
// Create a new TST with reset=true to ensure a clean state
let result = TST::new(&path, true);
assert!(result.is_ok());
let mut tree = result.unwrap();
// Insert keys with common prefixes - use fewer keys to avoid filling the lookup table
let keys = [
"apple", "application", "append"
];
let keys = ["apple", "application", "append"];
for key in &keys {
let set_result = tree.set(key, key.as_bytes().to_vec());
assert!(set_result.is_ok());
}
// Test getall with prefix "app"
let getall_result = tree.getall("app");
assert!(getall_result.is_ok());
let app_values = getall_result.unwrap();
// Convert values to strings for easier comparison
let app_value_strings: Vec<String> = app_values
.iter()
.map(|v| String::from_utf8_lossy(v).to_string())
.collect();
// Print the values for debugging
println!("Values with prefix 'app':");
for value in &app_value_strings {
println!(" {}", value);
}
// Check that each value is present
assert!(app_value_strings.contains(&"apple".to_string()));
assert!(app_value_strings.contains(&"application".to_string()));
assert!(app_value_strings.contains(&"append".to_string()));
// Make sure to clean up properly
cleanup_test_db(&path);
}
@ -262,38 +257,38 @@ fn test_getall_prefix() {
#[test]
fn test_empty_prefix() {
let path = get_test_db_path();
// Create a new TST with reset=true to ensure a clean state
let result = TST::new(&path, true);
assert!(result.is_ok());
let mut tree = result.unwrap();
// Insert some keys
let keys = ["apple", "banana", "cherry"];
for key in &keys {
let set_result = tree.set(key, key.as_bytes().to_vec());
assert!(set_result.is_ok());
}
// Test list with empty prefix (should return all keys)
let list_result = tree.list("");
assert!(list_result.is_ok());
let all_keys = list_result.unwrap();
// Print the keys for debugging
println!("Keys with empty prefix:");
for key in &all_keys {
println!(" {}", key);
}
// Check that each key is present
for key in &keys {
assert!(all_keys.contains(&key.to_string()));
}
// Make sure to clean up properly
cleanup_test_db(&path);
}
}

View File

@ -1,24 +1,24 @@
use tst::TST;
use std::env::temp_dir;
use std::fs;
use std::time::SystemTime;
use tst::TST;
fn get_test_db_path() -> String {
let timestamp = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap()
.as_nanos();
let path = temp_dir().join(format!("tst_prefix_test_{}", timestamp));
// If the path exists, remove it first
if path.exists() {
let _ = fs::remove_dir_all(&path);
}
// Create the directory
fs::create_dir_all(&path).unwrap();
path.to_string_lossy().to_string()
}
@ -30,9 +30,9 @@ fn cleanup_test_db(path: &str) {
#[test]
fn test_prefix_with_common_prefixes() {
let path = get_test_db_path();
let mut tree = TST::new(&path, true).unwrap();
// Insert keys with common prefixes
let test_data = [
("test", b"value1".to_vec()),
@ -41,34 +41,34 @@ fn test_prefix_with_common_prefixes() {
("tests", b"value4".to_vec()),
("tester", b"value5".to_vec()),
];
for (key, value) in &test_data {
tree.set(key, value.clone()).unwrap();
}
// Test prefix "test"
let keys = tree.list("test").unwrap();
assert_eq!(keys.len(), 5);
for (key, _) in &test_data {
assert!(keys.contains(&key.to_string()));
}
// Test prefix "teste"
let keys = tree.list("teste").unwrap();
assert_eq!(keys.len(), 2);
assert!(keys.contains(&"tested".to_string()));
assert!(keys.contains(&"tester".to_string()));
cleanup_test_db(&path);
}
#[test]
fn test_prefix_with_different_prefixes() {
let path = get_test_db_path();
let mut tree = TST::new(&path, true).unwrap();
// Insert keys with different prefixes
let test_data = [
("apple", b"fruit1".to_vec()),
@ -77,64 +77,64 @@ fn test_prefix_with_different_prefixes() {
("date", b"fruit4".to_vec()),
("elderberry", b"fruit5".to_vec()),
];
for (key, value) in &test_data {
tree.set(key, value.clone()).unwrap();
}
// Test each prefix
for (key, _) in &test_data {
let prefix = &key[0..1]; // First character
let keys = tree.list(prefix).unwrap();
assert!(keys.contains(&key.to_string()));
}
// Test non-existent prefix
let keys = tree.list("z").unwrap();
assert_eq!(keys.len(), 0);
cleanup_test_db(&path);
}
#[test]
fn test_prefix_with_empty_string() {
let path = get_test_db_path();
// Create a new TST with reset=true to ensure a clean state
let result = TST::new(&path, true);
assert!(result.is_ok());
let mut tree = result.unwrap();
// Insert some keys
let test_data = [
("apple", b"fruit1".to_vec()),
("banana", b"fruit2".to_vec()),
("cherry", b"fruit3".to_vec()),
];
for (key, value) in &test_data {
let set_result = tree.set(key, value.clone());
assert!(set_result.is_ok());
}
// Test empty prefix (should return all keys)
let list_result = tree.list("");
assert!(list_result.is_ok());
let keys = list_result.unwrap();
// Print the keys for debugging
println!("Keys with empty prefix:");
for key in &keys {
println!(" {}", key);
}
// Check that each key is present
for (key, _) in &test_data {
assert!(keys.contains(&key.to_string()));
}
// Make sure to clean up properly
cleanup_test_db(&path);
}
@ -142,9 +142,9 @@ fn test_prefix_with_empty_string() {
#[test]
fn test_getall_with_prefix() {
let path = get_test_db_path();
let mut tree = TST::new(&path, true).unwrap();
// Insert keys with common prefixes
let test_data = [
("test", b"value1".to_vec()),
@ -153,28 +153,28 @@ fn test_getall_with_prefix() {
("tests", b"value4".to_vec()),
("tester", b"value5".to_vec()),
];
for (key, value) in &test_data {
tree.set(key, value.clone()).unwrap();
}
// Test getall with prefix "test"
let values = tree.getall("test").unwrap();
assert_eq!(values.len(), 5);
for (_, value) in &test_data {
assert!(values.contains(value));
}
cleanup_test_db(&path);
}
#[test]
fn test_prefix_with_unicode_characters() {
let path = get_test_db_path();
let mut tree = TST::new(&path, true).unwrap();
// Insert keys with Unicode characters
let test_data = [
("café", b"coffee".to_vec()),
@ -182,77 +182,86 @@ fn test_prefix_with_unicode_characters() {
("caffè", b"italian coffee".to_vec()),
("café au lait", b"coffee with milk".to_vec()),
];
for (key, value) in &test_data {
tree.set(key, value.clone()).unwrap();
}
// Test prefix "café"
let keys = tree.list("café").unwrap();
// Print the keys for debugging
println!("Keys with prefix 'café':");
for key in &keys {
println!(" {}", key);
}
// Check that the keys we expect are present
assert!(keys.contains(&"café".to_string()));
assert!(keys.contains(&"café au lait".to_string()));
// We don't assert on the exact count because Unicode handling can vary
// Test prefix "caf"
let keys = tree.list("caf").unwrap();
// Print the keys for debugging
println!("Keys with prefix 'caf':");
for key in &keys {
println!(" {}", key);
}
// Check that each key is present individually
// Due to Unicode handling, we need to be careful with exact matching
// The important thing is that we can find the keys we need
// Check that we have at least the café and café au lait keys
assert!(keys.contains(&"café".to_string()));
assert!(keys.contains(&"café au lait".to_string()));
// We don't assert on the exact count because Unicode handling can vary
cleanup_test_db(&path);
}
#[test]
fn test_prefix_with_long_keys() {
let path = get_test_db_path();
let mut tree = TST::new(&path, true).unwrap();
// Insert long keys
let test_data = [
("this_is_a_very_long_key_for_testing_purposes_1", b"value1".to_vec()),
("this_is_a_very_long_key_for_testing_purposes_2", b"value2".to_vec()),
("this_is_a_very_long_key_for_testing_purposes_3", b"value3".to_vec()),
(
"this_is_a_very_long_key_for_testing_purposes_1",
b"value1".to_vec(),
),
(
"this_is_a_very_long_key_for_testing_purposes_2",
b"value2".to_vec(),
),
(
"this_is_a_very_long_key_for_testing_purposes_3",
b"value3".to_vec(),
),
("this_is_another_long_key_for_testing", b"value4".to_vec()),
];
for (key, value) in &test_data {
tree.set(key, value.clone()).unwrap();
}
// Test prefix "this_is_a_very"
let keys = tree.list("this_is_a_very").unwrap();
assert_eq!(keys.len(), 3);
// Test prefix "this_is"
let keys = tree.list("this_is").unwrap();
assert_eq!(keys.len(), 4);
for (key, _) in &test_data {
assert!(keys.contains(&key.to_string()));
}
cleanup_test_db(&path);
}
}