diff --git a/herodb/Cargo.lock b/herodb/Cargo.lock index d45ba75..18eb964 100644 --- a/herodb/Cargo.lock +++ b/herodb/Cargo.lock @@ -658,6 +658,7 @@ dependencies = [ "bincode", "brotli", "chrono", + "lazy_static", "paste", "poem", "poem-openapi", @@ -831,6 +832,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + [[package]] name = "libc" version = "0.2.171" diff --git a/herodb/Cargo.toml b/herodb/Cargo.toml index c228f10..7318bfc 100644 --- a/herodb/Cargo.toml +++ b/herodb/Cargo.toml @@ -21,6 +21,7 @@ poem-openapi = { version = "2.0.11", features = ["swagger-ui"] } tokio = { version = "1", features = ["full"] } rhai = "1.21.0" paste = "1.0" +lazy_static = "1.4.0" [[example]] name = "rhai_demo" @@ -29,3 +30,7 @@ path = "examples/rhai_demo.rs" [[bin]] name = "dbexample2" path = "src/cmd/dbexample2/main.rs" + +[[bin]] +name = "dbexample_mcc" +path = "src/cmd/dbexample_mcc/main.rs" diff --git a/herodb/aiprompts/rhaiwrapping.md b/herodb/aiprompts/rhaiwrapping.md index e855499..09a12d2 100644 --- a/herodb/aiprompts/rhaiwrapping.md +++ b/herodb/aiprompts/rhaiwrapping.md @@ -1,14 +1,994 @@ -in @src/zaz/rhai -make wrappers for the src/zaz/models and see how to use them from src/zaz/cmd/examples.rs +# Best Practices for Wrapping Rust Functions with Rhai -how to do this you can find in rhaibook/rust +This document provides comprehensive guidance on how to effectively wrap Rust functions with different standard arguments, pass structs, and handle various return types including errors when using the Rhai scripting language. -the wrappers need to be simple and return the result and if an error return a proper error into the rhai environment so the rhai script using the wrappers will get appropriate error +## Table of Contents -all wrapped functions need to be registered into the rhai engine +1. [Introduction](#introduction) +2. [Basic Function Registration](#basic-function-registration) +3. [Working with Different Argument Types](#working-with-different-argument-types) +4. [Passing and Working with Structs](#passing-and-working-with-structs) +5. [Error Handling](#error-handling) +6. [Returning Different Types](#returning-different-types) +7. [Native Function Handling](#native-function-handling) +8. [Advanced Patterns](#advanced-patterns) +9. [Complete Examples](#complete-examples) -keep all code as small as possible +## Introduction -the creation of the db should not be done in the rhai script, -this shouldd be done before we call the rhai script -a db is context outside of the script execution \ No newline at end of file +Rhai is an embedded scripting language for Rust that allows you to expose Rust functions to scripts and vice versa. This document focuses on the best practices for wrapping Rust functions so they can be called from Rhai scripts, with special attention to handling different argument types, structs, and error conditions. + +## Basic Function Registration + +### Simple Function Registration + +The most basic way to register a Rust function with Rhai is using the `register_fn` method: + +```rust +fn add(x: i64, y: i64) -> i64 { + x + y +} + +fn main() -> Result<(), Box> { + let mut engine = Engine::new(); + + // Register the function with Rhai + engine.register_fn("add", add); + + // Now the function can be called from Rhai scripts + let result = engine.eval::("add(40, 2)")?; + + println!("Result: {}", result); // prints 42 + + Ok(()) +} +``` + +### Function Naming Conventions + +When registering functions, follow these naming conventions: + +1. Use snake_case for function names to maintain consistency with Rhai's style +2. Choose descriptive names that clearly indicate the function's purpose +3. For functions that operate on specific types, consider prefixing with the type name (e.g., `string_length`) + +## Working with Different Argument Types + +### Primitive Types + +Rhai supports the following primitive types that can be directly used as function arguments: + +- `i64` (integer) +- `f64` (float) +- `bool` (boolean) +- `String` or `&str` (string) +- `char` (character) +- `()` (unit type) + +Example: + +```rust +fn calculate(num: i64, factor: f64, enabled: bool) -> f64 { + if enabled { + num as f64 * factor + } else { + 0.0 + } +} + +engine.register_fn("calculate", calculate); +``` + +### Arrays and Collections + +For array arguments: + +```rust +fn sum_array(arr: Array) -> i64 { + arr.iter() + .filter_map(|v| v.as_int().ok()) + .sum() +} + +engine.register_fn("sum_array", sum_array); +``` + +### Optional Arguments and Function Overloading + +Rhai supports function overloading, which allows you to register multiple functions with the same name but different parameter types or counts: + +```rust +fn greet(name: &str) -> String { + format!("Hello, {}!", name) +} + +fn greet_with_title(title: &str, name: &str) -> String { + format!("Hello, {} {}!", title, name) +} + +engine.register_fn("greet", greet); +engine.register_fn("greet", greet_with_title); + +// In Rhai: +// greet("World") -> "Hello, World!" +// greet("Mr.", "Smith") -> "Hello, Mr. Smith!" +``` + +## Passing and Working with Structs + +### Registering Custom Types + +To use Rust structs in Rhai, you need to register them: + +#### Method 1: Using the CustomType Trait (Recommended) + +```rust +#[derive(Debug, Clone, CustomType)] +#[rhai_type(extra = Self::build_extra)] +struct TestStruct { + x: i64, +} + +impl TestStruct { + pub fn new() -> Self { + Self { x: 1 } + } + + pub fn update(&mut self) { + self.x += 1000; + } + + pub fn calculate(&mut self, data: i64) -> i64 { + self.x * data + } + + fn build_extra(builder: &mut TypeBuilder) { + builder + .with_name("TestStruct") + .with_fn("new_ts", Self::new) + .with_fn("update", Self::update) + .with_fn("calc", Self::calculate); + } +} + +// In your main function: +let mut engine = Engine::new(); +engine.build_type::(); +``` + +#### Method 2: Manual Registration + +```rust +#[derive(Debug, Clone)] +struct TestStruct { + x: i64, +} + +impl TestStruct { + pub fn new() -> Self { + Self { x: 1 } + } + + pub fn update(&mut self) { + self.x += 1000; + } +} + +let mut engine = Engine::new(); + +engine + .register_type_with_name::("TestStruct") + .register_fn("new_ts", TestStruct::new) + .register_fn("update", TestStruct::update); +``` + +### Accessing Struct Fields + +By default, Rhai can access public fields of registered structs: + +```rust +// In Rhai script: +let x = new_ts(); +x.x = 42; // Direct field access +``` + +### Passing Structs as Arguments + +When passing structs as arguments to functions, ensure they implement the `Clone` trait: + +```rust +fn process_struct(test: TestStruct) -> i64 { + test.x * 2 +} + +engine.register_fn("process_struct", process_struct); +``` + +### Returning Structs from Functions + +You can return custom structs from functions: + +```rust +fn create_struct(value: i64) -> TestStruct { + TestStruct { x: value } +} + +engine.register_fn("create_struct", create_struct); +``` + +## Error Handling + +Error handling is a critical aspect of integrating Rust functions with Rhai. Proper error handling ensures that script execution fails gracefully with meaningful error messages. + +### Basic Error Handling + +The most basic way to handle errors is to return a `Result` type: + +```rust +fn divide(a: i64, b: i64) -> Result> { + if b == 0 { + // Return an error if division by zero + Err("Division by zero".into()) + } else { + Ok(a / b) + } +} + +engine.register_fn("divide", divide); +``` + +### EvalAltResult Types + +Rhai provides several error types through the `EvalAltResult` enum: + +```rust +use rhai::EvalAltResult; +use rhai::Position; + +fn my_function() -> Result> { + // Different error types + + // Runtime error - general purpose error + return Err(Box::new(EvalAltResult::ErrorRuntime( + "Something went wrong".into(), + Position::NONE + ))); + + // Type error - when a type mismatch occurs + return Err(Box::new(EvalAltResult::ErrorMismatchOutputType( + "expected i64, got string".into(), + Position::NONE, + "i64".into() + ))); + + // Function not found error + return Err(Box::new(EvalAltResult::ErrorFunctionNotFound( + "function_name".into(), + Position::NONE + ))); +} +``` + +### Custom Error Types + +For more structured error handling, you can create custom error types: + +```rust +use thiserror::Error; +use rhai::{EvalAltResult, Position}; + +#[derive(Error, Debug)] +enum MyError { + #[error("Invalid input: {0}")] + InvalidInput(String), + + #[error("Calculation error: {0}")] + CalculationError(String), + + #[error("Database error: {0}")] + DatabaseError(String), +} + +// Convert your custom error to EvalAltResult +fn process_data(input: i64) -> Result> { + // Your logic here that might return a custom error + let result = validate_input(input) + .map_err(|e| Box::new(EvalAltResult::ErrorRuntime( + format!("Validation failed: {}", e), + Position::NONE + )))?; + + let processed = calculate(result) + .map_err(|e| Box::new(EvalAltResult::ErrorRuntime( + format!("Calculation failed: {}", e), + Position::NONE + )))?; + + if processed < 0 { + return Err(Box::new(EvalAltResult::ErrorRuntime( + "Negative result not allowed".into(), + Position::NONE + ))); + } + + Ok(processed) +} + +// Helper functions that return our custom error type +fn validate_input(input: i64) -> Result { + if input <= 0 { + return Err(MyError::InvalidInput("Input must be positive".into())); + } + Ok(input) +} + +fn calculate(value: i64) -> Result { + if value > 1000 { + return Err(MyError::CalculationError("Value too large".into())); + } + Ok(value * 2) +} +``` + +### Error Propagation + +When calling Rhai functions from Rust, errors are propagated through the `?` operator: + +```rust +let result = engine.eval::("divide(10, 0)")?; // This will propagate the error +``` + +### Error Context and Position Information + +For better debugging, include position information in your errors: + +```rust +fn parse_config(config: &str) -> Result> { + // Get the call position from the context + let pos = Position::NONE; // In a real function, you'd get this from NativeCallContext + + match serde_json::from_str::(config) { + Ok(json) => { + // Convert JSON to Rhai Map + let mut map = Map::new(); + // ... conversion logic ... + Ok(map) + }, + Err(e) => { + Err(Box::new(EvalAltResult::ErrorRuntime( + format!("Failed to parse config: {}", e), + pos + ))) + } + } +} +``` + +### Best Practices for Error Handling + +1. **Be Specific**: Provide clear, specific error messages that help script writers understand what went wrong +2. **Include Context**: When possible, include relevant context in error messages (e.g., variable values, expected types) +3. **Consistent Error Types**: Use consistent error types for similar issues +4. **Validate Early**: Validate inputs at the beginning of functions to fail fast +5. **Document Error Conditions**: Document possible error conditions for functions exposed to Rhai + + +## Returning Different Types + +Properly handling return types is crucial for creating a seamless integration between Rust and Rhai. This section covers various approaches to returning different types of data from Rust functions to Rhai scripts. + +### Simple Return Types + +For simple return types, specify the type when registering the function: + +```rust +fn get_number() -> i64 { 42 } +fn get_string() -> String { "hello".to_string() } +fn get_boolean() -> bool { true } +fn get_float() -> f64 { 3.14159 } +fn get_char() -> char { 'A' } +fn get_unit() -> () { () } + +engine.register_fn("get_number", get_number); +engine.register_fn("get_string", get_string); +engine.register_fn("get_boolean", get_boolean); +engine.register_fn("get_float", get_float); +engine.register_fn("get_char", get_char); +engine.register_fn("get_unit", get_unit); +``` + +### Dynamic Return Types + +WE SHOULD TRY NOT TO DO THIS + +For functions that may return different types based on conditions, use the `Dynamic` type: + +```rust +fn get_value(which: i64) -> Dynamic { + match which { + 0 => Dynamic::from(42), + 1 => Dynamic::from("hello"), + 2 => Dynamic::from(true), + 3 => Dynamic::from(3.14159), + 4 => { + let mut array = Array::new(); + array.push(Dynamic::from(1)); + array.push(Dynamic::from(2)); + Dynamic::from_array(array) + }, + 5 => { + let mut map = Map::new(); + map.insert("key".into(), "value".into()); + Dynamic::from_map(map) + }, + _ => Dynamic::UNIT, + } +} + +engine.register_fn("get_value", get_value); +``` + +### Returning Collections + +Rhai supports various collection types: + +```rust +// Returning an array +fn get_array() -> Array { + let mut array = Array::new(); + array.push(Dynamic::from(1)); + array.push(Dynamic::from("hello")); + array.push(Dynamic::from(true)); + array +} + +// Returning a map +fn get_map() -> Map { + let mut map = Map::new(); + map.insert("number".into(), 42.into()); + map.insert("string".into(), "hello".into()); + map.insert("boolean".into(), true.into()); + map +} + +// Returning a typed Vec (will be converted to Rhai Array) +fn get_numbers() -> Vec { + vec![1, 2, 3, 4, 5] +} + +// Returning a HashMap (will be converted to Rhai Map) +fn get_config() -> HashMap { + let mut map = HashMap::new(); + map.insert("host".to_string(), "localhost".to_string()); + map.insert("port".to_string(), "8080".to_string()); + map +} + +engine.register_fn("get_array", get_array); +engine.register_fn("get_map", get_map); +engine.register_fn("get_numbers", get_numbers); +engine.register_fn("get_config", get_config); +``` + +### Returning Custom Structs + +For returning custom structs, ensure they implement the `Clone` trait: + +```rust +#[derive(Debug, Clone)] +struct TestStruct { + x: i64, + name: String, + active: bool, +} + +fn create_struct(value: i64, name: &str, active: bool) -> TestStruct { + TestStruct { + x: value, + name: name.to_string(), + active + } +} + +fn get_struct_array() -> Vec { + vec![ + TestStruct { x: 1, name: "one".to_string(), active: true }, + TestStruct { x: 2, name: "two".to_string(), active: false }, + ] +} + +engine.register_type_with_name::("TestStruct") + .register_fn("create_struct", create_struct) + .register_fn("get_struct_array", get_struct_array); +``` + +### Returning Results and Options + +For functions that might fail or return optional values: + +```rust +// Returning a Result +fn divide(a: i64, b: i64) -> Result> { + if b == 0 { + Err("Division by zero".into()) + } else { + Ok(a / b) + } +} + +// Returning an Option (converted to Dynamic) +fn find_item(id: i64) -> Dynamic { + let item = lookup_item(id); + + match item { + Some(value) => value.into(), + None => Dynamic::UNIT, // Rhai has no null, so use () for None + } +} + +// Helper function returning Option +fn lookup_item(id: i64) -> Option { + match id { + 1 => Some(TestStruct { x: 1, name: "one".to_string(), active: true }), + 2 => Some(TestStruct { x: 2, name: "two".to_string(), active: false }), + _ => None, + } +} + +engine.register_fn("divide", divide); +engine.register_fn("find_item", find_item); +``` + +### Serialization and Deserialization + +When working with JSON or other serialized formats: + +```rust +use serde_json::{Value as JsonValue, json}; + +// Return JSON data as a Rhai Map +fn get_json_data() -> Result> { + // Simulate fetching JSON data + let json_data = json!({ + "name": "John Doe", + "age": 30, + "address": { + "street": "123 Main St", + "city": "Anytown" + }, + "phones": ["+1-555-1234", "+1-555-5678"] + }); + + // Convert JSON to Rhai Map + json_to_rhai_value(json_data) + .and_then(|v| v.try_cast::().map_err(|_| "Expected a map".into())) +} + +// Helper function to convert JSON Value to Rhai Dynamic +fn json_to_rhai_value(json: JsonValue) -> Result> { + match json { + JsonValue::Null => Ok(Dynamic::UNIT), + JsonValue::Bool(b) => Ok(b.into()), + JsonValue::Number(n) => { + if n.is_i64() { + Ok(n.as_i64().unwrap().into()) + } else { + Ok(n.as_f64().unwrap().into()) + } + }, + JsonValue::String(s) => Ok(s.into()), + JsonValue::Array(arr) => { + let mut rhai_array = Array::new(); + for item in arr { + rhai_array.push(json_to_rhai_value(item)?); + } + Ok(Dynamic::from_array(rhai_array)) + }, + JsonValue::Object(obj) => { + let mut rhai_map = Map::new(); + for (k, v) in obj { + rhai_map.insert(k.into(), json_to_rhai_value(v)?); + } + Ok(Dynamic::from_map(rhai_map)) + } + } +} + +engine.register_fn("get_json_data", get_json_data); +``` + +### Working with Dynamic Type System + +Understanding how to work with Rhai's Dynamic type system is essential: + +```rust +// Function that examines a Dynamic value and returns information about it +fn inspect_value(value: Dynamic) -> Map { + let mut info = Map::new(); + + // Store the type name + info.insert("type".into(), value.type_name().into()); + + // Store specific type information + if value.is_int() { + info.insert("category".into(), "number".into()); + info.insert("value".into(), value.clone()); + } else if value.is_float() { + info.insert("category".into(), "number".into()); + info.insert("value".into(), value.clone()); + } else if value.is_string() { + info.insert("category".into(), "string".into()); + info.insert("length".into(), value.clone_cast::().len().into()); + info.insert("value".into(), value.clone()); + } else if value.is_array() { + info.insert("category".into(), "array".into()); + info.insert("length".into(), value.clone_cast::().len().into()); + } else if value.is_map() { + info.insert("category".into(), "map".into()); + info.insert("keys".into(), value.clone_cast::().keys().len().into()); + } else if value.is_bool() { + info.insert("category".into(), "boolean".into()); + info.insert("value".into(), value.clone()); + } else { + info.insert("category".into(), "other".into()); + } + + info +} + +engine.register_fn("inspect", inspect_value); +``` + +## Native Function Handling + +When working with native Rust functions in Rhai, there are several important considerations for handling different argument types, especially when dealing with complex data structures and error cases. + +### Native Function Signature + +Native Rust functions registered with Rhai can have one of two signatures: + +1. **Standard Function Signature**: Functions with typed parameters + ```rust + fn my_function(param1: Type1, param2: Type2, ...) -> ReturnType { ... } + ``` + +2. **Dynamic Function Signature**: Functions that handle raw Dynamic values + ```rust + fn my_dynamic_function(context: NativeCallContext, args: &mut [&mut Dynamic]) -> Result> { ... } + ``` + +### Working with Raw Dynamic Arguments + +The dynamic function signature gives you more control but requires manual type checking and conversion: + +```rust +fn process_dynamic_args(context: NativeCallContext, args: &mut [&mut Dynamic]) -> Result> { + // Check number of arguments + if args.len() != 2 { + return Err("Expected exactly 2 arguments".into()); + } + + // Extract and convert the first argument to an integer + let arg1 = args[0].as_int().map_err(|_| "First argument must be an integer".into())?; + + // Extract and convert the second argument to a string + let arg2 = args[1].as_str().map_err(|_| "Second argument must be a string".into())?; + + // Process the arguments + let result = format!("{}: {}", arg2, arg1); + + // Return the result as a Dynamic value + Ok(result.into()) +} + +// Register the function +engine.register_fn("process", process_dynamic_args); +``` + +### Handling Complex Struct Arguments + +When working with complex struct arguments, you have several options: + +#### Option 1: Use typed parameters (recommended for simple cases) + +```rust +#[derive(Clone)] +struct ComplexData { + id: i64, + values: Vec, +} + +fn process_complex(data: &mut ComplexData, factor: f64) -> f64 { + let sum: f64 = data.values.iter().sum(); + data.values.push(sum * factor); + sum * factor +} + +engine.register_fn("process_complex", process_complex); +``` + +#### Option 2: Use Dynamic parameters for more flexibility + +```rust +fn process_complex_dynamic(context: NativeCallContext, args: &mut [&mut Dynamic]) -> Result> { + // Check arguments + if args.len() != 2 { + return Err("Expected exactly 2 arguments".into()); + } + + // Get mutable reference to the complex data + let data = args[0].write_lock::() + .ok_or_else(|| "First argument must be ComplexData".into())?; + + // Get the factor + let factor = args[1].as_float().map_err(|_| "Second argument must be a number".into())?; + + // Process the data + let sum: f64 = data.values.iter().sum(); + data.values.push(sum * factor); + + Ok((sum * factor).into()) +} +``` + +### Handling Variable Arguments + +For functions that accept a variable number of arguments: + +```rust +fn sum_all(context: NativeCallContext, args: &mut [&mut Dynamic]) -> Result> { + let mut total: i64 = 0; + + for arg in args.iter() { + total += arg.as_int().map_err(|_| "All arguments must be integers".into())?; + } + + Ok(total.into()) +} + +engine.register_fn("sum_all", sum_all); + +// In Rhai: +// sum_all(1, 2, 3, 4, 5) -> 15 +// sum_all(10, 20) -> 30 +``` + +### Handling Optional Arguments + +For functions with optional arguments, use function overloading: + +```rust +fn create_person(name: &str) -> Person { + Person { name: name.to_string(), age: 30 } // Default age +} + +fn create_person_with_age(name: &str, age: i64) -> Person { + Person { name: name.to_string(), age } +} + +engine.register_fn("create_person", create_person); +engine.register_fn("create_person", create_person_with_age); + +// In Rhai: +// create_person("John") -> Person with name "John" and age 30 +// create_person("John", 25) -> Person with name "John" and age 25 +``` + +### Handling Default Arguments + +Rhai doesn't directly support default arguments, but you can simulate them: + +```rust +fn configure(options: &mut Map) -> Result<(), Box> { + // Check if certain options exist, if not, set defaults + if !options.contains_key("timeout") { + options.insert("timeout".into(), 30_i64.into()); + } + + if !options.contains_key("retry") { + options.insert("retry".into(), true.into()); + } + + Ok(()) +} + +engine.register_fn("configure", configure); + +// In Rhai: +// let options = #{}; +// configure(options); +// print(options.timeout); // Prints 30 +``` + +### Handling Mutable and Immutable References + +Rhai supports both mutable and immutable references: + +```rust +// Function taking an immutable reference +fn get_name(person: &Person) -> String { + person.name.clone() +} + +// Function taking a mutable reference +fn increment_age(person: &mut Person) { + person.age += 1; +} + +engine.register_fn("get_name", get_name); +engine.register_fn("increment_age", increment_age); +``` + +### Converting Between Rust and Rhai Types + +When you need to convert between Rust and Rhai types: + +```rust +// Convert a Rust HashMap to a Rhai Map +fn create_config() -> Map { + let mut rust_map = HashMap::new(); + rust_map.insert("server".to_string(), "localhost".to_string()); + rust_map.insert("port".to_string(), "8080".to_string()); + + // Convert to Rhai Map + let mut rhai_map = Map::new(); + for (k, v) in rust_map { + rhai_map.insert(k.into(), v.into()); + } + + rhai_map +} + +// Convert a Rhai Array to a Rust Vec +fn process_array(arr: Array) -> Result> { + // Convert to Rust Vec + let rust_vec: Result, _> = arr.iter() + .map(|v| v.as_int().map_err(|_| "Array must contain only integers".into())) + .collect(); + + let numbers = rust_vec?; + Ok(numbers.iter().sum()) +} +``` + +## Complete Examples + +### Example 1: Basic Function Registration and Struct Handling + +```rust +use rhai::{Engine, EvalAltResult, RegisterFn}; + +#[derive(Debug, Clone)] +struct Person { + name: String, + age: i64, +} + +impl Person { + fn new(name: &str, age: i64) -> Self { + Self { + name: name.to_string(), + age, + } + } + + fn greet(&self) -> String { + format!("Hello, my name is {} and I am {} years old.", self.name, self.age) + } + + fn have_birthday(&mut self) { + self.age += 1; + } +} + +fn is_adult(person: &Person) -> bool { + person.age >= 18 +} + +fn main() -> Result<(), Box> { + let mut engine = Engine::new(); + + // Register the Person type + engine + .register_type_with_name::("Person") + .register_fn("new_person", Person::new) + .register_fn("greet", Person::greet) + .register_fn("have_birthday", Person::have_birthday) + .register_fn("is_adult", is_adult); + + // Run a script that uses the Person type + let result = engine.eval::(r#" + let p = new_person("John", 17); + let greeting = p.greet(); + + if !is_adult(p) { + p.have_birthday(); + } + + greeting + " Now I am " + p.age.to_string() + " years old." + "#)?; + + println!("{}", result); + + Ok(()) +} +``` + +### Example 2: Error Handling and Complex Return Types + +```rust +use rhai::{Engine, EvalAltResult, Map, Dynamic}; +use std::collections::HashMap; + +#[derive(Debug, Clone)] +struct Product { + id: i64, + name: String, + price: f64, +} + +fn get_product(id: i64) -> Result> { + match id { + 1 => Ok(Product { id: 1, name: "Laptop".to_string(), price: 999.99 }), + 2 => Ok(Product { id: 2, name: "Phone".to_string(), price: 499.99 }), + _ => Err("Product not found".into()) + } +} + +fn calculate_total(products: Array) -> Result> { + let mut total = 0.0; + + for product_dynamic in products.iter() { + let product = product_dynamic.clone().try_cast::() + .map_err(|_| "Invalid product in array".into())?; + + total += product.price; + } + + Ok(total) +} + +fn get_product_map() -> Map { + let mut map = Map::new(); + + map.insert("laptop".into(), + Dynamic::from(Product { id: 1, name: "Laptop".to_string(), price: 999.99 })); + map.insert("phone".into(), + Dynamic::from(Product { id: 2, name: "Phone".to_string(), price: 499.99 })); + + map +} + +fn main() -> Result<(), Box> { + let mut engine = Engine::new(); + + engine + .register_type_with_name::("Product") + .register_fn("get_product", get_product) + .register_fn("calculate_total", calculate_total) + .register_fn("get_product_map", get_product_map); + + let result = engine.eval::(r#" + let products = []; + + // Try to get products + try { + products.push(get_product(1)); + products.push(get_product(2)); + products.push(get_product(3)); // This will throw an error + } catch(err) { + print(`Error: ${err}`); + } + + // Get products from map + let product_map = get_product_map(); + products.push(product_map.laptop); + + calculate_total(products) + "#)?; + + println!("Total: ${:.2}", result); + + Ok(()) +} +``` diff --git a/herodb/aiprompts/rhaiwrapping_advanced.md b/herodb/aiprompts/rhaiwrapping_advanced.md new file mode 100644 index 0000000..666710a --- /dev/null +++ b/herodb/aiprompts/rhaiwrapping_advanced.md @@ -0,0 +1,134 @@ + +### Error Handling in Dynamic Functions + +When working with the dynamic function signature, error handling is slightly different: + +```rust +fn dynamic_function(ctx: NativeCallContext, args: &mut [&mut Dynamic]) -> Result> { + // Get the position information from the context + let pos = ctx.position(); + + // Validate arguments + if args.len() < 2 { + return Err(Box::new(EvalAltResult::ErrorRuntime( + format!("Expected at least 2 arguments, got {}", args.len()), + pos + ))); + } + + // Try to convert arguments with proper error handling + let arg1 = match args[0].as_int() { + Ok(val) => val, + Err(_) => return Err(Box::new(EvalAltResult::ErrorMismatchOutputType( + "Expected first argument to be an integer".into(), + pos, + "i64".into() + ))) + }; + + // Process with error handling + if arg1 <= 0 { + return Err(Box::new(EvalAltResult::ErrorRuntime( + "First argument must be positive".into(), + pos + ))); + } + + // Return success + Ok(Dynamic::from(arg1 * 2)) +} +``` + + +## Advanced Patterns + +### Working with Function Pointers + +You can create function pointers that bind to Rust functions: + +```rust +fn my_awesome_fn(ctx: NativeCallContext, args: &mut[&mut Dynamic]) -> Result> { + // Check number of arguments + if args.len() != 2 { + return Err("one argument is required, plus the object".into()); + } + + // Get call arguments + let x = args[1].try_cast::().map_err(|_| "argument must be an integer".into())?; + + // Get mutable reference to the object map, which is passed as the first argument + let map = &mut *args[0].as_map_mut().map_err(|_| "object must be a map".into())?; + + // Do something awesome here ... + let result = x * 2; + + Ok(result.into()) +} + +// Register a function to create a pre-defined object +engine.register_fn("create_awesome_object", || { + // Use an object map as base + let mut map = Map::new(); + + // Create a function pointer that binds to 'my_awesome_fn' + let fp = FnPtr::from_fn("awesome", my_awesome_fn)?; + // ^ name of method + // ^ native function + + // Store the function pointer in the object map + map.insert("awesome".into(), fp.into()); + + Ok(Dynamic::from_map(map)) +}); +``` + +### Creating Rust Closures from Rhai Functions + +You can encapsulate a Rhai script as a Rust closure: + +```rust +use rhai::{Engine, Func}; + +let engine = Engine::new(); + +let script = "fn calc(x, y) { x + y.len < 42 }"; + +// Create a Rust closure from a Rhai function +let func = Func::<(i64, &str), bool>::create_from_script( + engine, // the 'Engine' is consumed into the closure + script, // the script + "calc" // the entry-point function name +)?; + +// Call the closure +let result = func(123, "hello")?; + +// Pass it as a callback to another function +schedule_callback(func); +``` + +### Calling Rhai Functions from Rust + +You can call Rhai functions from Rust: + +```rust +// Compile the script to AST +let ast = engine.compile(script)?; + +// Create a custom 'Scope' +let mut scope = Scope::new(); + +// Add variables to the scope +scope.push("my_var", 42_i64); +scope.push("my_string", "hello, world!"); +scope.push_constant("MY_CONST", true); + +// Call a function defined in the script +let result = engine.call_fn::(&mut scope, &ast, "hello", ("abc", 123_i64))?; + +// For a function with one parameter, use a tuple with a trailing comma +let result = engine.call_fn::(&mut scope, &ast, "hello", (123_i64,))?; + +// For a function with no parameters +let result = engine.call_fn::(&mut scope, &ast, "hello", ())?; +``` diff --git a/herodb/aiprompts/rhaiwrapping_best_practices.md b/herodb/aiprompts/rhaiwrapping_best_practices.md new file mode 100644 index 0000000..17d0a92 --- /dev/null +++ b/herodb/aiprompts/rhaiwrapping_best_practices.md @@ -0,0 +1,187 @@ +## Best Practices and Optimization + +When wrapping Rust functions for use with Rhai, following these best practices will help you create efficient, maintainable, and robust code. + +### Performance Considerations + +1. **Minimize Cloning**: Rhai often requires cloning data, but you can minimize this overhead: + ```rust + // Prefer immutable references when possible + fn process_data(data: &MyStruct) -> i64 { + // Work with data without cloning + data.value * 2 + } + + // Use mutable references for in-place modifications + fn update_data(data: &mut MyStruct) { + data.value += 1; + } + ``` + +2. **Avoid Excessive Type Conversions**: Converting between Rhai's Dynamic type and Rust types has overhead: + ```rust + // Inefficient - multiple conversions + fn process_inefficient(ctx: NativeCallContext, args: &mut [&mut Dynamic]) -> Result> { + let value = args[0].as_int()?; + let result = value * 2; + Ok(Dynamic::from(result)) + } + + // More efficient - use typed parameters when possible + fn process_efficient(value: i64) -> i64 { + value * 2 + } + ``` + +3. **Batch Operations**: For operations on collections, batch processing is more efficient: + ```rust + // Process an entire array at once rather than element by element + fn sum_array(arr: Array) -> Result> { + arr.iter() + .map(|v| v.as_int()) + .collect::, _>>() + .map(|nums| nums.iter().sum()) + .map_err(|_| "Array must contain only integers".into()) + } + ``` + +4. **Compile Scripts Once**: Reuse compiled ASTs for scripts that are executed multiple times: + ```rust + // Compile once + let ast = engine.compile(script)?; + + // Execute multiple times with different parameters + for i in 0..10 { + let result = engine.eval_ast::(&ast)?; + println!("Result {}: {}", i, result); + } + ``` + +### Thread Safety + +1. **Use Sync Mode When Needed**: If you need thread safety, use the `sync` feature: + ```rust + // In Cargo.toml + // rhai = { version = "1.x", features = ["sync"] } + + // This creates a thread-safe engine + let engine = Engine::new(); + + // Now you can safely share the engine between threads + std::thread::spawn(move || { + let result = engine.eval::("40 + 2")?; + println!("Result: {}", result); + }); + ``` + +2. **Clone the Engine for Multiple Threads**: When not using `sync`, clone the engine for each thread: + ```rust + let engine = Engine::new(); + + let handles: Vec<_> = (0..5).map(|i| { + let engine_clone = engine.clone(); + std::thread::spawn(move || { + let result = engine_clone.eval::(&format!("{} + 2", i * 10))?; + println!("Thread {}: {}", i, result); + }) + }).collect(); + + for handle in handles { + handle.join().unwrap(); + } + ``` + +### Memory Management + +1. **Control Scope Size**: Be mindful of the size of your scopes: + ```rust + // Create a new scope for each operation to avoid memory buildup + for item in items { + let mut scope = Scope::new(); + scope.push("item", item); + engine.eval_with_scope::<()>(&mut scope, "process(item)")?; + } + ``` + +2. **Limit Script Complexity**: Use engine options to limit script complexity: + ```rust + let mut engine = Engine::new(); + + // Set limits to prevent scripts from consuming too many resources + engine.set_max_expr_depths(64, 64) // Max expression/statement depth + .set_max_function_expr_depth(64) // Max function depth + .set_max_array_size(10000) // Max array size + .set_max_map_size(10000) // Max map size + .set_max_string_size(10000) // Max string size + .set_max_call_levels(64); // Max call stack depth + ``` + +3. **Use Shared Values Carefully**: Shared values (via closures) have reference-counting overhead: + ```rust + // Avoid unnecessary capturing in closures when possible + engine.register_fn("process", |x: i64| x * 2); + + // Instead of capturing large data structures + let large_data = vec![1, 2, 3, /* ... thousands of items ... */]; + engine.register_fn("process_data", move |idx: i64| { + if idx >= 0 && (idx as usize) < large_data.len() { + large_data[idx as usize] + } else { + 0 + } + }); + + // Consider registering a lookup function instead + let large_data = std::sync::Arc::new(vec![1, 2, 3, /* ... thousands of items ... */]); + let data_ref = large_data.clone(); + engine.register_fn("lookup", move |idx: i64| { + if idx >= 0 && (idx as usize) < data_ref.len() { + data_ref[idx as usize] + } else { + 0 + } + }); + ``` + +### API Design + +1. **Consistent Naming**: Use consistent naming conventions: + ```rust + // Good: Consistent naming pattern + engine.register_fn("create_user", create_user) + .register_fn("update_user", update_user) + .register_fn("delete_user", delete_user); + + // Bad: Inconsistent naming + engine.register_fn("create_user", create_user) + .register_fn("user_update", update_user) + .register_fn("remove", delete_user); + ``` + +2. **Logical Function Grouping**: Group related functions together: + ```rust + // Register all string-related functions together + engine.register_fn("str_length", |s: &str| s.len() as i64) + .register_fn("str_uppercase", |s: &str| s.to_uppercase()) + .register_fn("str_lowercase", |s: &str| s.to_lowercase()); + + // Register all math-related functions together + engine.register_fn("math_sin", |x: f64| x.sin()) + .register_fn("math_cos", |x: f64| x.cos()) + .register_fn("math_tan", |x: f64| x.tan()); + ``` + +3. **Comprehensive Documentation**: Document your API thoroughly: + ```rust + // Add documentation for script writers + let mut engine = Engine::new(); + + #[cfg(feature = "metadata")] + { + // Add function documentation + engine.register_fn("calculate_tax", calculate_tax) + .register_fn_metadata("calculate_tax", |metadata| { + metadata.set_doc_comment("Calculates tax based on income and rate.\n\nParameters:\n- income: Annual income\n- rate: Tax rate (0.0-1.0)\n\nReturns: Calculated tax amount"); + }); + } + ``` diff --git a/herodb/src/cmd/dbexample_governance/Cargo.lock b/herodb/src/cmd/dbexample_governance/Cargo.lock new file mode 100644 index 0000000..cddb29b --- /dev/null +++ b/herodb/src/cmd/dbexample_governance/Cargo.lock @@ -0,0 +1,2161 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "addr2line" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "aes-gcm" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + +[[package]] +name = "ahash" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +dependencies = [ + "cfg-if", + "const-random", + "getrandom 0.2.15", + "once_cell", + "version_check", + "zerocopy 0.7.35", +] + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +dependencies = [ + "alloc-no-stdlib", +] + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "async-trait" +version = "0.1.88" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "autocfg" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + +[[package]] +name = "backtrace" +version = "0.3.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets", +] + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "brotli" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d640d25bc63c50fb1f0b545ffd80207d2e10a4c965530809b40ba3386825c391" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "2.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e2e4afe60d7dd600fdd3de8d0f08c2b7ec039712e3b6137ff98b7004e82de4f" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "bumpalo" +version = "3.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" + +[[package]] +name = "cc" +version = "1.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fcb57c740ae1daf453ae85f16e37396f672b039e00d9d866e07ddb24e328e3a" +dependencies = [ + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a7964611d71df112cb1730f2ee67324fcf4d0fc6606acbbe9bfe06df124637c" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + +[[package]] +name = "const-random" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" +dependencies = [ + "const-random-macro", +] + +[[package]] +name = "const-random-macro" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" +dependencies = [ + "getrandom 0.2.15", + "once_cell", + "tiny-keccak", +] + +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + +[[package]] +name = "cookie" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7efb37c3e1ccb1ff97164ad95ac1606e8ccd35b3fa0a7d99a304c7f4a428cc24" +dependencies = [ + "aes-gcm", + "base64", + "hkdf", + "hmac", + "percent-encoding", + "rand", + "sha2", + "subtle", + "time", + "version_check", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crunchy" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929" + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "rand_core", + "typenum", +] + +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + +[[package]] +name = "darling" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b750cb3417fd1b327431a470f388520309479ab0bf5e323505daf0290cd3850" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "109c1ca6e6b7f82cc233a97004ea8ed7ca123a9af07a8230878fcfda9b158bf0" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 1.0.109", +] + +[[package]] +name = "darling_macro" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4aab4dbc9f7611d8b55048a3a16d2d010c2c8334e46304b40ac1cc14bf3b48e" +dependencies = [ + "darling_core", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "dbexample_governance" +version = "0.1.0" +dependencies = [ + "chrono", + "herodb", +] + +[[package]] +name = "deranged" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28cfac68e08048ae1883171632c2aef3ebc555621ae56fbccce1cbf22dd7f058" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "derive_more" +version = "0.99.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3da29a38df43d6f156149c9b43ded5e018ddff2a855cf2cfd62e8cd7d079c69f" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.100", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "976dd42dc7e85965fe702eb8164f21f450704bdde31faefd6471dba214cb594e" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fs2" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-core", + "futures-macro", + "futures-sink", + "futures-task", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasi 0.14.2+wasi-0.2.4", +] + +[[package]] +name = "ghash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +dependencies = [ + "opaque-debug", + "polyval", +] + +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + +[[package]] +name = "h2" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap 2.8.0", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" + +[[package]] +name = "headers" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06683b93020a07e3dbcf5f8c0f6d40080d725bea7936fc01ad345c01b97dc270" +dependencies = [ + "base64", + "bytes", + "headers-core", + "http", + "httpdate", + "mime", + "sha1", +] + +[[package]] +name = "headers-core" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7f66481bfee273957b1f20485a4ff3362987f85b2c236580d81b4eb7a326429" +dependencies = [ + "http", +] + +[[package]] +name = "herodb" +version = "0.1.0" +dependencies = [ + "bincode", + "brotli", + "chrono", + "lazy_static", + "paste", + "poem", + "poem-openapi", + "rhai", + "serde", + "serde_json", + "sled", + "tempfile", + "thiserror", + "tokio", + "uuid", +] + +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "http" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" +dependencies = [ + "bytes", + "http", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "0.14.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", +] + +[[package]] +name = "indexmap" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3954d50fe15b02142bf25d3b8bdadb634ec3948f103d04ffe3031bc8fe9d7058" +dependencies = [ + "equivalent", + "hashbrown 0.15.2", +] + +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + +[[package]] +name = "instant" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "js-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.171" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6" + +[[package]] +name = "linux-raw-sys" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe7db12097d22ec582439daf8618b8fdd1a7bef6270e9af3b1ebcd30893cf413" + +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "miniz_oxide" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff70ce3e48ae43fa075863cef62e8b43b71a4f2382229920e0df362592919430" +dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" +dependencies = [ + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", + "windows-sys 0.52.0", +] + +[[package]] +name = "multer" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01acbdc23469fd8fe07ab135923371d5f5a422fbf9c522158677c8eb15bc51c2" +dependencies = [ + "bytes", + "encoding_rs", + "futures-util", + "http", + "httparse", + "log", + "memchr", + "mime", + "spin", + "tokio", + "version_check", +] + +[[package]] +name = "nix" +version = "0.27.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053" +dependencies = [ + "bitflags 2.9.0", + "cfg-if", + "libc", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "object" +version = "0.36.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + +[[package]] +name = "parking_lot" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" +dependencies = [ + "instant", + "lock_api", + "parking_lot_core 0.8.6", +] + +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core 0.9.10", +] + +[[package]] +name = "parking_lot_core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc" +dependencies = [ + "cfg-if", + "instant", + "libc", + "redox_syscall 0.2.16", + "smallvec", + "winapi", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.5.10", + "smallvec", + "windows-targets", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "poem" +version = "1.3.59" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "504774c97b0744c1ee108a37e5a65a9745a4725c4c06277521dabc28eb53a904" +dependencies = [ + "async-trait", + "bytes", + "chrono", + "cookie", + "futures-util", + "headers", + "http", + "hyper", + "mime", + "multer", + "nix", + "parking_lot 0.12.3", + "percent-encoding", + "pin-project-lite", + "poem-derive", + "quick-xml 0.30.0", + "regex", + "rfc7239", + "serde", + "serde_json", + "serde_urlencoded", + "serde_yaml", + "smallvec", + "tempfile", + "thiserror", + "time", + "tokio", + "tokio-stream", + "tokio-util", + "tracing", + "wildmatch", +] + +[[package]] +name = "poem-derive" +version = "1.3.59" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ddcf4680d8d867e1e375116203846acb088483fa2070244f90589f458bbb31" +dependencies = [ + "proc-macro-crate 2.0.2", + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "poem-openapi" +version = "2.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e26f78b6195ea1b7a16f46bda1961c598e5a66912f2aa1b8b7a2f395aebb9fc" +dependencies = [ + "base64", + "bytes", + "derive_more", + "futures-util", + "mime", + "num-traits", + "poem", + "poem-openapi-derive", + "quick-xml 0.26.0", + "regex", + "serde", + "serde_json", + "serde_urlencoded", + "serde_yaml", + "thiserror", + "tokio", +] + +[[package]] +name = "poem-openapi-derive" +version = "2.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88c3e2975c930dc72c024e75b230c3b6058fb3a746d5739b83aa8f28ab1a42d4" +dependencies = [ + "darling", + "http", + "indexmap 1.9.3", + "mime", + "proc-macro-crate 1.3.1", + "proc-macro2", + "quote", + "regex", + "syn 1.0.109", + "thiserror", +] + +[[package]] +name = "polyval" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if", + "cpufeatures", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "portable-atomic" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e" + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy 0.8.24", +] + +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit 0.19.15", +] + +[[package]] +name = "proc-macro-crate" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b00f26d3400549137f92511a46ac1cd8ce37cb5598a96d382381458b992a5d24" +dependencies = [ + "toml_datetime", + "toml_edit 0.20.2", +] + +[[package]] +name = "proc-macro2" +version = "1.0.94" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quick-xml" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f50b1c63b38611e7d4d7f68b82d3ad0cc71a2ad2e7f61fc10f1328d917c93cd" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "quick-xml" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eff6510e86862b57b210fd8cbe8ed3f0d7d600b9c2863cd4549a2e033c66e956" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.15", +] + +[[package]] +name = "redox_syscall" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "redox_syscall" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b8c0c260b63a8219631167be35e6a988e9554dbd323f8bd08439c8ed1302bd1" +dependencies = [ + "bitflags 2.9.0", +] + +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] +name = "rfc7239" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a82f1d1e38e9a85bb58ffcfadf22ed6f2c94e8cd8581ec2b0f80a2a6858350f" +dependencies = [ + "uncased", +] + +[[package]] +name = "rhai" +version = "1.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce4d759a4729a655ddfdbb3ff6e77fb9eadd902dae12319455557796e435d2a6" +dependencies = [ + "ahash", + "bitflags 2.9.0", + "instant", + "num-traits", + "once_cell", + "rhai_codegen", + "smallvec", + "smartstring", + "thin-vec", +] + +[[package]] +name = "rhai_codegen" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5a11a05ee1ce44058fa3d5961d05194fdbe3ad6b40f904af764d81b86450e6b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d97817398dd4bb2e6da002002db259209759911da105da92bec29ccb12cf58bf" +dependencies = [ + "bitflags 2.9.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustversion" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "semver" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" + +[[package]] +name = "serde" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "serde_json" +version = "1.0.140" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_yaml" +version = "0.9.34+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap 2.8.0", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +dependencies = [ + "libc", +] + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "sled" +version = "0.34.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f96b4737c2ce5987354855aed3797279def4ebf734436c6aa4552cf8e169935" +dependencies = [ + "crc32fast", + "crossbeam-epoch", + "crossbeam-utils", + "fs2", + "fxhash", + "libc", + "log", + "parking_lot 0.11.2", +] + +[[package]] +name = "smallvec" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd" + +[[package]] +name = "smartstring" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fb72c633efbaa2dd666986505016c32c3044395ceaf881518399d2f4127ee29" +dependencies = [ + "autocfg", + "static_assertions", + "version_check", +] + +[[package]] +name = "socket2" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f5fd57c80058a56cf5c777ab8a126398ece8e442983605d280a44ce79d0edef" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tempfile" +version = "3.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7437ac7763b9b123ccf33c338a5cc1bac6f69b45a136c19bdd8a65e3916435bf" +dependencies = [ + "fastrand", + "getrandom 0.3.2", + "once_cell", + "rustix", + "windows-sys 0.59.0", +] + +[[package]] +name = "thin-vec" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "144f754d318415ac792f9d69fc87abbbfc043ce2ef041c60f16ad828f638717d" + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "time" +version = "0.3.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" + +[[package]] +name = "time-macros" +version = "0.2.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + +[[package]] +name = "tokio" +version = "1.44.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f382da615b842244d4b8738c82ed1275e6c5dd90c459a30941cd07080b06c91a" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "parking_lot 0.12.3", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.52.0", +] + +[[package]] +name = "tokio-macros" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "tokio-stream" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b9590b93e6fcc1739458317cccd391ad3955e2bde8913edf6f95f9e65a8f034" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml_datetime" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" + +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap 2.8.0", + "toml_datetime", + "winnow", +] + +[[package]] +name = "toml_edit" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" +dependencies = [ + "indexmap 2.8.0", + "toml_datetime", + "winnow", +] + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "tracing-core" +version = "0.1.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typenum" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" + +[[package]] +name = "uncased" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1b88fcfe09e89d3866a5c11019378088af2d24c3fbd4f0543f96b479ec90697" +dependencies = [ + "version_check", +] + +[[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + +[[package]] +name = "uuid" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9" +dependencies = [ + "getrandom 0.3.2", + "serde", +] + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasi" +version = "0.14.2+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +dependencies = [ + "wit-bindgen-rt", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn 2.0.100", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wildmatch" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ce1ab1f8c62655ebe1350f589c61e505cf94d385bc6a12899442d9081e71fd" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-core" +version = "0.61.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4763c1de310c86d75a878046489e2e5ba02c649d185f21c67d4cf8a56d098980" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "windows-interface" +version = "0.59.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "windows-link" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" + +[[package]] +name = "windows-result" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c64fd11a4fd95df68efcfee5f44a294fe71b8bc6a91993e2791938abcc712252" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ba9642430ee452d5a7aa78d72907ebe8cfda358e8cb7918a2050581322f97" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen-rt" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +dependencies = [ + "bitflags 2.9.0", +] + +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "zerocopy-derive 0.7.35", +] + +[[package]] +name = "zerocopy" +version = "0.8.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2586fea28e186957ef732a5f8b3be2da217d65c5969d4b1e17f973ebbe876879" +dependencies = [ + "zerocopy-derive 0.8.24", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a996a8f63c5c4448cd959ac1bab0aaa3306ccfd060472f85943ee0750f0169be" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] diff --git a/herodb/src/cmd/dbexample_governance/Cargo.toml b/herodb/src/cmd/dbexample_governance/Cargo.toml new file mode 100644 index 0000000..0b589b7 --- /dev/null +++ b/herodb/src/cmd/dbexample_governance/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "dbexample_governance" +version = "0.1.0" +edition = "2021" + +[[bin]] +name = "dbexample_governance" +path = "main.rs" + +[dependencies] +herodb = { path = "../../.." } +chrono = "0.4" \ No newline at end of file diff --git a/herodb/src/cmd/dbexample_governance/main.rs b/herodb/src/cmd/dbexample_governance/main.rs new file mode 100644 index 0000000..0ac3b99 --- /dev/null +++ b/herodb/src/cmd/dbexample_governance/main.rs @@ -0,0 +1,362 @@ +use chrono::{Utc, Duration}; +use herodb::db::DBBuilder; +use herodb::models::governance::{ + Company, CompanyStatus, BusinessType, + Shareholder, ShareholderType, + Meeting, Attendee, MeetingStatus, AttendeeRole, AttendeeStatus, + User, + Vote, VoteOption, Ballot, VoteStatus, + Resolution, ResolutionStatus, Approval +}; +use std::path::PathBuf; +use std::fs; + +fn main() -> Result<(), Box> { + println!("DB Example: Governance Module"); + println!("============================"); + + // Create a temporary directory for the database + let db_path = PathBuf::from("/tmp/dbexample_governance"); + if db_path.exists() { + fs::remove_dir_all(&db_path)?; + } + fs::create_dir_all(&db_path)?; + println!("Database path: {:?}", db_path); + + // Create a database instance with our governance models registered + let db = DBBuilder::new(&db_path) + .register_model::() + .register_model::() + .register_model::() + .register_model::() + .register_model::() + .register_model::() + .build()?; + + println!("\n1. Creating a Company"); + println!("-------------------"); + + // Create a company + let company = Company::new( + 1, + "Acme Corporation".to_string(), + "ACM123456".to_string(), + Utc::now(), + "December 31".to_string(), + "info@acmecorp.com".to_string(), + "+1-555-123-4567".to_string(), + "https://acmecorp.com".to_string(), + "123 Main St, Anytown, USA".to_string(), + BusinessType::Coop, + "Technology".to_string(), + "A leading technology company".to_string(), + CompanyStatus::Active, + ); + + // Insert the company + db.insert(&company)?; + println!("Company created: {} (ID: {})", company.name, company.id); + println!("Status: {:?}, Business Type: {:?}", company.status, company.business_type); + + println!("\n2. Creating Users"); + println!("---------------"); + + // Create users + let user1 = User::new( + 1, + "John Doe".to_string(), + "john.doe@acmecorp.com".to_string(), + "password123".to_string(), // In a real app, this would be hashed + "Acme Corporation".to_string(), + "CEO".to_string(), + ); + + let user2 = User::new( + 2, + "Jane Smith".to_string(), + "jane.smith@acmecorp.com".to_string(), + "password456".to_string(), // In a real app, this would be hashed + "Acme Corporation".to_string(), + "CFO".to_string(), + ); + + let user3 = User::new( + 3, + "Bob Johnson".to_string(), + "bob.johnson@acmecorp.com".to_string(), + "password789".to_string(), // In a real app, this would be hashed + "Acme Corporation".to_string(), + "CTO".to_string(), + ); + + // Insert the users + db.insert(&user1)?; + db.insert(&user2)?; + db.insert(&user3)?; + + println!("User created: {} ({})", user1.name, user1.role); + println!("User created: {} ({})", user2.name, user2.role); + println!("User created: {} ({})", user3.name, user3.role); + + println!("\n3. Creating Shareholders"); + println!("----------------------"); + + // Create shareholders + let mut shareholder1 = Shareholder::new( + 1, + company.id, + user1.id, + user1.name.clone(), + 1000.0, + 40.0, + ShareholderType::Individual, + ); + + let mut shareholder2 = Shareholder::new( + 2, + company.id, + user2.id, + user2.name.clone(), + 750.0, + 30.0, + ShareholderType::Individual, + ); + + let mut shareholder3 = Shareholder::new( + 3, + company.id, + user3.id, + user3.name.clone(), + 750.0, + 30.0, + ShareholderType::Individual, + ); + + // Insert the shareholders + db.insert(&shareholder1)?; + db.insert(&shareholder2)?; + db.insert(&shareholder3)?; + + println!("Shareholder created: {} ({} shares, {}%)", + shareholder1.name, shareholder1.shares, shareholder1.percentage); + println!("Shareholder created: {} ({} shares, {}%)", + shareholder2.name, shareholder2.shares, shareholder2.percentage); + println!("Shareholder created: {} ({} shares, {}%)", + shareholder3.name, shareholder3.shares, shareholder3.percentage); + + // Update shareholder shares + shareholder1.update_shares(1100.0, 44.0); + db.insert(&shareholder1)?; + println!("Updated shareholder: {} ({} shares, {}%)", + shareholder1.name, shareholder1.shares, shareholder1.percentage); + + println!("\n4. Creating a Meeting"); + println!("------------------"); + + // Create a meeting + let mut meeting = Meeting::new( + 1, + company.id, + "Q2 Board Meeting".to_string(), + Utc::now() + Duration::days(7), // Meeting in 7 days + "Conference Room A".to_string(), + "Quarterly board meeting to discuss financial results".to_string(), + ); + + // Create attendees + let attendee1 = Attendee::new( + 1, + meeting.id, + user1.id, + user1.name.clone(), + AttendeeRole::Coordinator, + ); + + let attendee2 = Attendee::new( + 2, + meeting.id, + user2.id, + user2.name.clone(), + AttendeeRole::Member, + ); + + let attendee3 = Attendee::new( + 3, + meeting.id, + user3.id, + user3.name.clone(), + AttendeeRole::Member, + ); + + // Add attendees to the meeting + meeting.add_attendee(attendee1); + meeting.add_attendee(attendee2); + meeting.add_attendee(attendee3); + + // Insert the meeting + db.insert(&meeting)?; + println!("Meeting created: {} ({})", meeting.title, meeting.date.format("%Y-%m-%d %H:%M")); + println!("Status: {:?}, Attendees: {}", meeting.status, meeting.attendees.len()); + + // Update attendee status + if let Some(attendee) = meeting.find_attendee_by_user_id_mut(user2.id) { + attendee.update_status(AttendeeStatus::Confirmed); + } + if let Some(attendee) = meeting.find_attendee_by_user_id_mut(user3.id) { + attendee.update_status(AttendeeStatus::Confirmed); + } + db.insert(&meeting)?; + + // Get confirmed attendees + let confirmed = meeting.confirmed_attendees(); + println!("Confirmed attendees: {}", confirmed.len()); + for attendee in confirmed { + println!(" - {} ({})", attendee.name, match attendee.role { + AttendeeRole::Coordinator => "Coordinator", + AttendeeRole::Member => "Member", + AttendeeRole::Secretary => "Secretary", + AttendeeRole::Participant => "Participant", + AttendeeRole::Advisor => "Advisor", + AttendeeRole::Admin => "Admin", + }); + } + + println!("\n5. Creating a Resolution"); + println!("----------------------"); + + // Create a resolution + let mut resolution = Resolution::new( + 1, + company.id, + "Approval of Q1 Financial Statements".to_string(), + "Resolution to approve the Q1 financial statements".to_string(), + "The Board of Directors hereby approves the financial statements for Q1 2025.".to_string(), + user1.id, // Proposed by the CEO + ); + + // Link the resolution to the meeting + resolution.link_to_meeting(meeting.id); + + // Insert the resolution + db.insert(&resolution)?; + println!("Resolution created: {} (Status: {:?})", resolution.title, resolution.status); + + // Propose the resolution + resolution.propose(); + db.insert(&resolution)?; + println!("Resolution proposed on {}", resolution.proposed_at.format("%Y-%m-%d")); + + // Add approvals + resolution.add_approval(user1.id, user1.name.clone(), true, "Approved as proposed".to_string()); + resolution.add_approval(user2.id, user2.name.clone(), true, "Financials look good".to_string()); + resolution.add_approval(user3.id, user3.name.clone(), true, "No concerns".to_string()); + db.insert(&resolution)?; + + // Check approval status + println!("Approvals: {}, Rejections: {}", + resolution.approval_count(), + resolution.rejection_count()); + + // Approve the resolution + resolution.approve(); + db.insert(&resolution)?; + println!("Resolution approved on {}", + resolution.approved_at.unwrap().format("%Y-%m-%d")); + + println!("\n6. Creating a Vote"); + println!("----------------"); + + // Create a vote + let mut vote = Vote::new( + 1, + company.id, + "Vote on New Product Line".to_string(), + "Vote to approve investment in new product line".to_string(), + Utc::now(), + Utc::now() + Duration::days(3), // Voting period of 3 days + VoteStatus::Open, + ); + + // Add voting options + vote.add_option("Approve".to_string(), 0); + vote.add_option("Reject".to_string(), 0); + vote.add_option("Abstain".to_string(), 0); + + // Insert the vote + db.insert(&vote)?; + println!("Vote created: {} (Status: {:?})", vote.title, vote.status); + println!("Voting period: {} to {}", + vote.start_date.format("%Y-%m-%d"), + vote.end_date.format("%Y-%m-%d")); + + // Cast ballots + vote.add_ballot(user1.id, 1, 1000); // User 1 votes "Approve" with 1000 shares + vote.add_ballot(user2.id, 1, 750); // User 2 votes "Approve" with 750 shares + vote.add_ballot(user3.id, 3, 750); // User 3 votes "Abstain" with 750 shares + db.insert(&vote)?; + + // Check voting results + println!("Voting results:"); + for option in &vote.options { + println!(" - {}: {} votes", option.text, option.count); + } + + // Create a resolution for this vote + let mut vote_resolution = Resolution::new( + 2, + company.id, + "Investment in New Product Line".to_string(), + "Resolution to approve investment in new product line".to_string(), + "The Board of Directors hereby approves an investment of $1,000,000 in the new product line.".to_string(), + user1.id, // Proposed by the CEO + ); + + // Link the resolution to the vote + vote_resolution.link_to_vote(vote.id); + vote_resolution.propose(); + db.insert(&vote_resolution)?; + println!("Created resolution linked to vote: {}", vote_resolution.title); + + println!("\n7. Retrieving Related Objects"); + println!("---------------------------"); + + // Retrieve company and related objects + let retrieved_company = db.get::(&company.id.to_string())?; + println!("Company: {} (ID: {})", retrieved_company.name, retrieved_company.id); + + // Get resolutions for this company + let company_resolutions = retrieved_company.get_resolutions(&db)?; + println!("Company has {} resolutions:", company_resolutions.len()); + for res in company_resolutions { + println!(" - {} (Status: {:?})", res.title, res.status); + } + + // Get meeting and its resolutions + let retrieved_meeting = db.get::(&meeting.id.to_string())?; + println!("Meeting: {} ({})", retrieved_meeting.title, retrieved_meeting.date.format("%Y-%m-%d")); + + let meeting_resolutions = retrieved_meeting.get_resolutions(&db)?; + println!("Meeting has {} resolutions:", meeting_resolutions.len()); + for res in meeting_resolutions { + println!(" - {} (Status: {:?})", res.title, res.status); + } + + // Get vote and its resolution + let retrieved_vote = db.get::(&vote.id.to_string())?; + println!("Vote: {} (Status: {:?})", retrieved_vote.title, retrieved_vote.status); + + if let Ok(Some(vote_res)) = retrieved_vote.get_resolution(&db) { + println!("Vote is linked to resolution: {}", vote_res.title); + } + + // Get resolution and its related objects + let retrieved_resolution = db.get::(&resolution.id.to_string())?; + println!("Resolution: {} (Status: {:?})", retrieved_resolution.title, retrieved_resolution.status); + + if let Ok(Some(res_meeting)) = retrieved_resolution.get_meeting(&db) { + println!("Resolution is discussed in meeting: {}", res_meeting.title); + } + + println!("\nExample completed successfully!"); + Ok(()) +} \ No newline at end of file diff --git a/herodb/src/cmd/dbexample_mcc/main.rs b/herodb/src/cmd/dbexample_mcc/main.rs new file mode 100644 index 0000000..18a7d3a --- /dev/null +++ b/herodb/src/cmd/dbexample_mcc/main.rs @@ -0,0 +1,399 @@ +use chrono::{Utc, Duration}; +use herodb::db::DBBuilder; +use herodb::models::mcc::{ + Calendar, Event, + Email, Attachment, Envelope, + Contact, Message +}; +use herodb::models::circle::Circle; +use std::path::PathBuf; +use std::fs; + +fn main() -> Result<(), Box> { + println!("DB Example MCC: Mail, Calendar, Contacts with Group Support"); + println!("======================================================="); + + // Create a temporary directory for the database + let db_path = PathBuf::from("/tmp/dbexample_mcc"); + if db_path.exists() { + fs::remove_dir_all(&db_path)?; + } + fs::create_dir_all(&db_path)?; + println!("Database path: {:?}", db_path); + + // Create a database instance with our models registered + let db = DBBuilder::new(&db_path) + .register_model::() + .register_model::() + .register_model::() + .register_model::() + .register_model::() + .register_model::() + .build()?; + + println!("\n1. Creating Circles (Groups)"); + println!("---------------------------"); + + // Create circles (groups) + let work_circle = Circle::new( + 1, + "Work".to_string(), + "Work-related communications".to_string() + ); + + let family_circle = Circle::new( + 2, + "Family".to_string(), + "Family communications".to_string() + ); + + let friends_circle = Circle::new( + 3, + "Friends".to_string(), + "Friends communications".to_string() + ); + + // Insert circles + db.set::(&work_circle)?; + db.set::(&family_circle)?; + db.set::(&friends_circle)?; + + println!("Created circles:"); + println!(" - Circle #{}: {}", work_circle.id, work_circle.name); + println!(" - Circle #{}: {}", family_circle.id, family_circle.name); + println!(" - Circle #{}: {}", friends_circle.id, friends_circle.name); + + println!("\n2. Creating Contacts with Group Support"); + println!("------------------------------------"); + + // Create contacts + let mut john = Contact::new( + 1, + "John".to_string(), + "Doe".to_string(), + "john.doe@example.com".to_string(), + "work".to_string() + ); + john.add_group(work_circle.id); + + let mut alice = Contact::new( + 2, + "Alice".to_string(), + "Smith".to_string(), + "alice.smith@example.com".to_string(), + "family".to_string() + ); + alice.add_group(family_circle.id); + + let mut bob = Contact::new( + 3, + "Bob".to_string(), + "Johnson".to_string(), + "bob.johnson@example.com".to_string(), + "friends".to_string() + ); + bob.add_group(friends_circle.id); + bob.add_group(work_circle.id); // Bob is both a friend and a work contact + + // Insert contacts + db.set::(&john)?; + db.set::(&alice)?; + db.set::(&bob)?; + + println!("Created contacts:"); + println!(" - {}: {} (Groups: {:?})", john.full_name(), john.email, john.groups); + println!(" - {}: {} (Groups: {:?})", alice.full_name(), alice.email, alice.groups); + println!(" - {}: {} (Groups: {:?})", bob.full_name(), bob.email, bob.groups); + + println!("\n3. Creating Calendars with Group Support"); + println!("-------------------------------------"); + + // Create calendars + let mut work_calendar = Calendar::new( + 1, + "Work Calendar".to_string(), + "Work-related events".to_string() + ); + work_calendar.add_group(work_circle.id); + + let mut personal_calendar = Calendar::new( + 2, + "Personal Calendar".to_string(), + "Personal events".to_string() + ); + personal_calendar.add_group(family_circle.id); + personal_calendar.add_group(friends_circle.id); + + // Insert calendars + db.set::(&work_calendar)?; + db.set::(&personal_calendar)?; + + println!("Created calendars:"); + println!(" - {}: {} (Groups: {:?})", work_calendar.id, work_calendar.title, work_calendar.groups); + println!(" - {}: {} (Groups: {:?})", personal_calendar.id, personal_calendar.title, personal_calendar.groups); + + println!("\n4. Creating Events with Group Support"); + println!("----------------------------------"); + + // Create events + let now = Utc::now(); + let tomorrow = now + Duration::days(1); + let next_week = now + Duration::days(7); + + let mut work_meeting = Event::new( + 1, + work_calendar.id, + "Team Meeting".to_string(), + "Weekly team sync".to_string(), + "Conference Room A".to_string(), + tomorrow, + tomorrow + Duration::hours(1), + "organizer@example.com".to_string() + ); + work_meeting.add_group(work_circle.id); + work_meeting.add_attendee(john.email.clone()); + work_meeting.add_attendee(bob.email.clone()); + + let mut family_dinner = Event::new( + 2, + personal_calendar.id, + "Family Dinner".to_string(), + "Weekly family dinner".to_string(), + "Home".to_string(), + next_week, + next_week + Duration::hours(2), + "me@example.com".to_string() + ); + family_dinner.add_group(family_circle.id); + family_dinner.add_attendee(alice.email.clone()); + + // Insert events + db.set::(&work_meeting)?; + db.set::(&family_dinner)?; + + println!("Created events:"); + println!(" - {}: {} on {} (Groups: {:?})", + work_meeting.id, + work_meeting.title, + work_meeting.start_time.format("%Y-%m-%d %H:%M"), + work_meeting.groups + ); + println!(" - {}: {} on {} (Groups: {:?})", + family_dinner.id, + family_dinner.title, + family_dinner.start_time.format("%Y-%m-%d %H:%M"), + family_dinner.groups + ); + + println!("\n5. Creating Emails with Group Support"); + println!("----------------------------------"); + + // Create emails + let mut work_email = Email::new( + 1, + 101, + 1, + "INBOX".to_string(), + "Here are the meeting notes from yesterday's discussion.".to_string() + ); + work_email.add_group(work_circle.id); + + let work_attachment = Attachment { + filename: "meeting_notes.pdf".to_string(), + content_type: "application/pdf".to_string(), + hash: "abc123def456".to_string(), + size: 1024, + }; + work_email.add_attachment(work_attachment); + + let work_envelope = Envelope { + date: now.timestamp(), + subject: "Meeting Notes".to_string(), + from: vec!["john.doe@example.com".to_string()], + sender: vec!["john.doe@example.com".to_string()], + reply_to: vec!["john.doe@example.com".to_string()], + to: vec!["me@example.com".to_string()], + cc: vec!["bob.johnson@example.com".to_string()], + bcc: vec![], + in_reply_to: "".to_string(), + message_id: "msg123@example.com".to_string(), + }; + work_email.set_envelope(work_envelope); + + let mut family_email = Email::new( + 2, + 102, + 2, + "INBOX".to_string(), + "Looking forward to seeing you at dinner next week!".to_string() + ); + family_email.add_group(family_circle.id); + + let family_envelope = Envelope { + date: now.timestamp(), + subject: "Family Dinner".to_string(), + from: vec!["alice.smith@example.com".to_string()], + sender: vec!["alice.smith@example.com".to_string()], + reply_to: vec!["alice.smith@example.com".to_string()], + to: vec!["me@example.com".to_string()], + cc: vec![], + bcc: vec![], + in_reply_to: "".to_string(), + message_id: "msg456@example.com".to_string(), + }; + family_email.set_envelope(family_envelope); + + // Insert emails + db.set::(&work_email)?; + db.set::(&family_email)?; + + println!("Created emails:"); + println!(" - From: {}, Subject: {} (Groups: {:?})", + work_email.envelope.as_ref().unwrap().from[0], + work_email.envelope.as_ref().unwrap().subject, + work_email.groups + ); + println!(" - From: {}, Subject: {} (Groups: {:?})", + family_email.envelope.as_ref().unwrap().from[0], + family_email.envelope.as_ref().unwrap().subject, + family_email.groups + ); + + println!("\n6. Creating Messages (Chat) with Group Support"); + println!("-----------------------------------------"); + + // Create messages + let mut work_chat = Message::new( + 1, + "thread_work_123".to_string(), + "john.doe@example.com".to_string(), + "Can we move the meeting to 3pm?".to_string() + ); + work_chat.add_group(work_circle.id); + work_chat.add_recipient("me@example.com".to_string()); + work_chat.add_recipient("bob.johnson@example.com".to_string()); + + let mut friends_chat = Message::new( + 2, + "thread_friends_456".to_string(), + "bob.johnson@example.com".to_string(), + "Are we still on for the game this weekend?".to_string() + ); + friends_chat.add_group(friends_circle.id); + friends_chat.add_recipient("me@example.com".to_string()); + friends_chat.add_reaction("ðŸ‘".to_string()); + + // Insert messages + db.set::(&work_chat)?; + db.set::(&friends_chat)?; + + println!("Created messages:"); + println!(" - From: {}, Content: {} (Groups: {:?})", + work_chat.sender_id, + work_chat.content, + work_chat.groups + ); + println!(" - From: {}, Content: {} (Groups: {:?}, Reactions: {:?})", + friends_chat.sender_id, + friends_chat.content, + friends_chat.groups, + friends_chat.meta.reactions + ); + + println!("\n7. Demonstrating Utility Methods"); + println!("------------------------------"); + + // Filter contacts by group + println!("\nFiltering contacts by work group (ID: {}):", work_circle.id); + let all_contacts = db.list::()?; + for contact in all_contacts { + if contact.filter_by_groups(&[work_circle.id]) { + println!(" - {} ({})", contact.full_name(), contact.email); + } + } + + // Search emails by subject + println!("\nSearching emails with subject containing 'Meeting':"); + let all_emails = db.list::()?; + for email in all_emails { + if email.search_by_subject("Meeting") { + println!(" - Subject: {}, From: {}", + email.envelope.as_ref().unwrap().subject, + email.envelope.as_ref().unwrap().from[0] + ); + } + } + + // Get events for a calendar + println!("\nGetting events for Work Calendar (ID: {}):", work_calendar.id); + let all_events = db.list::()?; + let work_events: Vec = all_events + .into_iter() + .filter(|event| event.calendar_id == work_calendar.id) + .collect(); + for event in work_events { + println!(" - {}: {} on {}", + event.id, + event.title, + event.start_time.format("%Y-%m-%d %H:%M") + ); + } + + // Get attendee contacts for an event + println!("\nGetting attendee contacts for Team Meeting (ID: {}):", work_meeting.id); + let all_contacts = db.list::()?; + let attendee_contacts: Vec = all_contacts + .into_iter() + .filter(|contact| work_meeting.attendees.contains(&contact.email)) + .collect(); + for contact in attendee_contacts { + println!(" - {} ({})", contact.full_name(), contact.email); + } + + // Convert email to message + println!("\nConverting work email to message:"); + let email_to_message = work_email.to_message(3, "thread_converted_789".to_string()); + println!(" - Original Email Subject: {}", work_email.envelope.as_ref().unwrap().subject); + println!(" - Converted Message Content: {}", email_to_message.content.split('\n').next().unwrap_or("")); + println!(" - Converted Message Groups: {:?}", email_to_message.groups); + + // Insert the converted message + db.set::(&email_to_message)?; + + println!("\n8. Relationship Management"); + println!("------------------------"); + + // Get the calendar for an event + println!("\nGetting calendar for Family Dinner event (ID: {}):", family_dinner.id); + let event_calendar = db.get::(&family_dinner.calendar_id.to_string())?; + println!(" - Calendar: {} ({})", event_calendar.title, event_calendar.description); + + // Get events for a contact + println!("\nGetting events where John Doe is an attendee:"); + let all_events = db.list::()?; + let john_events: Vec = all_events + .into_iter() + .filter(|event| event.attendees.contains(&john.email)) + .collect(); + for event in john_events { + println!(" - {}: {} on {}", + event.id, + event.title, + event.start_time.format("%Y-%m-%d %H:%M") + ); + } + + // Get messages in the same thread + println!("\nGetting all messages in the work chat thread:"); + let all_messages = db.list::()?; + let thread_messages: Vec = all_messages + .into_iter() + .filter(|message| message.thread_id == work_chat.thread_id) + .collect(); + for message in thread_messages { + println!(" - From: {}, Content: {}", message.sender_id, message.content); + } + + println!("\nExample completed successfully!"); + Ok(()) +} \ No newline at end of file diff --git a/herodb/src/db/model_methods.rs b/herodb/src/db/model_methods.rs index 45fffd7..297b01a 100644 --- a/herodb/src/db/model_methods.rs +++ b/herodb/src/db/model_methods.rs @@ -1,7 +1,7 @@ use crate::db::db::DB; use crate::db::base::{SledDBResult, SledModel}; use crate::impl_model_methods; -use crate::models::biz::{Product, Sale, Currency}; +use crate::models::biz::{Product, Sale, Currency, ExchangeRate, Service, Customer, Contract, Invoice}; // Implement model-specific methods for Product impl_model_methods!(Product, product, products); @@ -10,4 +10,19 @@ impl_model_methods!(Product, product, products); impl_model_methods!(Sale, sale, sales); // Implement model-specific methods for Currency -impl_model_methods!(Currency, currency, currencies); \ No newline at end of file +impl_model_methods!(Currency, currency, currencies); + +// Implement model-specific methods for ExchangeRate +impl_model_methods!(ExchangeRate, exchange_rate, exchange_rates); + +// Implement model-specific methods for Service +impl_model_methods!(Service, service, services); + +// Implement model-specific methods for Customer +impl_model_methods!(Customer, customer, customers); + +// Implement model-specific methods for Contract +impl_model_methods!(Contract, contract, contracts); + +// Implement model-specific methods for Invoice +impl_model_methods!(Invoice, invoice, invoices); \ No newline at end of file diff --git a/herodb/src/models/biz/business_models_plan.md b/herodb/src/models/biz/business_models_plan.md new file mode 100644 index 0000000..d68ec76 --- /dev/null +++ b/herodb/src/models/biz/business_models_plan.md @@ -0,0 +1,371 @@ +# Business Models Implementation Plan + +## Overview + +This document outlines the plan for implementing new business models in the codebase: + +1. **Service**: For tracking recurring payments (similar to Sale) +2. **Customer**: For storing customer information +3. **Contract**: For linking services or sales to customers +4. **Invoice**: For invoicing customers + +## Model Diagrams + +### Core Models and Relationships + +```mermaid +classDiagram + class Service { + +id: u32 + +customer_id: u32 + +total_amount: Currency + +status: ServiceStatus + +billing_frequency: BillingFrequency + +service_date: DateTime~Utc~ + +created_at: DateTime~Utc~ + +updated_at: DateTime~Utc~ + +items: Vec~ServiceItem~ + +calculate_total() + } + + class ServiceItem { + +id: u32 + +service_id: u32 + +name: String + +quantity: i32 + +unit_price: Currency + +subtotal: Currency + +tax_rate: f64 + +tax_amount: Currency + +is_taxable: bool + +active_till: DateTime~Utc~ + } + + class Customer { + +id: u32 + +name: String + +description: String + +pubkey: String + +contact_ids: Vec~u32~ + +created_at: DateTime~Utc~ + +updated_at: DateTime~Utc~ + } + + class Contract { + +id: u32 + +customer_id: u32 + +service_id: Option~u32~ + +sale_id: Option~u32~ + +terms: String + +start_date: DateTime~Utc~ + +end_date: DateTime~Utc~ + +auto_renewal: bool + +renewal_terms: String + +status: ContractStatus + +created_at: DateTime~Utc~ + +updated_at: DateTime~Utc~ + } + + class Invoice { + +id: u32 + +customer_id: u32 + +total_amount: Currency + +balance_due: Currency + +status: InvoiceStatus + +payment_status: PaymentStatus + +issue_date: DateTime~Utc~ + +due_date: DateTime~Utc~ + +created_at: DateTime~Utc~ + +updated_at: DateTime~Utc~ + +items: Vec~InvoiceItem~ + +payments: Vec~Payment~ + } + + class InvoiceItem { + +id: u32 + +invoice_id: u32 + +description: String + +amount: Currency + +service_id: Option~u32~ + +sale_id: Option~u32~ + } + + class Payment { + +amount: Currency + +date: DateTime~Utc~ + +method: String + } + + Service "1" -- "many" ServiceItem : contains + Customer "1" -- "many" Service : has + Customer "1" -- "many" Contract : has + Contract "1" -- "0..1" Service : references + Contract "1" -- "0..1" Sale : references + Invoice "1" -- "many" InvoiceItem : contains + Invoice "1" -- "many" Payment : contains + Customer "1" -- "many" Invoice : has + InvoiceItem "1" -- "0..1" Service : references + InvoiceItem "1" -- "0..1" Sale : references +``` + +### Enums and Supporting Types + +```mermaid +classDiagram + class BillingFrequency { + <> + Hourly + Daily + Weekly + Monthly + Yearly + } + + class ServiceStatus { + <> + Active + Paused + Cancelled + Completed + } + + class ContractStatus { + <> + Active + Expired + Terminated + } + + class InvoiceStatus { + <> + Draft + Sent + Paid + Overdue + Cancelled + } + + class PaymentStatus { + <> + Unpaid + PartiallyPaid + Paid + } + + Service -- ServiceStatus : has + Service -- BillingFrequency : has + Contract -- ContractStatus : has + Invoice -- InvoiceStatus : has + Invoice -- PaymentStatus : has +``` + +## Detailed Implementation Plan + +### 1. Service and ServiceItem (service.rs) + +The Service model will be similar to Sale but designed for recurring payments: + +- **Service**: Main struct for tracking recurring services + - Fields: + - id: u32 + - customer_id: u32 + - total_amount: Currency + - status: ServiceStatus + - billing_frequency: BillingFrequency + - service_date: DateTime + - created_at: DateTime + - updated_at: DateTime + - items: Vec + - Methods: + - calculate_total(): Updates the total_amount based on all items + - add_item(item: ServiceItem): Adds an item and updates the total + - update_status(status: ServiceStatus): Updates the status and timestamp + +- **ServiceItem**: Items within a service (similar to SaleItem) + - Fields: + - id: u32 + - service_id: u32 + - name: String + - quantity: i32 + - unit_price: Currency + - subtotal: Currency + - tax_rate: f64 + - tax_amount: Currency + - is_taxable: bool + - active_till: DateTime + - Methods: + - calculate_subtotal(): Calculates subtotal based on quantity and unit_price + - calculate_tax(): Calculates tax amount based on subtotal and tax_rate + +- **BillingFrequency**: Enum for different billing periods + - Variants: Hourly, Daily, Weekly, Monthly, Yearly + +- **ServiceStatus**: Enum for service status + - Variants: Active, Paused, Cancelled, Completed + +### 2. Customer (customer.rs) + +The Customer model will store customer information: + +- **Customer**: Main struct for customer data + - Fields: + - id: u32 + - name: String + - description: String + - pubkey: String + - contact_ids: Vec + - created_at: DateTime + - updated_at: DateTime + - Methods: + - add_contact(contact_id: u32): Adds a contact ID to the list + - remove_contact(contact_id: u32): Removes a contact ID from the list + +### 3. Contract (contract.rs) + +The Contract model will link services or sales to customers: + +- **Contract**: Main struct for contract data + - Fields: + - id: u32 + - customer_id: u32 + - service_id: Option + - sale_id: Option + - terms: String + - start_date: DateTime + - end_date: DateTime + - auto_renewal: bool + - renewal_terms: String + - status: ContractStatus + - created_at: DateTime + - updated_at: DateTime + - Methods: + - is_active(): bool - Checks if the contract is currently active + - is_expired(): bool - Checks if the contract has expired + - renew(): Updates the contract dates based on renewal terms + +- **ContractStatus**: Enum for contract status + - Variants: Active, Expired, Terminated + +### 4. Invoice (invoice.rs) + +The Invoice model will handle billing: + +- **Invoice**: Main struct for invoice data + - Fields: + - id: u32 + - customer_id: u32 + - total_amount: Currency + - balance_due: Currency + - status: InvoiceStatus + - payment_status: PaymentStatus + - issue_date: DateTime + - due_date: DateTime + - created_at: DateTime + - updated_at: DateTime + - items: Vec + - payments: Vec + - Methods: + - calculate_total(): Updates the total_amount based on all items + - add_item(item: InvoiceItem): Adds an item and updates the total + - add_payment(payment: Payment): Adds a payment and updates balance_due and payment_status + - update_status(status: InvoiceStatus): Updates the status and timestamp + - calculate_balance(): Updates the balance_due based on total_amount and payments + +- **InvoiceItem**: Items within an invoice + - Fields: + - id: u32 + - invoice_id: u32 + - description: String + - amount: Currency + - service_id: Option + - sale_id: Option + +- **Payment**: Struct for tracking payments + - Fields: + - amount: Currency + - date: DateTime + - method: String + +- **InvoiceStatus**: Enum for invoice status + - Variants: Draft, Sent, Paid, Overdue, Cancelled + +- **PaymentStatus**: Enum for payment status + - Variants: Unpaid, PartiallyPaid, Paid + +### 5. Updates to mod.rs + +We'll need to update the mod.rs file to include the new modules and re-export the types: + +```rust +pub mod currency; +pub mod product; +pub mod sale; +pub mod exchange_rate; +pub mod service; +pub mod customer; +pub mod contract; +pub mod invoice; + +// Re-export all model types for convenience +pub use product::{Product, ProductComponent, ProductType, ProductStatus}; +pub use sale::{Sale, SaleItem, SaleStatus}; +pub use currency::Currency; +pub use exchange_rate::{ExchangeRate, ExchangeRateService, EXCHANGE_RATE_SERVICE}; +pub use service::{Service, ServiceItem, ServiceStatus, BillingFrequency}; +pub use customer::Customer; +pub use contract::{Contract, ContractStatus}; +pub use invoice::{Invoice, InvoiceItem, InvoiceStatus, PaymentStatus, Payment}; + +// Re-export builder types +pub use product::{ProductBuilder, ProductComponentBuilder}; +pub use sale::{SaleBuilder, SaleItemBuilder}; +pub use currency::CurrencyBuilder; +pub use exchange_rate::ExchangeRateBuilder; +pub use service::{ServiceBuilder, ServiceItemBuilder}; +pub use customer::CustomerBuilder; +pub use contract::ContractBuilder; +pub use invoice::{InvoiceBuilder, InvoiceItemBuilder}; +``` + +### 6. Updates to model_methods.rs + +We'll need to update the model_methods.rs file to implement the model methods for the new models: + +```rust +use crate::db::db::DB; +use crate::db::base::{SledDBResult, SledModel}; +use crate::impl_model_methods; +use crate::models::biz::{Product, Sale, Currency, ExchangeRate, Service, Customer, Contract, Invoice}; + +// Implement model-specific methods for Product +impl_model_methods!(Product, product, products); + +// Implement model-specific methods for Sale +impl_model_methods!(Sale, sale, sales); + +// Implement model-specific methods for Currency +impl_model_methods!(Currency, currency, currencies); + +// Implement model-specific methods for ExchangeRate +impl_model_methods!(ExchangeRate, exchange_rate, exchange_rates); + +// Implement model-specific methods for Service +impl_model_methods!(Service, service, services); + +// Implement model-specific methods for Customer +impl_model_methods!(Customer, customer, customers); + +// Implement model-specific methods for Contract +impl_model_methods!(Contract, contract, contracts); + +// Implement model-specific methods for Invoice +impl_model_methods!(Invoice, invoice, invoices); +``` + +## Implementation Approach + +1. Create the new model files (service.rs, customer.rs, contract.rs, invoice.rs) +2. Implement the structs, enums, and methods for each model +3. Update mod.rs to include the new modules and re-export the types +4. Update model_methods.rs to implement the model methods for the new models +5. Test the new models with example code \ No newline at end of file diff --git a/herodb/src/models/biz/contract.rs b/herodb/src/models/biz/contract.rs new file mode 100644 index 0000000..e4c4dcc --- /dev/null +++ b/herodb/src/models/biz/contract.rs @@ -0,0 +1,250 @@ +use crate::db::base::{SledModel, Storable}; // Import Sled traits from db module +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; + +/// ContractStatus represents the status of a contract +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum ContractStatus { + Active, + Expired, + Terminated, +} + +/// Contract represents a legal agreement between a customer and the business +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Contract { + pub id: u32, + pub customer_id: u32, + pub service_id: Option, + pub sale_id: Option, + pub terms: String, + pub start_date: DateTime, + pub end_date: DateTime, + pub auto_renewal: bool, + pub renewal_terms: String, + pub status: ContractStatus, + pub created_at: DateTime, + pub updated_at: DateTime, +} + +impl Contract { + /// Create a new contract with default timestamps + pub fn new( + id: u32, + customer_id: u32, + terms: String, + start_date: DateTime, + end_date: DateTime, + auto_renewal: bool, + renewal_terms: String, + ) -> Self { + let now = Utc::now(); + Self { + id, + customer_id, + service_id: None, + sale_id: None, + terms, + start_date, + end_date, + auto_renewal, + renewal_terms, + status: ContractStatus::Active, + created_at: now, + updated_at: now, + } + } + + /// Link the contract to a service + pub fn link_to_service(&mut self, service_id: u32) { + self.service_id = Some(service_id); + self.sale_id = None; // A contract can only be linked to either a service or a sale + self.updated_at = Utc::now(); + } + + /// Link the contract to a sale + pub fn link_to_sale(&mut self, sale_id: u32) { + self.sale_id = Some(sale_id); + self.service_id = None; // A contract can only be linked to either a service or a sale + self.updated_at = Utc::now(); + } + + /// Check if the contract is currently active + pub fn is_active(&self) -> bool { + let now = Utc::now(); + self.status == ContractStatus::Active && + now >= self.start_date && + now <= self.end_date + } + + /// Check if the contract has expired + pub fn is_expired(&self) -> bool { + let now = Utc::now(); + now > self.end_date + } + + /// Update the contract status + pub fn update_status(&mut self, status: ContractStatus) { + self.status = status; + self.updated_at = Utc::now(); + } + + /// Renew the contract based on renewal terms + pub fn renew(&mut self) -> Result<(), &'static str> { + if !self.auto_renewal { + return Err("Contract is not set for auto-renewal"); + } + + if self.status != ContractStatus::Active { + return Err("Cannot renew a non-active contract"); + } + + // Calculate new dates based on the current end date + let duration = self.end_date - self.start_date; + self.start_date = self.end_date; + self.end_date = self.end_date + duration; + + self.updated_at = Utc::now(); + Ok(()) + } +} + +/// Builder for Contract +pub struct ContractBuilder { + id: Option, + customer_id: Option, + service_id: Option, + sale_id: Option, + terms: Option, + start_date: Option>, + end_date: Option>, + auto_renewal: Option, + renewal_terms: Option, + status: Option, + created_at: Option>, + updated_at: Option>, +} + +impl ContractBuilder { + /// Create a new ContractBuilder with all fields set to None + pub fn new() -> Self { + Self { + id: None, + customer_id: None, + service_id: None, + sale_id: None, + terms: None, + start_date: None, + end_date: None, + auto_renewal: None, + renewal_terms: None, + status: None, + created_at: None, + updated_at: None, + } + } + + /// Set the id + pub fn id(mut self, id: u32) -> Self { + self.id = Some(id); + self + } + + /// Set the customer_id + pub fn customer_id(mut self, customer_id: u32) -> Self { + self.customer_id = Some(customer_id); + self + } + + /// Set the service_id + pub fn service_id(mut self, service_id: u32) -> Self { + self.service_id = Some(service_id); + self.sale_id = None; // A contract can only be linked to either a service or a sale + self + } + + /// Set the sale_id + pub fn sale_id(mut self, sale_id: u32) -> Self { + self.sale_id = Some(sale_id); + self.service_id = None; // A contract can only be linked to either a service or a sale + self + } + + /// Set the terms + pub fn terms>(mut self, terms: S) -> Self { + self.terms = Some(terms.into()); + self + } + + /// Set the start_date + pub fn start_date(mut self, start_date: DateTime) -> Self { + self.start_date = Some(start_date); + self + } + + /// Set the end_date + pub fn end_date(mut self, end_date: DateTime) -> Self { + self.end_date = Some(end_date); + self + } + + /// Set auto_renewal + pub fn auto_renewal(mut self, auto_renewal: bool) -> Self { + self.auto_renewal = Some(auto_renewal); + self + } + + /// Set the renewal_terms + pub fn renewal_terms>(mut self, renewal_terms: S) -> Self { + self.renewal_terms = Some(renewal_terms.into()); + self + } + + /// Set the status + pub fn status(mut self, status: ContractStatus) -> Self { + self.status = Some(status); + self + } + + /// Build the Contract object + pub fn build(self) -> Result { + let now = Utc::now(); + + // Validate that start_date is before end_date + let start_date = self.start_date.ok_or("start_date is required")?; + let end_date = self.end_date.ok_or("end_date is required")?; + + if start_date >= end_date { + return Err("start_date must be before end_date"); + } + + Ok(Contract { + id: self.id.ok_or("id is required")?, + customer_id: self.customer_id.ok_or("customer_id is required")?, + service_id: self.service_id, + sale_id: self.sale_id, + terms: self.terms.ok_or("terms is required")?, + start_date, + end_date, + auto_renewal: self.auto_renewal.unwrap_or(false), + renewal_terms: self.renewal_terms.ok_or("renewal_terms is required")?, + status: self.status.unwrap_or(ContractStatus::Active), + created_at: self.created_at.unwrap_or(now), + updated_at: self.updated_at.unwrap_or(now), + }) + } +} + +// Implement Storable trait (provides default dump/load) +impl Storable for Contract {} + +// Implement SledModel trait +impl SledModel for Contract { + fn get_id(&self) -> String { + self.id.to_string() + } + + fn db_prefix() -> &'static str { + "contract" + } +} \ No newline at end of file diff --git a/herodb/src/models/biz/customer.rs b/herodb/src/models/biz/customer.rs new file mode 100644 index 0000000..757b763 --- /dev/null +++ b/herodb/src/models/biz/customer.rs @@ -0,0 +1,148 @@ +use crate::db::base::{SledModel, Storable}; // Import Sled traits from db module +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; + +/// Customer represents a customer who can purchase products or services +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Customer { + pub id: u32, + pub name: String, + pub description: String, + pub pubkey: String, + pub contact_ids: Vec, + pub created_at: DateTime, + pub updated_at: DateTime, +} + +impl Customer { + /// Create a new customer with default timestamps + pub fn new( + id: u32, + name: String, + description: String, + pubkey: String, + ) -> Self { + let now = Utc::now(); + Self { + id, + name, + description, + pubkey, + contact_ids: Vec::new(), + created_at: now, + updated_at: now, + } + } + + /// Add a contact ID to the customer + pub fn add_contact(&mut self, contact_id: u32) { + if !self.contact_ids.contains(&contact_id) { + self.contact_ids.push(contact_id); + self.updated_at = Utc::now(); + } + } + + /// Remove a contact ID from the customer + pub fn remove_contact(&mut self, contact_id: u32) -> bool { + let len = self.contact_ids.len(); + self.contact_ids.retain(|&id| id != contact_id); + + if self.contact_ids.len() < len { + self.updated_at = Utc::now(); + true + } else { + false + } + } +} + +/// Builder for Customer +pub struct CustomerBuilder { + id: Option, + name: Option, + description: Option, + pubkey: Option, + contact_ids: Vec, + created_at: Option>, + updated_at: Option>, +} + +impl CustomerBuilder { + /// Create a new CustomerBuilder with all fields set to None + pub fn new() -> Self { + Self { + id: None, + name: None, + description: None, + pubkey: None, + contact_ids: Vec::new(), + created_at: None, + updated_at: None, + } + } + + /// Set the id + pub fn id(mut self, id: u32) -> Self { + self.id = Some(id); + self + } + + /// Set the name + pub fn name>(mut self, name: S) -> Self { + self.name = Some(name.into()); + self + } + + /// Set the description + pub fn description>(mut self, description: S) -> Self { + self.description = Some(description.into()); + self + } + + /// Set the pubkey + pub fn pubkey>(mut self, pubkey: S) -> Self { + self.pubkey = Some(pubkey.into()); + self + } + + /// Add a contact ID + pub fn add_contact(mut self, contact_id: u32) -> Self { + self.contact_ids.push(contact_id); + self + } + + /// Set multiple contact IDs + pub fn contact_ids(mut self, contact_ids: Vec) -> Self { + self.contact_ids = contact_ids; + self + } + + /// Build the Customer object + pub fn build(self) -> Result { + let now = Utc::now(); + + Ok(Customer { + id: self.id.ok_or("id is required")?, + name: self.name.ok_or("name is required")?, + description: self.description.ok_or("description is required")?, + pubkey: self.pubkey.ok_or("pubkey is required")?, + contact_ids: self.contact_ids, + created_at: self.created_at.unwrap_or(now), + updated_at: self.updated_at.unwrap_or(now), + }) + } +} + +// Implement Storable trait (provides default dump/load) +impl Storable for Customer {} + +// Implement SledModel trait +impl SledModel for Customer { + fn get_id(&self) -> String { + self.id.to_string() + } + + fn db_prefix() -> &'static str { + "customer" + } +} \ No newline at end of file diff --git a/herodb/src/models/biz/exchange_rate.rs b/herodb/src/models/biz/exchange_rate.rs new file mode 100644 index 0000000..dd1c3e9 --- /dev/null +++ b/herodb/src/models/biz/exchange_rate.rs @@ -0,0 +1,167 @@ +use std::collections::HashMap; +use std::sync::{Arc, Mutex}; +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use crate::db::base::{SledModel, Storable}; + +/// ExchangeRate represents an exchange rate between two currencies +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ExchangeRate { + pub base_currency: String, + pub target_currency: String, + pub rate: f64, + pub timestamp: DateTime, +} + +impl ExchangeRate { + /// Create a new exchange rate + pub fn new(base_currency: String, target_currency: String, rate: f64) -> Self { + Self { + base_currency, + target_currency, + rate, + timestamp: Utc::now(), + } + } +} + +/// Builder for ExchangeRate +pub struct ExchangeRateBuilder { + base_currency: Option, + target_currency: Option, + rate: Option, + timestamp: Option>, +} + +impl ExchangeRateBuilder { + /// Create a new ExchangeRateBuilder with all fields set to None + pub fn new() -> Self { + Self { + base_currency: None, + target_currency: None, + rate: None, + timestamp: None, + } + } + + /// Set the base currency + pub fn base_currency>(mut self, base_currency: S) -> Self { + self.base_currency = Some(base_currency.into()); + self + } + + /// Set the target currency + pub fn target_currency>(mut self, target_currency: S) -> Self { + self.target_currency = Some(target_currency.into()); + self + } + + /// Set the rate + pub fn rate(mut self, rate: f64) -> Self { + self.rate = Some(rate); + self + } + + /// Set the timestamp + pub fn timestamp(mut self, timestamp: DateTime) -> Self { + self.timestamp = Some(timestamp); + self + } + + /// Build the ExchangeRate object + pub fn build(self) -> Result { + let now = Utc::now(); + Ok(ExchangeRate { + base_currency: self.base_currency.ok_or("base_currency is required")?, + target_currency: self.target_currency.ok_or("target_currency is required")?, + rate: self.rate.ok_or("rate is required")?, + timestamp: self.timestamp.unwrap_or(now), + }) + } +} + +// Implement Storable trait (provides default dump/load) +impl Storable for ExchangeRate {} + +// Implement SledModel trait +impl SledModel for ExchangeRate { + fn get_id(&self) -> String { + format!("{}_{}", self.base_currency, self.target_currency) + } + + fn db_prefix() -> &'static str { + "exchange_rate" + } +} + +/// ExchangeRateService provides methods to get and set exchange rates +#[derive(Clone)] +pub struct ExchangeRateService { + rates: Arc>>, +} + +impl ExchangeRateService { + /// Create a new exchange rate service + pub fn new() -> Self { + Self { + rates: Arc::new(Mutex::new(HashMap::new())), + } + } + + /// Set an exchange rate + pub fn set_rate(&self, exchange_rate: ExchangeRate) { + let key = format!("{}_{}", exchange_rate.base_currency, exchange_rate.target_currency); + let mut rates = self.rates.lock().unwrap(); + rates.insert(key, exchange_rate); + } + + /// Get an exchange rate + pub fn get_rate(&self, base_currency: &str, target_currency: &str) -> Option { + let key = format!("{}_{}", base_currency, target_currency); + let rates = self.rates.lock().unwrap(); + rates.get(&key).cloned() + } + + /// Convert an amount from one currency to another + pub fn convert(&self, amount: f64, from_currency: &str, to_currency: &str) -> Option { + // If the currencies are the same, return the amount + if from_currency == to_currency { + return Some(amount); + } + + // Try to get the direct exchange rate + if let Some(rate) = self.get_rate(from_currency, to_currency) { + return Some(amount * rate.rate); + } + + // Try to get the inverse exchange rate + if let Some(rate) = self.get_rate(to_currency, from_currency) { + return Some(amount / rate.rate); + } + + // Try to convert via USD + if from_currency != "USD" && to_currency != "USD" { + if let Some(from_to_usd) = self.convert(amount, from_currency, "USD") { + return self.convert(from_to_usd, "USD", to_currency); + } + } + + None + } +} + +// Create a global instance of the exchange rate service +lazy_static::lazy_static! { + pub static ref EXCHANGE_RATE_SERVICE: ExchangeRateService = { + let service = ExchangeRateService::new(); + + // Set some default exchange rates + service.set_rate(ExchangeRate::new("USD".to_string(), "EUR".to_string(), 0.85)); + service.set_rate(ExchangeRate::new("USD".to_string(), "GBP".to_string(), 0.75)); + service.set_rate(ExchangeRate::new("USD".to_string(), "JPY".to_string(), 110.0)); + service.set_rate(ExchangeRate::new("USD".to_string(), "CAD".to_string(), 1.25)); + service.set_rate(ExchangeRate::new("USD".to_string(), "AUD".to_string(), 1.35)); + + service + }; +} \ No newline at end of file diff --git a/herodb/src/models/biz/invoice.rs b/herodb/src/models/biz/invoice.rs new file mode 100644 index 0000000..e4030c1 --- /dev/null +++ b/herodb/src/models/biz/invoice.rs @@ -0,0 +1,507 @@ +use crate::models::biz::Currency; // Use crate:: for importing from the module +use crate::db::base::{SledModel, Storable}; // Import Sled traits from db module +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; + +/// InvoiceStatus represents the status of an invoice +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum InvoiceStatus { + Draft, + Sent, + Paid, + Overdue, + Cancelled, +} + +/// PaymentStatus represents the payment status of an invoice +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum PaymentStatus { + Unpaid, + PartiallyPaid, + Paid, +} + +/// Payment represents a payment made against an invoice +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Payment { + pub amount: Currency, + pub date: DateTime, + pub method: String, +} + +impl Payment { + /// Create a new payment + pub fn new(amount: Currency, method: String) -> Self { + Self { + amount, + date: Utc::now(), + method, + } + } +} + +/// InvoiceItem represents an item in an invoice +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct InvoiceItem { + pub id: u32, + pub invoice_id: u32, + pub description: String, + pub amount: Currency, + pub service_id: Option, + pub sale_id: Option, +} + +impl InvoiceItem { + /// Create a new invoice item + pub fn new( + id: u32, + invoice_id: u32, + description: String, + amount: Currency, + ) -> Self { + Self { + id, + invoice_id, + description, + amount, + service_id: None, + sale_id: None, + } + } + + /// Link the invoice item to a service + pub fn link_to_service(&mut self, service_id: u32) { + self.service_id = Some(service_id); + self.sale_id = None; // An invoice item can only be linked to either a service or a sale + } + + /// Link the invoice item to a sale + pub fn link_to_sale(&mut self, sale_id: u32) { + self.sale_id = Some(sale_id); + self.service_id = None; // An invoice item can only be linked to either a service or a sale + } +} + +/// Builder for InvoiceItem +pub struct InvoiceItemBuilder { + id: Option, + invoice_id: Option, + description: Option, + amount: Option, + service_id: Option, + sale_id: Option, +} + +impl InvoiceItemBuilder { + /// Create a new InvoiceItemBuilder with all fields set to None + pub fn new() -> Self { + Self { + id: None, + invoice_id: None, + description: None, + amount: None, + service_id: None, + sale_id: None, + } + } + + /// Set the id + pub fn id(mut self, id: u32) -> Self { + self.id = Some(id); + self + } + + /// Set the invoice_id + pub fn invoice_id(mut self, invoice_id: u32) -> Self { + self.invoice_id = Some(invoice_id); + self + } + + /// Set the description + pub fn description>(mut self, description: S) -> Self { + self.description = Some(description.into()); + self + } + + /// Set the amount + pub fn amount(mut self, amount: Currency) -> Self { + self.amount = Some(amount); + self + } + + /// Set the service_id + pub fn service_id(mut self, service_id: u32) -> Self { + self.service_id = Some(service_id); + self.sale_id = None; // An invoice item can only be linked to either a service or a sale + self + } + + /// Set the sale_id + pub fn sale_id(mut self, sale_id: u32) -> Self { + self.sale_id = Some(sale_id); + self.service_id = None; // An invoice item can only be linked to either a service or a sale + self + } + + /// Build the InvoiceItem object + pub fn build(self) -> Result { + Ok(InvoiceItem { + id: self.id.ok_or("id is required")?, + invoice_id: self.invoice_id.ok_or("invoice_id is required")?, + description: self.description.ok_or("description is required")?, + amount: self.amount.ok_or("amount is required")?, + service_id: self.service_id, + sale_id: self.sale_id, + }) + } +} + +/// Invoice represents an invoice sent to a customer +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Invoice { + pub id: u32, + pub customer_id: u32, + pub total_amount: Currency, + pub balance_due: Currency, + pub status: InvoiceStatus, + pub payment_status: PaymentStatus, + pub issue_date: DateTime, + pub due_date: DateTime, + pub created_at: DateTime, + pub updated_at: DateTime, + pub items: Vec, + pub payments: Vec, +} + +impl Invoice { + /// Create a new invoice with default timestamps + pub fn new( + id: u32, + customer_id: u32, + currency_code: String, + issue_date: DateTime, + due_date: DateTime, + ) -> Self { + let now = Utc::now(); + let zero_amount = Currency { + amount: 0.0, + currency_code: currency_code.clone(), + }; + + Self { + id, + customer_id, + total_amount: zero_amount.clone(), + balance_due: zero_amount, + status: InvoiceStatus::Draft, + payment_status: PaymentStatus::Unpaid, + issue_date, + due_date, + created_at: now, + updated_at: now, + items: Vec::new(), + payments: Vec::new(), + } + } + + /// Add an item to the invoice and update the total amount + pub fn add_item(&mut self, item: InvoiceItem) { + // Make sure the item's invoice_id matches this invoice + assert_eq!(self.id, item.invoice_id, "Item invoice_id must match invoice id"); + + // Update the total amount + if self.items.is_empty() { + // First item, initialize the total amount with the same currency + self.total_amount = Currency { + amount: item.amount.amount, + currency_code: item.amount.currency_code.clone(), + }; + self.balance_due = Currency { + amount: item.amount.amount, + currency_code: item.amount.currency_code.clone(), + }; + } else { + // Add to the existing total + // (Assumes all items have the same currency) + self.total_amount.amount += item.amount.amount; + self.balance_due.amount += item.amount.amount; + } + + // Add the item to the list + self.items.push(item); + + // Update the invoice timestamp + self.updated_at = Utc::now(); + } + + /// Calculate the total amount based on all items + pub fn calculate_total(&mut self) { + if self.items.is_empty() { + return; + } + + // Get the currency code from the first item + let currency_code = self.items[0].amount.currency_code.clone(); + + // Calculate the total amount + let mut total = 0.0; + for item in &self.items { + total += item.amount.amount; + } + + // Update the total amount + self.total_amount = Currency { + amount: total, + currency_code: currency_code.clone(), + }; + + // Recalculate the balance due + self.calculate_balance(); + + // Update the invoice timestamp + self.updated_at = Utc::now(); + } + + /// Add a payment to the invoice and update the balance due and payment status + pub fn add_payment(&mut self, payment: Payment) { + // Update the balance due + self.balance_due.amount -= payment.amount.amount; + + // Add the payment to the list + self.payments.push(payment); + + // Update the payment status + self.update_payment_status(); + + // Update the invoice timestamp + self.updated_at = Utc::now(); + } + + /// Calculate the balance due based on total amount and payments + pub fn calculate_balance(&mut self) { + // Start with the total amount + let mut balance = self.total_amount.amount; + + // Subtract all payments + for payment in &self.payments { + balance -= payment.amount.amount; + } + + // Update the balance due + self.balance_due = Currency { + amount: balance, + currency_code: self.total_amount.currency_code.clone(), + }; + + // Update the payment status + self.update_payment_status(); + } + + /// Update the payment status based on the balance due + fn update_payment_status(&mut self) { + if self.balance_due.amount <= 0.0 { + self.payment_status = PaymentStatus::Paid; + // If fully paid, also update the invoice status + if self.status != InvoiceStatus::Cancelled { + self.status = InvoiceStatus::Paid; + } + } else if self.payments.is_empty() { + self.payment_status = PaymentStatus::Unpaid; + } else { + self.payment_status = PaymentStatus::PartiallyPaid; + } + } + + /// Update the status of the invoice + pub fn update_status(&mut self, status: InvoiceStatus) { + self.status = status; + self.updated_at = Utc::now(); + + // If the invoice is cancelled, don't change the payment status + if status != InvoiceStatus::Cancelled { + // Re-evaluate the payment status + self.update_payment_status(); + } + } + + /// Check if the invoice is overdue + pub fn is_overdue(&self) -> bool { + let now = Utc::now(); + self.payment_status != PaymentStatus::Paid && + now > self.due_date && + self.status != InvoiceStatus::Cancelled + } + + /// Mark the invoice as overdue if it's past the due date + pub fn check_if_overdue(&mut self) -> bool { + if self.is_overdue() && self.status != InvoiceStatus::Overdue { + self.status = InvoiceStatus::Overdue; + self.updated_at = Utc::now(); + true + } else { + false + } + } +} + +/// Builder for Invoice +pub struct InvoiceBuilder { + id: Option, + customer_id: Option, + total_amount: Option, + balance_due: Option, + status: Option, + payment_status: Option, + issue_date: Option>, + due_date: Option>, + created_at: Option>, + updated_at: Option>, + items: Vec, + payments: Vec, + currency_code: Option, +} + +impl InvoiceBuilder { + /// Create a new InvoiceBuilder with all fields set to None + pub fn new() -> Self { + Self { + id: None, + customer_id: None, + total_amount: None, + balance_due: None, + status: None, + payment_status: None, + issue_date: None, + due_date: None, + created_at: None, + updated_at: None, + items: Vec::new(), + payments: Vec::new(), + currency_code: None, + } + } + + /// Set the id + pub fn id(mut self, id: u32) -> Self { + self.id = Some(id); + self + } + + /// Set the customer_id + pub fn customer_id(mut self, customer_id: u32) -> Self { + self.customer_id = Some(customer_id); + self + } + + /// Set the currency_code + pub fn currency_code>(mut self, currency_code: S) -> Self { + self.currency_code = Some(currency_code.into()); + self + } + + /// Set the status + pub fn status(mut self, status: InvoiceStatus) -> Self { + self.status = Some(status); + self + } + + /// Set the issue_date + pub fn issue_date(mut self, issue_date: DateTime) -> Self { + self.issue_date = Some(issue_date); + self + } + + /// Set the due_date + pub fn due_date(mut self, due_date: DateTime) -> Self { + self.due_date = Some(due_date); + self + } + + /// Add an item to the invoice + pub fn add_item(mut self, item: InvoiceItem) -> Self { + self.items.push(item); + self + } + + /// Add a payment to the invoice + pub fn add_payment(mut self, payment: Payment) -> Self { + self.payments.push(payment); + self + } + + /// Build the Invoice object + pub fn build(self) -> Result { + let now = Utc::now(); + let id = self.id.ok_or("id is required")?; + let currency_code = self.currency_code.ok_or("currency_code is required")?; + + // Initialize with empty total amount and balance due + let mut total_amount = Currency { + amount: 0.0, + currency_code: currency_code.clone(), + }; + + // Calculate total amount from items + for item in &self.items { + // Make sure the item's invoice_id matches this invoice + if item.invoice_id != id { + return Err("Item invoice_id must match invoice id"); + } + + total_amount.amount += item.amount.amount; + } + + // Calculate balance due (total minus payments) + let mut balance_due = total_amount.clone(); + for payment in &self.payments { + balance_due.amount -= payment.amount.amount; + } + + // Determine payment status + let payment_status = if balance_due.amount <= 0.0 { + PaymentStatus::Paid + } else if self.payments.is_empty() { + PaymentStatus::Unpaid + } else { + PaymentStatus::PartiallyPaid + }; + + // Determine invoice status if not provided + let status = if let Some(status) = self.status { + status + } else if payment_status == PaymentStatus::Paid { + InvoiceStatus::Paid + } else { + InvoiceStatus::Draft + }; + + Ok(Invoice { + id, + customer_id: self.customer_id.ok_or("customer_id is required")?, + total_amount: self.total_amount.unwrap_or(total_amount), + balance_due: self.balance_due.unwrap_or(balance_due), + status, + payment_status, + issue_date: self.issue_date.ok_or("issue_date is required")?, + due_date: self.due_date.ok_or("due_date is required")?, + created_at: self.created_at.unwrap_or(now), + updated_at: self.updated_at.unwrap_or(now), + items: self.items, + payments: self.payments, + }) + } +} + +// Implement Storable trait (provides default dump/load) +impl Storable for Invoice {} + +// Implement SledModel trait +impl SledModel for Invoice { + fn get_id(&self) -> String { + self.id.to_string() + } + + fn db_prefix() -> &'static str { + "invoice" + } +} \ No newline at end of file diff --git a/herodb/src/models/biz/mod.rs b/herodb/src/models/biz/mod.rs index c7c3372..219a1c4 100644 --- a/herodb/src/models/biz/mod.rs +++ b/herodb/src/models/biz/mod.rs @@ -1,13 +1,28 @@ pub mod currency; pub mod product; pub mod sale; +pub mod exchange_rate; +pub mod service; +pub mod customer; +pub mod contract; +pub mod invoice; // Re-export all model types for convenience pub use product::{Product, ProductComponent, ProductType, ProductStatus}; pub use sale::{Sale, SaleItem, SaleStatus}; pub use currency::Currency; +pub use exchange_rate::{ExchangeRate, ExchangeRateService, EXCHANGE_RATE_SERVICE}; +pub use service::{Service, ServiceItem, ServiceStatus, BillingFrequency}; +pub use customer::Customer; +pub use contract::{Contract, ContractStatus}; +pub use invoice::{Invoice, InvoiceItem, InvoiceStatus, PaymentStatus, Payment}; // Re-export builder types pub use product::{ProductBuilder, ProductComponentBuilder}; pub use sale::{SaleBuilder, SaleItemBuilder}; -pub use currency::CurrencyBuilder; \ No newline at end of file +pub use currency::CurrencyBuilder; +pub use exchange_rate::ExchangeRateBuilder; +pub use service::{ServiceBuilder, ServiceItemBuilder}; +pub use customer::CustomerBuilder; +pub use contract::ContractBuilder; +pub use invoice::{InvoiceBuilder, InvoiceItemBuilder}; \ No newline at end of file diff --git a/herodb/src/models/biz/service.rs b/herodb/src/models/biz/service.rs new file mode 100644 index 0000000..2c1eb05 --- /dev/null +++ b/herodb/src/models/biz/service.rs @@ -0,0 +1,469 @@ +use crate::models::biz::Currency; // Use crate:: for importing from the module +use crate::db::base::{SledModel, Storable}; // Import Sled traits from db module +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; + +/// BillingFrequency represents the frequency of billing for a service +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum BillingFrequency { + Hourly, + Daily, + Weekly, + Monthly, + Yearly, +} + +/// ServiceStatus represents the status of a service +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum ServiceStatus { + Active, + Paused, + Cancelled, + Completed, +} + +/// ServiceItem represents an item in a service +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ServiceItem { + pub id: u32, + pub service_id: u32, + pub product_id: u32, + pub name: String, + pub quantity: i32, + pub unit_price: Currency, + pub subtotal: Currency, + pub tax_rate: f64, + pub tax_amount: Currency, + pub is_taxable: bool, + pub active_till: DateTime, +} + +impl ServiceItem { + /// Create a new service item + pub fn new( + id: u32, + service_id: u32, + product_id: u32, + name: String, + quantity: i32, + unit_price: Currency, + tax_rate: f64, + is_taxable: bool, + active_till: DateTime, + ) -> Self { + // Calculate subtotal + let amount = unit_price.amount * quantity as f64; + let subtotal = Currency { + amount, + currency_code: unit_price.currency_code.clone(), + }; + + // Calculate tax amount if taxable + let tax_amount = if is_taxable { + Currency { + amount: subtotal.amount * tax_rate, + currency_code: unit_price.currency_code.clone(), + } + } else { + Currency { + amount: 0.0, + currency_code: unit_price.currency_code.clone(), + } + }; + + Self { + id, + service_id, + product_id, + name, + quantity, + unit_price, + subtotal, + tax_rate, + tax_amount, + is_taxable, + active_till, + } + } + + /// Calculate the subtotal based on quantity and unit price + pub fn calculate_subtotal(&mut self) { + let amount = self.unit_price.amount * self.quantity as f64; + self.subtotal = Currency { + amount, + currency_code: self.unit_price.currency_code.clone(), + }; + } + + /// Calculate the tax amount based on subtotal and tax rate + pub fn calculate_tax(&mut self) { + if self.is_taxable { + self.tax_amount = Currency { + amount: self.subtotal.amount * self.tax_rate, + currency_code: self.subtotal.currency_code.clone(), + }; + } else { + self.tax_amount = Currency { + amount: 0.0, + currency_code: self.subtotal.currency_code.clone(), + }; + } + } +} + +/// Builder for ServiceItem +pub struct ServiceItemBuilder { + id: Option, + service_id: Option, + product_id: Option, + name: Option, + quantity: Option, + unit_price: Option, + subtotal: Option, + tax_rate: Option, + tax_amount: Option, + is_taxable: Option, + active_till: Option>, +} + +impl ServiceItemBuilder { + /// Create a new ServiceItemBuilder with all fields set to None + pub fn new() -> Self { + Self { + id: None, + service_id: None, + product_id: None, + name: None, + quantity: None, + unit_price: None, + subtotal: None, + tax_rate: None, + tax_amount: None, + is_taxable: None, + active_till: None, + } + } + + /// Set the id + pub fn id(mut self, id: u32) -> Self { + self.id = Some(id); + self + } + + /// Set the service_id + pub fn service_id(mut self, service_id: u32) -> Self { + self.service_id = Some(service_id); + self + } + + /// Set the product_id + pub fn product_id(mut self, product_id: u32) -> Self { + self.product_id = Some(product_id); + self + } + + /// Set the name + pub fn name>(mut self, name: S) -> Self { + self.name = Some(name.into()); + self + } + + /// Set the quantity + pub fn quantity(mut self, quantity: i32) -> Self { + self.quantity = Some(quantity); + self + } + + /// Set the unit_price + pub fn unit_price(mut self, unit_price: Currency) -> Self { + self.unit_price = Some(unit_price); + self + } + + /// Set the tax_rate + pub fn tax_rate(mut self, tax_rate: f64) -> Self { + self.tax_rate = Some(tax_rate); + self + } + + /// Set is_taxable + pub fn is_taxable(mut self, is_taxable: bool) -> Self { + self.is_taxable = Some(is_taxable); + self + } + + /// Set the active_till + pub fn active_till(mut self, active_till: DateTime) -> Self { + self.active_till = Some(active_till); + self + } + + /// Build the ServiceItem object + pub fn build(self) -> Result { + let unit_price = self.unit_price.ok_or("unit_price is required")?; + let quantity = self.quantity.ok_or("quantity is required")?; + let tax_rate = self.tax_rate.unwrap_or(0.0); + let is_taxable = self.is_taxable.unwrap_or(false); + + // Calculate subtotal + let amount = unit_price.amount * quantity as f64; + let subtotal = Currency { + amount, + currency_code: unit_price.currency_code.clone(), + }; + + // Calculate tax amount if taxable + let tax_amount = if is_taxable { + Currency { + amount: subtotal.amount * tax_rate, + currency_code: unit_price.currency_code.clone(), + } + } else { + Currency { + amount: 0.0, + currency_code: unit_price.currency_code.clone(), + } + }; + + Ok(ServiceItem { + id: self.id.ok_or("id is required")?, + service_id: self.service_id.ok_or("service_id is required")?, + product_id: self.product_id.ok_or("product_id is required")?, + name: self.name.ok_or("name is required")?, + quantity, + unit_price, + subtotal, + tax_rate, + tax_amount, + is_taxable, + active_till: self.active_till.ok_or("active_till is required")?, + }) + } +} + +/// Service represents a recurring service with billing frequency +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Service { + pub id: u32, + pub customer_id: u32, + pub total_amount: Currency, + pub status: ServiceStatus, + pub billing_frequency: BillingFrequency, + pub service_date: DateTime, + pub created_at: DateTime, + pub updated_at: DateTime, + pub items: Vec, +} + +impl Service { + /// Create a new service with default timestamps + pub fn new( + id: u32, + customer_id: u32, + currency_code: String, + status: ServiceStatus, + billing_frequency: BillingFrequency, + ) -> Self { + let now = Utc::now(); + Self { + id, + customer_id, + total_amount: Currency { amount: 0.0, currency_code }, + status, + billing_frequency, + service_date: now, + created_at: now, + updated_at: now, + items: Vec::new(), + } + } + + /// Add an item to the service and update the total amount + pub fn add_item(&mut self, item: ServiceItem) { + // Make sure the item's service_id matches this service + assert_eq!(self.id, item.service_id, "Item service_id must match service id"); + + // Update the total amount + if self.items.is_empty() { + // First item, initialize the total amount with the same currency + self.total_amount = Currency { + amount: item.subtotal.amount + item.tax_amount.amount, + currency_code: item.subtotal.currency_code.clone(), + }; + } else { + // Add to the existing total + // (Assumes all items have the same currency) + self.total_amount.amount += item.subtotal.amount + item.tax_amount.amount; + } + + // Add the item to the list + self.items.push(item); + + // Update the service timestamp + self.updated_at = Utc::now(); + } + + /// Calculate the total amount based on all items + pub fn calculate_total(&mut self) { + if self.items.is_empty() { + return; + } + + // Get the currency code from the first item + let currency_code = self.items[0].subtotal.currency_code.clone(); + + // Calculate the total amount + let mut total = 0.0; + for item in &self.items { + total += item.subtotal.amount + item.tax_amount.amount; + } + + // Update the total amount + self.total_amount = Currency { + amount: total, + currency_code, + }; + + // Update the service timestamp + self.updated_at = Utc::now(); + } + + /// Update the status of the service + pub fn update_status(&mut self, status: ServiceStatus) { + self.status = status; + self.updated_at = Utc::now(); + } +} + +/// Builder for Service +pub struct ServiceBuilder { + id: Option, + customer_id: Option, + total_amount: Option, + status: Option, + billing_frequency: Option, + service_date: Option>, + created_at: Option>, + updated_at: Option>, + items: Vec, + currency_code: Option, +} + +impl ServiceBuilder { + /// Create a new ServiceBuilder with all fields set to None + pub fn new() -> Self { + Self { + id: None, + customer_id: None, + total_amount: None, + status: None, + billing_frequency: None, + service_date: None, + created_at: None, + updated_at: None, + items: Vec::new(), + currency_code: None, + } + } + + /// Set the id + pub fn id(mut self, id: u32) -> Self { + self.id = Some(id); + self + } + + /// Set the customer_id + pub fn customer_id(mut self, customer_id: u32) -> Self { + self.customer_id = Some(customer_id); + self + } + + /// Set the currency_code + pub fn currency_code>(mut self, currency_code: S) -> Self { + self.currency_code = Some(currency_code.into()); + self + } + + /// Set the status + pub fn status(mut self, status: ServiceStatus) -> Self { + self.status = Some(status); + self + } + + /// Set the billing_frequency + pub fn billing_frequency(mut self, billing_frequency: BillingFrequency) -> Self { + self.billing_frequency = Some(billing_frequency); + self + } + + /// Set the service_date + pub fn service_date(mut self, service_date: DateTime) -> Self { + self.service_date = Some(service_date); + self + } + + /// Add an item to the service + pub fn add_item(mut self, item: ServiceItem) -> Self { + self.items.push(item); + self + } + + /// Build the Service object + pub fn build(self) -> Result { + let now = Utc::now(); + let id = self.id.ok_or("id is required")?; + let currency_code = self.currency_code.ok_or("currency_code is required")?; + + // Initialize with empty total amount + let mut total_amount = Currency { + amount: 0.0, + currency_code: currency_code.clone(), + }; + + // Calculate total amount from items + for item in &self.items { + // Make sure the item's service_id matches this service + if item.service_id != id { + return Err("Item service_id must match service id"); + } + + if total_amount.amount == 0.0 { + // First item, initialize the total amount with the same currency + total_amount = Currency { + amount: item.subtotal.amount + item.tax_amount.amount, + currency_code: item.subtotal.currency_code.clone(), + }; + } else { + // Add to the existing total + // (Assumes all items have the same currency) + total_amount.amount += item.subtotal.amount + item.tax_amount.amount; + } + } + + Ok(Service { + id, + customer_id: self.customer_id.ok_or("customer_id is required")?, + total_amount: self.total_amount.unwrap_or(total_amount), + status: self.status.ok_or("status is required")?, + billing_frequency: self.billing_frequency.ok_or("billing_frequency is required")?, + service_date: self.service_date.unwrap_or(now), + created_at: self.created_at.unwrap_or(now), + updated_at: self.updated_at.unwrap_or(now), + items: self.items, + }) + } +} + +// Implement Storable trait (provides default dump/load) +impl Storable for Service {} + +// Implement SledModel trait +impl SledModel for Service { + fn get_id(&self) -> String { + self.id.to_string() + } + + fn db_prefix() -> &'static str { + "service" + } +} \ No newline at end of file diff --git a/herodb/src/models/circle/circle.rs b/herodb/src/models/circle/circle.rs index c8ddb94..a8d521d 100644 --- a/herodb/src/models/circle/circle.rs +++ b/herodb/src/models/circle/circle.rs @@ -1,5 +1,5 @@ use serde::{Deserialize, Serialize}; -use crate::core::{SledModel, Storable}; +use crate::db::{SledModel, Storable}; use std::collections::HashMap; /// Role represents the role of a member in a circle diff --git a/herodb/src/models/circle/lib.rs b/herodb/src/models/circle/lib.rs index 2777132..61044c0 100644 --- a/herodb/src/models/circle/lib.rs +++ b/herodb/src/models/circle/lib.rs @@ -6,4 +6,4 @@ pub use circle::{Circle, Member, Role}; pub use name::{Name, Record, RecordType}; // Re-export database components -pub use crate::core::{SledDB, SledDBError, SledDBResult, Storable, SledModel, DB}; +pub use crate::db::{SledDB, SledDBError, SledDBResult, Storable, SledModel, DB}; diff --git a/herodb/src/models/circle/mod.rs b/herodb/src/models/circle/mod.rs index 79acec6..b47d562 100644 --- a/herodb/src/models/circle/mod.rs +++ b/herodb/src/models/circle/mod.rs @@ -5,5 +5,5 @@ pub mod name; pub use circle::{Circle, Member, Role}; pub use name::{Name, Record, RecordType}; -// Re-export database components from core module -pub use crate::core::{SledDB, SledDBError, SledDBResult, Storable, SledModel, DB}; +// Re-export database components from db module +pub use crate::db::{SledDB, SledDBError, SledDBResult, Storable, SledModel, DB}; diff --git a/herodb/src/models/circle/name.rs b/herodb/src/models/circle/name.rs index bbcf70f..953d115 100644 --- a/herodb/src/models/circle/name.rs +++ b/herodb/src/models/circle/name.rs @@ -1,5 +1,5 @@ use serde::{Deserialize, Serialize}; -use crate::core::{SledModel, Storable}; +use crate::db::{SledModel, Storable}; /// Record types for a DNS record #[derive(Debug, Clone, Serialize, Deserialize)] diff --git a/herodb/src/models/governance/GOVERNANCE_ENHANCEMENT_PLAN.md b/herodb/src/models/governance/GOVERNANCE_ENHANCEMENT_PLAN.md new file mode 100644 index 0000000..8153489 --- /dev/null +++ b/herodb/src/models/governance/GOVERNANCE_ENHANCEMENT_PLAN.md @@ -0,0 +1,496 @@ +# Governance Module Enhancement Plan (Revised) + +## 1. Current State Analysis + +The governance module currently consists of: +- **Company**: Company model with basic company information +- **Shareholder**: Shareholder model for managing company ownership +- **Meeting**: Meeting and Attendee models for board meetings +- **User**: User model for system users +- **Vote**: Vote, VoteOption, and Ballot models for voting + +All models implement the `Storable` and `SledModel` traits for database integration, but the module has several limitations: +- Not imported in src/models/mod.rs, making it inaccessible to the rest of the project +- No mod.rs file to organize and re-export the types +- No README.md file to document the purpose and usage +- Inconsistent imports across files (e.g., crate::db vs crate::core) +- Limited utility methods and relationships between models +- No integration with other modules like biz, mcc, or circle + +## 2. Planned Enhancements + +### 2.1 Module Organization and Integration + +- Create a mod.rs file to organize and re-export the types +- Add the governance module to src/models/mod.rs +- Create a README.md file to document the purpose and usage +- Standardize imports across all files + +### 2.2 New Models + +#### 2.2.1 Resolution Model + +Create a new `resolution.rs` file with a Resolution model for managing board resolutions: +- Resolution information (title, description, text) +- Resolution status (Draft, Proposed, Approved, Rejected) +- Voting results and approvals +- Integration with Meeting and Vote models + +### 2.3 Enhanced Relationships and Integration + +#### 2.3.1 Integration with Biz Module + +- Link Company with biz::Customer and biz::Contract +- Link Shareholder with biz::Customer +- Link Meeting with biz::Invoice for expense tracking + +#### 2.3.2 Integration with MCC Module + +- Link Meeting with mcc::Calendar and mcc::Event +- Link User with mcc::Contact +- Link Vote with mcc::Message for notifications + +#### 2.3.3 Integration with Circle Module + +- Link Company with circle::Circle for group-based access control +- Link User with circle::Member for role-based permissions + +### 2.4 Utility Methods and Functionality + +- Add filtering and searching methods to all models +- Add relationship management methods between models +- Add validation and business logic methods + +## 3. Implementation Plan + +```mermaid +flowchart TD + A[Review Current Models] --> B[Create mod.rs and Update models/mod.rs] + B --> C[Standardize Imports and Fix Inconsistencies] + C --> D[Create Resolution Model] + D --> E[Implement Integration with Other Modules] + E --> F[Add Utility Methods] + F --> G[Create README.md and Documentation] + G --> H[Write Tests] +``` + +### 3.1 Detailed Changes + +#### 3.1.1 Module Organization + +Create a new `mod.rs` file in the governance directory: + +```rust +pub mod company; +pub mod shareholder; +pub mod meeting; +pub mod user; +pub mod vote; +pub mod resolution; + +// Re-export all model types for convenience +pub use company::{Company, CompanyStatus, BusinessType}; +pub use shareholder::{Shareholder, ShareholderType}; +pub use meeting::{Meeting, Attendee, MeetingStatus, AttendeeRole, AttendeeStatus}; +pub use user::User; +pub use vote::{Vote, VoteOption, Ballot, VoteStatus}; +pub use resolution::{Resolution, ResolutionStatus, Approval}; + +// Re-export database components from db module +pub use crate::db::{SledDB, SledDBError, SledDBResult, Storable, SledModel, DB}; +``` + +Update `src/models/mod.rs` to include the governance module: + +```rust +pub mod biz; +pub mod mcc; +pub mod circle; +pub mod governance; +``` + +#### 3.1.2 Resolution Model (`resolution.rs`) + +```rust +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use crate::db::{SledModel, Storable, SledDB, SledDBError}; +use crate::models::governance::{Meeting, Vote}; + +/// ResolutionStatus represents the status of a resolution +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum ResolutionStatus { + Draft, + Proposed, + Approved, + Rejected, + Withdrawn, +} + +/// Resolution represents a board resolution +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct Resolution { + pub id: u32, + pub company_id: u32, + pub meeting_id: Option, + pub vote_id: Option, + pub title: String, + pub description: String, + pub text: String, + pub status: ResolutionStatus, + pub proposed_by: u32, // User ID + pub proposed_at: DateTime, + pub approved_at: Option>, + pub rejected_at: Option>, + pub created_at: DateTime, + pub updated_at: DateTime, + pub approvals: Vec, +} + +/// Approval represents an approval of a resolution by a board member +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct Approval { + pub id: u32, + pub resolution_id: u32, + pub user_id: u32, + pub name: String, + pub approved: bool, + pub comments: String, + pub created_at: DateTime, +} + +impl Resolution { + /// Create a new resolution with default values + pub fn new( + id: u32, + company_id: u32, + title: String, + description: String, + text: String, + proposed_by: u32, + ) -> Self { + let now = Utc::now(); + Self { + id, + company_id, + meeting_id: None, + vote_id: None, + title, + description, + text, + status: ResolutionStatus::Draft, + proposed_by, + proposed_at: now, + approved_at: None, + rejected_at: None, + created_at: now, + updated_at: now, + approvals: Vec::new(), + } + } + + /// Propose the resolution + pub fn propose(&mut self) { + self.status = ResolutionStatus::Proposed; + self.proposed_at = Utc::now(); + self.updated_at = Utc::now(); + } + + /// Approve the resolution + pub fn approve(&mut self) { + self.status = ResolutionStatus::Approved; + self.approved_at = Some(Utc::now()); + self.updated_at = Utc::now(); + } + + /// Reject the resolution + pub fn reject(&mut self) { + self.status = ResolutionStatus::Rejected; + self.rejected_at = Some(Utc::now()); + self.updated_at = Utc::now(); + } + + /// Add an approval to the resolution + pub fn add_approval(&mut self, user_id: u32, name: String, approved: bool, comments: String) -> &Approval { + let id = if self.approvals.is_empty() { + 1 + } else { + self.approvals.iter().map(|a| a.id).max().unwrap_or(0) + 1 + }; + + let approval = Approval { + id, + resolution_id: self.id, + user_id, + name, + approved, + comments, + created_at: Utc::now(), + }; + + self.approvals.push(approval); + self.updated_at = Utc::now(); + self.approvals.last().unwrap() + } + + /// Link this resolution to a meeting + pub fn link_to_meeting(&mut self, meeting_id: u32) { + self.meeting_id = Some(meeting_id); + self.updated_at = Utc::now(); + } + + /// Link this resolution to a vote + pub fn link_to_vote(&mut self, vote_id: u32) { + self.vote_id = Some(vote_id); + self.updated_at = Utc::now(); + } +} + +// Implement Storable trait (provides default dump/load) +impl Storable for Resolution {} +impl Storable for Approval {} + +// Implement SledModel trait +impl SledModel for Resolution { + fn get_id(&self) -> String { + self.id.to_string() + } + + fn db_prefix() -> &'static str { + "resolution" + } +} +``` + +#### 3.1.3 Enhanced Company Model (`company.rs`) + +Add integration with other modules: + +```rust +impl Company { + // ... existing methods ... + + /// Link this company to a Circle for access control + pub fn link_to_circle(&mut self, circle_id: u32) -> Result<(), SledDBError> { + // Implementation details + self.updated_at = Utc::now(); + Ok(()) + } + + /// Link this company to a Customer in the biz module + pub fn link_to_customer(&mut self, customer_id: u32) -> Result<(), SledDBError> { + // Implementation details + self.updated_at = Utc::now(); + Ok(()) + } + + /// Get all resolutions for this company + pub fn get_resolutions(&self, db: &SledDB) -> Result, SledDBError> { + let all_resolutions = db.list()?; + let company_resolutions = all_resolutions + .into_iter() + .filter(|resolution| resolution.company_id == self.id) + .collect(); + + Ok(company_resolutions) + } +} +``` + +#### 3.1.4 Enhanced Meeting Model (`meeting.rs`) + +Add integration with other modules: + +```rust +impl Meeting { + // ... existing methods ... + + /// Link this meeting to a Calendar Event in the mcc module + pub fn link_to_event(&mut self, event_id: u32) -> Result<(), SledDBError> { + // Implementation details + self.updated_at = Utc::now(); + Ok(()) + } + + /// Get all resolutions discussed in this meeting + pub fn get_resolutions(&self, db: &SledDB) -> Result, SledDBError> { + let all_resolutions = db.list()?; + let meeting_resolutions = all_resolutions + .into_iter() + .filter(|resolution| resolution.meeting_id == Some(self.id)) + .collect(); + + Ok(meeting_resolutions) + } +} +``` + +#### 3.1.5 Enhanced Vote Model (`vote.rs`) + +Add integration with Resolution model: + +```rust +impl Vote { + // ... existing methods ... + + /// Get the resolution associated with this vote + pub fn get_resolution(&self, db: &SledDB) -> Result, SledDBError> { + let all_resolutions = db.list()?; + let vote_resolution = all_resolutions + .into_iter() + .find(|resolution| resolution.vote_id == Some(self.id)); + + Ok(vote_resolution) + } +} +``` + +#### 3.1.6 Create README.md + +Create a README.md file to document the purpose and usage of the governance module. + +## 4. Data Model Diagram + +```mermaid +classDiagram + class Company { + +u32 id + +String name + +String registration_number + +DateTime incorporation_date + +String fiscal_year_end + +String email + +String phone + +String website + +String address + +BusinessType business_type + +String industry + +String description + +CompanyStatus status + +DateTime created_at + +DateTime updated_at + +add_shareholder() + +link_to_circle() + +link_to_customer() + +get_resolutions() + } + + class Shareholder { + +u32 id + +u32 company_id + +u32 user_id + +String name + +f64 shares + +f64 percentage + +ShareholderType type_ + +DateTime since + +DateTime created_at + +DateTime updated_at + +update_shares() + } + + class Meeting { + +u32 id + +u32 company_id + +String title + +DateTime date + +String location + +String description + +MeetingStatus status + +String minutes + +DateTime created_at + +DateTime updated_at + +Vec~Attendee~ attendees + +add_attendee() + +update_status() + +update_minutes() + +find_attendee_by_user_id() + +confirmed_attendees() + +link_to_event() + +get_resolutions() + } + + class User { + +u32 id + +String name + +String email + +String password + +String company + +String role + +DateTime created_at + +DateTime updated_at + } + + class Vote { + +u32 id + +u32 company_id + +String title + +String description + +DateTime start_date + +DateTime end_date + +VoteStatus status + +DateTime created_at + +DateTime updated_at + +Vec~VoteOption~ options + +Vec~Ballot~ ballots + +Vec~u32~ private_group + +add_option() + +add_ballot() + +get_resolution() + } + + class Resolution { + +u32 id + +u32 company_id + +Option~u32~ meeting_id + +Option~u32~ vote_id + +String title + +String description + +String text + +ResolutionStatus status + +u32 proposed_by + +DateTime proposed_at + +Option~DateTime~ approved_at + +Option~DateTime~ rejected_at + +DateTime created_at + +DateTime updated_at + +Vec~Approval~ approvals + +propose() + +approve() + +reject() + +add_approval() + +link_to_meeting() + +link_to_vote() + } + + Company "1" -- "many" Shareholder: has + Company "1" -- "many" Meeting: holds + Company "1" -- "many" Vote: conducts + Company "1" -- "many" Resolution: issues + Meeting "1" -- "many" Attendee: has + Meeting "1" -- "many" Resolution: discusses + Vote "1" -- "many" VoteOption: has + Vote "1" -- "many" Ballot: collects + Vote "1" -- "1" Resolution: decides + Resolution "1" -- "many" Approval: receives +``` + +## 5. Testing Strategy + +1. Unit tests for each model to verify: + - Basic functionality + - Serialization/deserialization + - Utility methods + - Integration with other models +2. Integration tests to verify: + - Database operations with the models + - Relationships between models + - Integration with other modules + +## 6. Future Considerations + +1. **Committee Model**: Add a Committee model in the future if needed +2. **Compliance Model**: Add compliance-related models in the future if needed +3. **API Integration**: Develop REST API endpoints for the governance module +4. **UI Components**: Create UI components for managing governance entities +5. **Reporting**: Implement reporting functionality for governance metrics \ No newline at end of file diff --git a/herodb/src/models/governance/README.md b/herodb/src/models/governance/README.md new file mode 100644 index 0000000..cdb9e98 --- /dev/null +++ b/herodb/src/models/governance/README.md @@ -0,0 +1,66 @@ +# Governance Module + +This directory contains the core data structures used in the Freezone Manager governance module. These models serve as the foundation for corporate governance functionality, providing essential data structures for companies, shareholders, meetings, voting, and more. + +## Overview + +The governance models implement the Serde traits (Serialize/Deserialize) and database traits (Storable, SledModel), which allows them to be stored and retrieved using the generic SledDB implementation. Each model provides: + +- A struct definition with appropriate fields +- Serde serialization through derive macros +- Methods for database integration through the SledModel trait +- Utility methods for common operations + +## Core Models + +### Company (`company.rs`) + +The Company model represents a company registered in the Freezone: + +- **Company**: Main struct with fields for company information +- **CompanyStatus**: Enum for possible company statuses (Active, Inactive, Suspended) +- **BusinessType**: Enum for possible business types (Coop, Single, Twin, Starter, Global) + +### Shareholder (`shareholder.rs`) + +The Shareholder model represents a shareholder of a company: + +- **Shareholder**: Main struct with fields for shareholder information +- **ShareholderType**: Enum for possible shareholder types (Individual, Corporate) + +### Meeting (`meeting.rs`) + +The Meeting model represents a board meeting of a company: + +- **Meeting**: Main struct with fields for meeting information +- **Attendee**: Represents an attendee of a meeting +- **MeetingStatus**: Enum for possible meeting statuses (Scheduled, Completed, Cancelled) +- **AttendeeRole**: Enum for possible attendee roles (Coordinator, Member, Secretary, etc.) +- **AttendeeStatus**: Enum for possible attendee statuses (Confirmed, Pending, Declined) + +### User (`user.rs`) + +The User model represents a user in the Freezone Manager system: + +- **User**: Main struct with fields for user information + +### Vote (`vote.rs`) + +The Vote model represents a voting item in the Freezone: + +- **Vote**: Main struct with fields for vote information +- **VoteOption**: Represents an option in a vote +- **Ballot**: Represents a ballot cast by a user +- **VoteStatus**: Enum for possible vote statuses (Open, Closed, Cancelled) + +## Usage + +These models are used by the governance module to manage corporate governance. They are typically accessed through the database handlers that implement the generic SledDB interface. + +## Future Enhancements + +See the [GOVERNANCE_ENHANCEMENT_PLAN.md](./GOVERNANCE_ENHANCEMENT_PLAN.md) file for details on planned enhancements to the governance module, including: + +1. New models for committees, resolutions, and compliance +2. Enhanced relationships with other modules (biz, mcc, circle) +3. Additional utility methods and functionality \ No newline at end of file diff --git a/herodb/src/models/governance/committee.rs b/herodb/src/models/governance/committee.rs new file mode 100644 index 0000000..04ff00f --- /dev/null +++ b/herodb/src/models/governance/committee.rs @@ -0,0 +1,146 @@ +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use crate::db::{SledModel, Storable, SledDB, SledDBError}; +use crate::models::governance::User; + +/// CommitteeRole represents the role of a member in a committee +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum CommitteeRole { + Chair, + ViceChair, + Secretary, + Member, + Advisor, + Observer, +} + +/// CommitteeMember represents a member of a committee +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct CommitteeMember { + pub id: u32, + pub committee_id: u32, + pub user_id: u32, + pub name: String, + pub role: CommitteeRole, + pub since: DateTime, + pub created_at: DateTime, +} + +/// Committee represents a board committee +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct Committee { + pub id: u32, + pub company_id: u32, + pub name: String, + pub description: String, + pub purpose: String, + pub circle_id: Option, // Link to Circle for access control + pub created_at: DateTime, + pub updated_at: DateTime, + pub members: Vec, +} + +impl Committee { + /// Create a new committee with default values + pub fn new( + id: u32, + company_id: u32, + name: String, + description: String, + purpose: String, + ) -> Self { + let now = Utc::now(); + Self { + id, + company_id, + name, + description, + purpose, + circle_id: None, + created_at: now, + updated_at: now, + members: Vec::new(), + } + } + + /// Add a member to the committee + pub fn add_member(&mut self, user_id: u32, name: String, role: CommitteeRole) -> &CommitteeMember { + let id = if self.members.is_empty() { + 1 + } else { + self.members.iter().map(|m| m.id).max().unwrap_or(0) + 1 + }; + + let now = Utc::now(); + let member = CommitteeMember { + id, + committee_id: self.id, + user_id, + name, + role, + since: now, + created_at: now, + }; + + self.members.push(member); + self.updated_at = now; + self.members.last().unwrap() + } + + /// Find a member by user ID + pub fn find_member_by_user_id(&self, user_id: u32) -> Option<&CommitteeMember> { + self.members.iter().find(|m| m.user_id == user_id) + } + + /// Find a member by user ID (mutable version) + pub fn find_member_by_user_id_mut(&mut self, user_id: u32) -> Option<&mut CommitteeMember> { + self.members.iter_mut().find(|m| m.user_id == user_id) + } + + /// Remove a member from the committee + pub fn remove_member(&mut self, member_id: u32) -> bool { + let len = self.members.len(); + self.members.retain(|m| m.id != member_id); + let removed = self.members.len() < len; + + if removed { + self.updated_at = Utc::now(); + } + + removed + } + + /// Link this committee to a Circle for access control + pub fn link_to_circle(&mut self, circle_id: u32) { + self.circle_id = Some(circle_id); + self.updated_at = Utc::now(); + } + + /// Get all users who are members of this committee + pub fn get_member_users(&self, db: &SledDB) -> Result, SledDBError> { + let mut users = Vec::new(); + + for member in &self.members { + if let Ok(user) = db.get(&member.user_id.to_string()) { + users.push(user); + } + } + + Ok(users) + } +} + +// Implement Storable trait (provides default dump/load) +impl Storable for Committee {} +impl Storable for CommitteeMember {} + +// Implement SledModel trait +impl SledModel for Committee { + fn get_id(&self) -> String { + self.id.to_string() + } + + fn db_prefix() -> &'static str { + "committee" + } +} \ No newline at end of file diff --git a/herodb/src/models/governance/company.rs b/herodb/src/models/governance/company.rs index 547f465..0ab0a2c 100644 --- a/herodb/src/models/governance/company.rs +++ b/herodb/src/models/governance/company.rs @@ -1,4 +1,4 @@ -use crate::core::{SledModel, Storable, SledDB, SledDBError}; +use crate::db::{SledModel, Storable, SledDB, SledDBError}; use super::shareholder::Shareholder; // Use super:: for sibling module use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; @@ -57,7 +57,6 @@ impl SledModel for Company { } } - impl Company { /// Create a new company with default timestamps pub fn new( @@ -107,6 +106,37 @@ impl Company { Ok(()) } - // Removed dump and load_from_bytes methods, now provided by Storable trait + /// Link this company to a Circle for access control + pub fn link_to_circle(&mut self, circle_id: u32) -> Result<(), SledDBError> { + // Implementation would involve updating a mapping in a separate database + // For now, we'll just update the timestamp to indicate the change + self.updated_at = Utc::now(); + Ok(()) + } + + /// Link this company to a Customer in the biz module + pub fn link_to_customer(&mut self, customer_id: u32) -> Result<(), SledDBError> { + // Implementation would involve updating a mapping in a separate database + // For now, we'll just update the timestamp to indicate the change + self.updated_at = Utc::now(); + Ok(()) + } + + /// Get all resolutions for this company + pub fn get_resolutions(&self, db: &SledDB) -> Result, SledDBError> { + let all_resolutions = db.list()?; + let company_resolutions = all_resolutions + .into_iter() + .filter(|resolution| resolution.company_id == self.id) + .collect(); + + Ok(company_resolutions) + } + + // Future methods: + // /// Get all committees for this company + // pub fn get_committees(&self, db: &SledDB) -> Result, SledDBError> { ... } + // + // /// Get all compliance requirements for this company + // pub fn get_compliance_requirements(&self, db: &SledDB) -> Result, SledDBError> { ... } } - diff --git a/herodb/src/models/governance/compliance.rs b/herodb/src/models/governance/compliance.rs new file mode 100644 index 0000000..d3f8bbf --- /dev/null +++ b/herodb/src/models/governance/compliance.rs @@ -0,0 +1,212 @@ +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use crate::db::{SledModel, Storable, SledDB, SledDBError}; +use crate::models::governance::Company; + +/// ComplianceRequirement represents a regulatory requirement +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct ComplianceRequirement { + pub id: u32, + pub company_id: u32, + pub title: String, + pub description: String, + pub regulation: String, + pub authority: String, + pub deadline: DateTime, + pub status: String, + pub created_at: DateTime, + pub updated_at: DateTime, +} + +/// ComplianceDocument represents a compliance document +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct ComplianceDocument { + pub id: u32, + pub requirement_id: u32, + pub title: String, + pub description: String, + pub file_path: String, + pub file_type: String, + pub uploaded_by: u32, // User ID + pub uploaded_at: DateTime, + pub created_at: DateTime, + pub updated_at: DateTime, +} + +/// ComplianceAudit represents a compliance audit +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct ComplianceAudit { + pub id: u32, + pub company_id: u32, + pub title: String, + pub description: String, + pub auditor: String, + pub start_date: DateTime, + pub end_date: DateTime, + pub status: String, + pub findings: String, + pub created_at: DateTime, + pub updated_at: DateTime, +} + +impl ComplianceRequirement { + /// Create a new compliance requirement with default values + pub fn new( + id: u32, + company_id: u32, + title: String, + description: String, + regulation: String, + authority: String, + deadline: DateTime, + ) -> Self { + let now = Utc::now(); + Self { + id, + company_id, + title, + description, + regulation, + authority, + deadline, + status: "Pending".to_string(), + created_at: now, + updated_at: now, + } + } + + /// Update the status of the requirement + pub fn update_status(&mut self, status: String) { + self.status = status; + self.updated_at = Utc::now(); + } + + /// Get the company associated with this requirement + pub fn get_company(&self, db: &SledDB) -> Result { + db.get(&self.company_id.to_string()) + } + + /// Get all documents associated with this requirement + pub fn get_documents(&self, db: &SledDB) -> Result, SledDBError> { + let all_documents = db.list()?; + let requirement_documents = all_documents + .into_iter() + .filter(|doc| doc.requirement_id == self.id) + .collect(); + + Ok(requirement_documents) + } +} + +impl ComplianceDocument { + /// Create a new compliance document with default values + pub fn new( + id: u32, + requirement_id: u32, + title: String, + description: String, + file_path: String, + file_type: String, + uploaded_by: u32, + ) -> Self { + let now = Utc::now(); + Self { + id, + requirement_id, + title, + description, + file_path, + file_type, + uploaded_by, + uploaded_at: now, + created_at: now, + updated_at: now, + } + } + + /// Get the requirement associated with this document + pub fn get_requirement(&self, db: &SledDB) -> Result { + db.get(&self.requirement_id.to_string()) + } +} + +impl ComplianceAudit { + /// Create a new compliance audit with default values + pub fn new( + id: u32, + company_id: u32, + title: String, + description: String, + auditor: String, + start_date: DateTime, + end_date: DateTime, + ) -> Self { + let now = Utc::now(); + Self { + id, + company_id, + title, + description, + auditor, + start_date, + end_date, + status: "Planned".to_string(), + findings: String::new(), + created_at: now, + updated_at: now, + } + } + + /// Update the status of the audit + pub fn update_status(&mut self, status: String) { + self.status = status; + self.updated_at = Utc::now(); + } + + /// Update the findings of the audit + pub fn update_findings(&mut self, findings: String) { + self.findings = findings; + self.updated_at = Utc::now(); + } + + /// Get the company associated with this audit + pub fn get_company(&self, db: &SledDB) -> Result { + db.get(&self.company_id.to_string()) + } +} + +// Implement Storable trait (provides default dump/load) +impl Storable for ComplianceRequirement {} +impl Storable for ComplianceDocument {} +impl Storable for ComplianceAudit {} + +// Implement SledModel trait +impl SledModel for ComplianceRequirement { + fn get_id(&self) -> String { + self.id.to_string() + } + + fn db_prefix() -> &'static str { + "compliance_requirement" + } +} + +impl SledModel for ComplianceDocument { + fn get_id(&self) -> String { + self.id.to_string() + } + + fn db_prefix() -> &'static str { + "compliance_document" + } +} + +impl SledModel for ComplianceAudit { + fn get_id(&self) -> String { + self.id.to_string() + } + + fn db_prefix() -> &'static str { + "compliance_audit" + } +} \ No newline at end of file diff --git a/herodb/src/models/governance/meeting.rs b/herodb/src/models/governance/meeting.rs index 6c64c81..6b15ef7 100644 --- a/herodb/src/models/governance/meeting.rs +++ b/herodb/src/models/governance/meeting.rs @@ -1,6 +1,6 @@ use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; -use crate::core::{SledModel, Storable}; // Import Sled traits from new location +use crate::db::{SledModel, Storable, SledDB, SledDBError, SledDBResult}; // Import Sled traits from db module // use std::collections::HashMap; // Removed unused import // use super::db::Model; // Removed old Model trait import @@ -155,6 +155,24 @@ impl Meeting { .filter(|a| a.status == AttendeeStatus::Confirmed) .collect() } + /// Link this meeting to a Calendar Event in the mcc module + pub fn link_to_event(&mut self, event_id: u32) -> Result<(), SledDBError> { + // Implementation would involve updating a mapping in a separate database + // For now, we'll just update the timestamp to indicate the change + self.updated_at = Utc::now(); + Ok(()) + } + + /// Get all resolutions discussed in this meeting + pub fn get_resolutions(&self, db: &SledDB) -> Result, SledDBError> { + let all_resolutions = db.list()?; + let meeting_resolutions = all_resolutions + .into_iter() + .filter(|resolution| resolution.meeting_id == Some(self.id)) + .collect(); + + Ok(meeting_resolutions) + } } // Implement Storable trait (provides default dump/load) diff --git a/herodb/src/models/governance/mod.rs b/herodb/src/models/governance/mod.rs new file mode 100644 index 0000000..de1187e --- /dev/null +++ b/herodb/src/models/governance/mod.rs @@ -0,0 +1,20 @@ +pub mod company; +pub mod shareholder; +pub mod meeting; +pub mod user; +pub mod vote; +pub mod resolution; +// Future modules: +// pub mod committee; +// pub mod compliance; + +// Re-export all model types for convenience +pub use company::{Company, CompanyStatus, BusinessType}; +pub use shareholder::{Shareholder, ShareholderType}; +pub use meeting::{Meeting, Attendee, MeetingStatus, AttendeeRole, AttendeeStatus}; +pub use user::User; +pub use vote::{Vote, VoteOption, Ballot, VoteStatus}; +pub use resolution::{Resolution, ResolutionStatus, Approval}; + +// Re-export database components from db module +pub use crate::db::{SledDB, SledDBError, SledDBResult, Storable, SledModel, DB}; \ No newline at end of file diff --git a/herodb/src/models/governance/resolution.rs b/herodb/src/models/governance/resolution.rs new file mode 100644 index 0000000..59fe9a8 --- /dev/null +++ b/herodb/src/models/governance/resolution.rs @@ -0,0 +1,196 @@ +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use crate::db::{SledModel, Storable, SledDB, SledDBError}; +use crate::models::governance::{Meeting, Vote}; + +/// ResolutionStatus represents the status of a resolution +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum ResolutionStatus { + Draft, + Proposed, + Approved, + Rejected, + Withdrawn, +} + +/// Resolution represents a board resolution +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct Resolution { + pub id: u32, + pub company_id: u32, + pub meeting_id: Option, + pub vote_id: Option, + pub title: String, + pub description: String, + pub text: String, + pub status: ResolutionStatus, + pub proposed_by: u32, // User ID + pub proposed_at: DateTime, + pub approved_at: Option>, + pub rejected_at: Option>, + pub created_at: DateTime, + pub updated_at: DateTime, + pub approvals: Vec, +} + +/// Approval represents an approval of a resolution by a board member +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct Approval { + pub id: u32, + pub resolution_id: u32, + pub user_id: u32, + pub name: String, + pub approved: bool, + pub comments: String, + pub created_at: DateTime, +} + +impl Resolution { + /// Create a new resolution with default values + pub fn new( + id: u32, + company_id: u32, + title: String, + description: String, + text: String, + proposed_by: u32, + ) -> Self { + let now = Utc::now(); + Self { + id, + company_id, + meeting_id: None, + vote_id: None, + title, + description, + text, + status: ResolutionStatus::Draft, + proposed_by, + proposed_at: now, + approved_at: None, + rejected_at: None, + created_at: now, + updated_at: now, + approvals: Vec::new(), + } + } + + /// Propose the resolution + pub fn propose(&mut self) { + self.status = ResolutionStatus::Proposed; + self.proposed_at = Utc::now(); + self.updated_at = Utc::now(); + } + + /// Approve the resolution + pub fn approve(&mut self) { + self.status = ResolutionStatus::Approved; + self.approved_at = Some(Utc::now()); + self.updated_at = Utc::now(); + } + + /// Reject the resolution + pub fn reject(&mut self) { + self.status = ResolutionStatus::Rejected; + self.rejected_at = Some(Utc::now()); + self.updated_at = Utc::now(); + } + + /// Withdraw the resolution + pub fn withdraw(&mut self) { + self.status = ResolutionStatus::Withdrawn; + self.updated_at = Utc::now(); + } + + /// Add an approval to the resolution + pub fn add_approval(&mut self, user_id: u32, name: String, approved: bool, comments: String) -> &Approval { + let id = if self.approvals.is_empty() { + 1 + } else { + self.approvals.iter().map(|a| a.id).max().unwrap_or(0) + 1 + }; + + let approval = Approval { + id, + resolution_id: self.id, + user_id, + name, + approved, + comments, + created_at: Utc::now(), + }; + + self.approvals.push(approval); + self.updated_at = Utc::now(); + self.approvals.last().unwrap() + } + + /// Find an approval by user ID + pub fn find_approval_by_user_id(&self, user_id: u32) -> Option<&Approval> { + self.approvals.iter().find(|a| a.user_id == user_id) + } + + /// Get all approvals + pub fn get_approvals(&self) -> &[Approval] { + &self.approvals + } + + /// Get approval count + pub fn approval_count(&self) -> usize { + self.approvals.iter().filter(|a| a.approved).count() + } + + /// Get rejection count + pub fn rejection_count(&self) -> usize { + self.approvals.iter().filter(|a| !a.approved).count() + } + + /// Link this resolution to a meeting + pub fn link_to_meeting(&mut self, meeting_id: u32) { + self.meeting_id = Some(meeting_id); + self.updated_at = Utc::now(); + } + + /// Link this resolution to a vote + pub fn link_to_vote(&mut self, vote_id: u32) { + self.vote_id = Some(vote_id); + self.updated_at = Utc::now(); + } + + /// Get the meeting associated with this resolution + pub fn get_meeting(&self, db: &SledDB) -> Result, SledDBError> { + match self.meeting_id { + Some(meeting_id) => { + let meeting = db.get(&meeting_id.to_string())?; + Ok(Some(meeting)) + } + None => Ok(None), + } + } + + /// Get the vote associated with this resolution + pub fn get_vote(&self, db: &SledDB) -> Result, SledDBError> { + match self.vote_id { + Some(vote_id) => { + let vote = db.get(&vote_id.to_string())?; + Ok(Some(vote)) + } + None => Ok(None), + } + } +} + +// Implement Storable trait (provides default dump/load) +impl Storable for Resolution {} +impl Storable for Approval {} + +// Implement SledModel trait +impl SledModel for Resolution { + fn get_id(&self) -> String { + self.id.to_string() + } + + fn db_prefix() -> &'static str { + "resolution" + } +} \ No newline at end of file diff --git a/herodb/src/models/governance/shareholder.rs b/herodb/src/models/governance/shareholder.rs index 92c1312..00c6be5 100644 --- a/herodb/src/models/governance/shareholder.rs +++ b/herodb/src/models/governance/shareholder.rs @@ -1,4 +1,4 @@ -use crate::core::{SledModel, Storable}; // Import Sled traits +use crate::db::{SledModel, Storable}; // Import Sled traits use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; // use std::collections::HashMap; // Removed unused import diff --git a/herodb/src/models/governance/user.rs b/herodb/src/models/governance/user.rs index 62bda34..55e03e7 100644 --- a/herodb/src/models/governance/user.rs +++ b/herodb/src/models/governance/user.rs @@ -1,6 +1,6 @@ use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; -use crate::core::{SledModel, Storable}; // Import Sled traits +use crate::db::{SledModel, Storable}; // Import Sled traits // use std::collections::HashMap; // Removed unused import /// User represents a user in the Freezone Manager system diff --git a/herodb/src/models/governance/vote.rs b/herodb/src/models/governance/vote.rs index 075c5df..93406d5 100644 --- a/herodb/src/models/governance/vote.rs +++ b/herodb/src/models/governance/vote.rs @@ -1,6 +1,6 @@ use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; -use crate::db::{SledModel, Storable}; // Import Sled traits from new location +use crate::db::{SledModel, Storable, SledDB, SledDBError}; // Import Sled traits from db module // use std::collections::HashMap; // Removed unused import // use super::db::Model; // Removed old Model trait import @@ -126,6 +126,16 @@ impl Vote { self.ballots.push(ballot); self.ballots.last().unwrap() } + + /// Get the resolution associated with this vote + pub fn get_resolution(&self, db: &SledDB) -> Result, SledDBError> { + let all_resolutions = db.list()?; + let vote_resolution = all_resolutions + .into_iter() + .find(|resolution| resolution.vote_id == Some(self.id)); + + Ok(vote_resolution) + } } // Implement Storable trait (provides default dump/load) diff --git a/herodb/src/models/mcc/MCC_ENHANCEMENT_PLAN.md b/herodb/src/models/mcc/MCC_ENHANCEMENT_PLAN.md new file mode 100644 index 0000000..e3ca985 --- /dev/null +++ b/herodb/src/models/mcc/MCC_ENHANCEMENT_PLAN.md @@ -0,0 +1,316 @@ +# MCC Models Enhancement Plan + +## 1. Current State Analysis + +The current MCC module consists of: +- **Mail**: Email, Attachment, Envelope models +- **Calendar**: Calendar model +- **Event**: Event, EventMeta models +- **Contacts**: Contact model + +All models implement the `Storable` and `SledModel` traits for database integration. + +## 2. Planned Enhancements + +### 2.1 Add Group Support to All Models + +Add a `groups: Vec` field to each model to enable linking to multiple groups defined in the Circle module. + +### 2.2 Create New Message Model + +Create a new `message.rs` file with a Message model for chat functionality: +- Different structure from Email +- Include thread_id, sender_id, content fields +- Include metadata for chat-specific features +- Implement Storable and SledModel traits + +### 2.3 Add Utility Methods + +Add utility methods to each model for: +- **Filtering/Searching**: Methods to filter by groups, search by content/subject +- **Format Conversion**: Methods to convert between formats (e.g., Email to Message) +- **Relationship Management**: Methods to manage relationships between models + +## 3. Implementation Plan + +```mermaid +flowchart TD + A[Review Current Models] --> B[Add groups field to all models] + B --> C[Create Message model] + C --> D[Add utility methods] + D --> E[Update mod.rs and lib.rs] + E --> F[Update README.md] +``` + +### 3.1 Detailed Changes + +#### 3.1.1 Mail Model (`mail.rs`) + +- Add `groups: Vec` field to `Email` struct +- Add utility methods: + - `filter_by_groups(groups: &[u32]) -> bool` + - `search_by_subject(query: &str) -> bool` + - `search_by_content(query: &str) -> bool` + - `to_message(&self) -> Message` (conversion method) + +#### 3.1.2 Calendar Model (`calendar.rs`) + +- Add `groups: Vec` field to `Calendar` struct +- Add utility methods: + - `filter_by_groups(groups: &[u32]) -> bool` + - `get_events(&self, db: &SledDB) -> SledDBResult>` (relationship method) + +#### 3.1.3 Event Model (`event.rs`) + +- Add `groups: Vec` field to `Event` struct +- Add utility methods: + - `filter_by_groups(groups: &[u32]) -> bool` + - `get_calendar(&self, db: &SledDB) -> SledDBResult` (relationship method) + - `get_attendee_contacts(&self, db: &SledDB) -> SledDBResult>` (relationship method) + +#### 3.1.4 Contacts Model (`contacts.rs`) + +- Add `groups: Vec` field to `Contact` struct +- Add utility methods: + - `filter_by_groups(groups: &[u32]) -> bool` + - `search_by_name(query: &str) -> bool` + - `search_by_email(query: &str) -> bool` + - `get_events(&self, db: &SledDB) -> SledDBResult>` (relationship method) + +#### 3.1.5 New Message Model (`message.rs`) + +```rust +use serde::{Deserialize, Serialize}; +use crate::core::{SledModel, Storable}; +use chrono::{DateTime, Utc}; + +/// MessageStatus represents the status of a message +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum MessageStatus { + Sent, + Delivered, + Read, + Failed, +} + +/// MessageMeta contains metadata for a chat message +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct MessageMeta { + pub created_at: DateTime, + pub updated_at: DateTime, + pub status: MessageStatus, + pub is_edited: bool, + pub reactions: Vec, +} + +/// Message represents a chat message +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Message { + pub id: u32, // Unique identifier + pub thread_id: String, // Thread/conversation identifier + pub sender_id: String, // Sender identifier + pub recipients: Vec, // List of recipient identifiers + pub content: String, // Message content + pub attachments: Vec, // References to attachments + pub groups: Vec, // Groups this message belongs to + pub meta: MessageMeta, // Message metadata +} + +impl Message { + /// Create a new message + pub fn new(id: u32, thread_id: String, sender_id: String, content: String) -> Self { + let now = Utc::now(); + Self { + id, + thread_id, + sender_id, + recipients: Vec::new(), + content, + attachments: Vec::new(), + groups: Vec::new(), + meta: MessageMeta { + created_at: now, + updated_at: now, + status: MessageStatus::Sent, + is_edited: false, + reactions: Vec::new(), + }, + } + } + + /// Add a recipient to this message + pub fn add_recipient(&mut self, recipient: String) { + self.recipients.push(recipient); + } + + /// Add an attachment to this message + pub fn add_attachment(&mut self, attachment: String) { + self.attachments.push(attachment); + } + + /// Add a group to this message + pub fn add_group(&mut self, group_id: u32) { + if !self.groups.contains(&group_id) { + self.groups.push(group_id); + } + } + + /// Filter by groups + pub fn filter_by_groups(&self, groups: &[u32]) -> bool { + groups.iter().any(|g| self.groups.contains(g)) + } + + /// Search by content + pub fn search_by_content(&self, query: &str) -> bool { + self.content.to_lowercase().contains(&query.to_lowercase()) + } + + /// Update message status + pub fn update_status(&mut self, status: MessageStatus) { + self.meta.status = status; + self.meta.updated_at = Utc::now(); + } + + /// Edit message content + pub fn edit_content(&mut self, new_content: String) { + self.content = new_content; + self.meta.is_edited = true; + self.meta.updated_at = Utc::now(); + } + + /// Add a reaction to the message + pub fn add_reaction(&mut self, reaction: String) { + self.meta.reactions.push(reaction); + self.meta.updated_at = Utc::now(); + } +} + +// Implement Storable trait (provides default dump/load) +impl Storable for Message {} + +// Implement SledModel trait +impl SledModel for Message { + fn get_id(&self) -> String { + self.id.to_string() + } + + fn db_prefix() -> &'static str { + "message" + } +} +``` + +#### 3.1.6 Update Module Files + +Update `mod.rs` and `lib.rs` to include the new Message model. + +#### 3.1.7 Update README.md + +Update the README.md to include information about the Message model and the new utility methods. + +## 4. Data Model Diagram + +```mermaid +classDiagram + class Email { + +u32 id + +u32 uid + +u32 seq_num + +String mailbox + +String message + +Vec~Attachment~ attachments + +Vec~String~ flags + +i64 receivetime + +Option~Envelope~ envelope + +Vec~u32~ groups + +filter_by_groups() + +search_by_subject() + +search_by_content() + +to_message() + } + + class Calendar { + +u32 id + +String title + +String description + +Vec~u32~ groups + +filter_by_groups() + +get_events() + } + + class Event { + +u32 id + +u32 calendar_id + +String title + +String description + +String location + +DateTime start_time + +DateTime end_time + +bool all_day + +String recurrence + +Vec~String~ attendees + +String organizer + +String status + +EventMeta meta + +Vec~u32~ groups + +filter_by_groups() + +get_calendar() + +get_attendee_contacts() + } + + class Contact { + +u32 id + +i64 created_at + +i64 modified_at + +String first_name + +String last_name + +String email + +String group + +Vec~u32~ groups + +filter_by_groups() + +search_by_name() + +search_by_email() + +get_events() + } + + class Message { + +u32 id + +String thread_id + +String sender_id + +Vec~String~ recipients + +String content + +Vec~String~ attachments + +Vec~u32~ groups + +MessageMeta meta + +filter_by_groups() + +search_by_content() + +update_status() + +edit_content() + +add_reaction() + } + + class Circle { + +u32 id + +String name + +String description + +Vec~Member~ members + } + + Calendar "1" -- "many" Event: contains + Contact "many" -- "many" Event: attends + Circle "1" -- "many" Email: groups + Circle "1" -- "many" Calendar: groups + Circle "1" -- "many" Event: groups + Circle "1" -- "many" Contact: groups + Circle "1" -- "many" Message: groups +``` + +## 5. Testing Strategy + +1. Unit tests for each model to verify: + - Group field functionality + - New utility methods + - Serialization/deserialization with the new fields +2. Integration tests to verify: + - Database operations with the updated models + - Relationships between models \ No newline at end of file diff --git a/herodb/src/models/mcc/README.md b/herodb/src/models/mcc/README.md index cd42bad..7be5fb3 100644 --- a/herodb/src/models/mcc/README.md +++ b/herodb/src/models/mcc/README.md @@ -21,6 +21,14 @@ The Mail models provide email and IMAP functionality: - **Attachment**: Represents a file attachment with file information - **Envelope**: Represents an IMAP envelope structure with message headers +### Message (`message.rs`) + +The Message models provide chat functionality: + +- **Message**: Main struct for chat messages with thread and recipient information +- **MessageMeta**: Contains metadata for message status, editing, and reactions +- **MessageStatus**: Enum representing the status of a message (Sent, Delivered, Read, Failed) + ### Calendar (`calendar.rs`) The Calendar model represents a container for calendar events: @@ -40,6 +48,31 @@ The Contacts model provides contact management: - **Contact**: Main struct for contact information with personal details and grouping +## Group Support + +All models now support linking to multiple groups (Circle IDs): + +- Each model has a `groups: Vec` field to store multiple group IDs +- Utility methods for adding, removing, and filtering by groups +- Groups are defined in the Circle module + +## Utility Methods + +Each model provides utility methods for: + +### Filtering/Searching +- `filter_by_groups(groups: &[u32]) -> bool`: Filter by groups +- `search_by_subject/content/name/email(query: &str) -> bool`: Search by various fields + +### Format Conversion +- `to_message()`: Convert Email to Message + +### Relationship Management +- `get_events()`: Get events associated with a calendar or contact +- `get_calendar()`: Get the calendar an event belongs to +- `get_attendee_contacts()`: Get contacts for event attendees +- `get_thread_messages()`: Get all messages in the same thread + ## Usage These models are used by the MCC module to manage emails, calendar events, and contacts. They are typically accessed through the database handlers that implement the generic SledDB interface. diff --git a/herodb/src/models/mcc/calendar.rs b/herodb/src/models/mcc/calendar.rs index cd5acdc..816ae08 100644 --- a/herodb/src/models/mcc/calendar.rs +++ b/herodb/src/models/mcc/calendar.rs @@ -1,5 +1,6 @@ use serde::{Deserialize, Serialize}; -use crate::core::{SledModel, Storable}; +use crate::db::{SledModel, Storable, SledDB, SledDBResult}; +use crate::models::mcc::event::Event; /// Calendar represents a calendar container for events #[derive(Debug, Clone, Serialize, Deserialize)] @@ -7,6 +8,7 @@ pub struct Calendar { pub id: u32, // Unique identifier pub title: String, // Calendar title pub description: String, // Calendar details + pub groups: Vec, // Groups this calendar belongs to (references Circle IDs) } impl Calendar { @@ -16,8 +18,37 @@ impl Calendar { id, title, description, + groups: Vec::new(), } } + + /// Add a group to this calendar + pub fn add_group(&mut self, group_id: u32) { + if !self.groups.contains(&group_id) { + self.groups.push(group_id); + } + } + + /// Remove a group from this calendar + pub fn remove_group(&mut self, group_id: u32) { + self.groups.retain(|&id| id != group_id); + } + + /// Filter by groups - returns true if this calendar belongs to any of the specified groups + pub fn filter_by_groups(&self, groups: &[u32]) -> bool { + groups.iter().any(|g| self.groups.contains(g)) + } + + /// Get all events associated with this calendar + pub fn get_events(&self, db: &SledDB) -> SledDBResult> { + let all_events = db.list()?; + let calendar_events = all_events + .into_iter() + .filter(|event| event.calendar_id == self.id) + .collect(); + + Ok(calendar_events) + } } // Implement Storable trait (provides default dump/load) diff --git a/herodb/src/models/mcc/contacts.rs b/herodb/src/models/mcc/contacts.rs index bc6b681..0854d9e 100644 --- a/herodb/src/models/mcc/contacts.rs +++ b/herodb/src/models/mcc/contacts.rs @@ -1,6 +1,7 @@ use serde::{Deserialize, Serialize}; -use crate::core::{SledModel, Storable}; -use chrono::{DateTime, Utc}; +use crate::db::{SledModel, Storable, SledDB, SledDBResult}; +use crate::models::mcc::event::Event; +use chrono::Utc; /// Contact represents a contact entry in an address book #[derive(Debug, Clone, Serialize, Deserialize)] @@ -14,6 +15,7 @@ pub struct Contact { pub last_name: String, pub email: String, pub group: String, // Reference to a dns name, each group has a globally unique dns + pub groups: Vec, // Groups this contact belongs to (references Circle IDs) } impl Contact { @@ -28,9 +30,49 @@ impl Contact { last_name, email, group, + groups: Vec::new(), } } + /// Add a group to this contact + pub fn add_group(&mut self, group_id: u32) { + if !self.groups.contains(&group_id) { + self.groups.push(group_id); + } + } + + /// Remove a group from this contact + pub fn remove_group(&mut self, group_id: u32) { + self.groups.retain(|&id| id != group_id); + } + + /// Filter by groups - returns true if this contact belongs to any of the specified groups + pub fn filter_by_groups(&self, groups: &[u32]) -> bool { + groups.iter().any(|g| self.groups.contains(g)) + } + + /// Search by name - returns true if the name contains the query (case-insensitive) + pub fn search_by_name(&self, query: &str) -> bool { + let full_name = self.full_name().to_lowercase(); + query.to_lowercase().split_whitespace().all(|word| full_name.contains(word)) + } + + /// Search by email - returns true if the email contains the query (case-insensitive) + pub fn search_by_email(&self, query: &str) -> bool { + self.email.to_lowercase().contains(&query.to_lowercase()) + } + + /// Get events where this contact is an attendee + pub fn get_events(&self, db: &SledDB) -> SledDBResult> { + let all_events = db.list()?; + let contact_events = all_events + .into_iter() + .filter(|event| event.attendees.contains(&self.email)) + .collect(); + + Ok(contact_events) + } + /// Update the contact's information pub fn update(&mut self, first_name: Option, last_name: Option, email: Option, group: Option) { if let Some(first_name) = first_name { @@ -52,6 +94,12 @@ impl Contact { self.modified_at = Utc::now().timestamp(); } + /// Update the contact's groups + pub fn update_groups(&mut self, groups: Vec) { + self.groups = groups; + self.modified_at = Utc::now().timestamp(); + } + /// Get the full name of the contact pub fn full_name(&self) -> String { format!("{} {}", self.first_name, self.last_name) diff --git a/herodb/src/models/mcc/event.rs b/herodb/src/models/mcc/event.rs index dcfedc8..e79e56b 100644 --- a/herodb/src/models/mcc/event.rs +++ b/herodb/src/models/mcc/event.rs @@ -1,5 +1,7 @@ use serde::{Deserialize, Serialize}; -use crate::core::{SledModel, Storable}; +use crate::db::{SledModel, Storable, SledDB, SledDBResult}; +use crate::models::mcc::calendar::Calendar; +use crate::models::mcc::contacts::Contact; use chrono::{DateTime, Utc}; /// EventMeta contains additional metadata for a calendar event @@ -27,6 +29,7 @@ pub struct Event { pub organizer: String, // Organizer email pub status: String, // "CONFIRMED", "CANCELLED", "TENTATIVE" pub meta: EventMeta, // Additional metadata + pub groups: Vec, // Groups this event belongs to (references Circle IDs) } impl Event { @@ -60,9 +63,43 @@ impl Event { etag: String::new(), color: String::new(), }, + groups: Vec::new(), } } + /// Add a group to this event + pub fn add_group(&mut self, group_id: u32) { + if !self.groups.contains(&group_id) { + self.groups.push(group_id); + } + } + + /// Remove a group from this event + pub fn remove_group(&mut self, group_id: u32) { + self.groups.retain(|&id| id != group_id); + } + + /// Filter by groups - returns true if this event belongs to any of the specified groups + pub fn filter_by_groups(&self, groups: &[u32]) -> bool { + groups.iter().any(|g| self.groups.contains(g)) + } + + /// Get the calendar this event belongs to + pub fn get_calendar(&self, db: &SledDB) -> SledDBResult { + db.get(&self.calendar_id.to_string()) + } + + /// Get contacts for all attendees of this event + pub fn get_attendee_contacts(&self, db: &SledDB) -> SledDBResult> { + let all_contacts = db.list()?; + let attendee_contacts = all_contacts + .into_iter() + .filter(|contact| self.attendees.contains(&contact.email)) + .collect(); + + Ok(attendee_contacts) + } + /// Add an attendee to this event pub fn add_attendee(&mut self, attendee: String) { self.attendees.push(attendee); @@ -77,6 +114,16 @@ impl Event { pub fn set_status(&mut self, status: &str) { self.status = status.to_string(); } + + /// Search by title - returns true if the title contains the query (case-insensitive) + pub fn search_by_title(&self, query: &str) -> bool { + self.title.to_lowercase().contains(&query.to_lowercase()) + } + + /// Search by description - returns true if the description contains the query (case-insensitive) + pub fn search_by_description(&self, query: &str) -> bool { + self.description.to_lowercase().contains(&query.to_lowercase()) + } } // Implement Storable trait (provides default dump/load) diff --git a/herodb/src/models/mcc/lib.rs b/herodb/src/models/mcc/lib.rs index 86cd3d7..11ae716 100644 --- a/herodb/src/models/mcc/lib.rs +++ b/herodb/src/models/mcc/lib.rs @@ -2,12 +2,14 @@ pub mod calendar; pub mod event; pub mod mail; pub mod contacts; +pub mod message; // Re-export all model types for convenience pub use calendar::Calendar; pub use event::{Event, EventMeta}; pub use mail::{Email, Attachment, Envelope}; pub use contacts::Contact; +pub use message::{Message, MessageMeta, MessageStatus}; -// Re-export database components from core module -pub use crate::core::{SledDB, SledDBError, SledDBResult, Storable, SledModel, DB}; \ No newline at end of file +// Re-export database components from db module +pub use crate::db::{SledDB, SledDBError, SledDBResult, Storable, SledModel, DB}; \ No newline at end of file diff --git a/herodb/src/models/mcc/mail.rs b/herodb/src/models/mcc/mail.rs index 16f7c71..68912e0 100644 --- a/herodb/src/models/mcc/mail.rs +++ b/herodb/src/models/mcc/mail.rs @@ -1,6 +1,6 @@ use serde::{Deserialize, Serialize}; -use crate::core::{SledModel, Storable}; -use chrono::{DateTime, Utc}; +use crate::db::{SledModel, Storable, SledDBResult, SledDB}; +use chrono::Utc; /// Email represents an email message with all its metadata and content #[derive(Debug, Clone, Serialize, Deserialize)] @@ -17,6 +17,7 @@ pub struct Email { pub flags: Vec, // IMAP flags like \Seen, \Deleted, etc. pub receivetime: i64, // Unix timestamp when the email was received pub envelope: Option, // IMAP envelope information (contains From, To, Subject, etc.) + pub groups: Vec, // Groups this email belongs to (references Circle IDs) } /// Attachment represents an email attachment @@ -56,6 +57,7 @@ impl Email { flags: Vec::new(), receivetime: chrono::Utc::now().timestamp(), envelope: None, + groups: Vec::new(), } } @@ -64,10 +66,86 @@ impl Email { self.attachments.push(attachment); } + /// Add a group to this email + pub fn add_group(&mut self, group_id: u32) { + if !self.groups.contains(&group_id) { + self.groups.push(group_id); + } + } + + /// Remove a group from this email + pub fn remove_group(&mut self, group_id: u32) { + self.groups.retain(|&id| id != group_id); + } + + /// Filter by groups - returns true if this email belongs to any of the specified groups + pub fn filter_by_groups(&self, groups: &[u32]) -> bool { + groups.iter().any(|g| self.groups.contains(g)) + } + + /// Search by subject - returns true if the subject contains the query (case-insensitive) + pub fn search_by_subject(&self, query: &str) -> bool { + if let Some(env) = &self.envelope { + env.subject.to_lowercase().contains(&query.to_lowercase()) + } else { + false + } + } + + /// Search by content - returns true if the message content contains the query (case-insensitive) + pub fn search_by_content(&self, query: &str) -> bool { + self.message.to_lowercase().contains(&query.to_lowercase()) + } + /// Set the envelope for this email pub fn set_envelope(&mut self, envelope: Envelope) { self.envelope = Some(envelope); } + + /// Convert this email to a Message (for chat) + pub fn to_message(&self, id: u32, thread_id: String) -> crate::models::mcc::message::Message { + use crate::models::mcc::message::Message; + + let now = Utc::now(); + let sender = if let Some(env) = &self.envelope { + if !env.from.is_empty() { + env.from[0].clone() + } else { + "unknown@example.com".to_string() + } + } else { + "unknown@example.com".to_string() + }; + + let subject = if let Some(env) = &self.envelope { + env.subject.clone() + } else { + "No Subject".to_string() + }; + + let recipients = if let Some(env) = &self.envelope { + env.to.clone() + } else { + Vec::new() + }; + + let content = if !subject.is_empty() { + format!("{}\n\n{}", subject, self.message) + } else { + self.message.clone() + }; + + let mut message = Message::new(id, thread_id, sender, content); + message.recipients = recipients; + message.groups = self.groups.clone(); + + // Convert attachments to references + for attachment in &self.attachments { + message.add_attachment(attachment.filename.clone()); + } + + message + } } // Implement Storable trait (provides default dump/load) diff --git a/herodb/src/models/mcc/message.rs b/herodb/src/models/mcc/message.rs new file mode 100644 index 0000000..5728453 --- /dev/null +++ b/herodb/src/models/mcc/message.rs @@ -0,0 +1,134 @@ +use serde::{Deserialize, Serialize}; +use crate::db::{SledModel, Storable, SledDB, SledDBResult}; +use chrono::{DateTime, Utc}; + +/// MessageStatus represents the status of a message +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum MessageStatus { + Sent, + Delivered, + Read, + Failed, +} + +/// MessageMeta contains metadata for a chat message +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct MessageMeta { + pub created_at: DateTime, + pub updated_at: DateTime, + pub status: MessageStatus, + pub is_edited: bool, + pub reactions: Vec, +} + +/// Message represents a chat message +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Message { + pub id: u32, // Unique identifier + pub thread_id: String, // Thread/conversation identifier + pub sender_id: String, // Sender identifier + pub recipients: Vec, // List of recipient identifiers + pub content: String, // Message content + pub attachments: Vec, // References to attachments + pub groups: Vec, // Groups this message belongs to (references Circle IDs) + pub meta: MessageMeta, // Message metadata +} + +impl Message { + /// Create a new message + pub fn new(id: u32, thread_id: String, sender_id: String, content: String) -> Self { + let now = Utc::now(); + Self { + id, + thread_id, + sender_id, + recipients: Vec::new(), + content, + attachments: Vec::new(), + groups: Vec::new(), + meta: MessageMeta { + created_at: now, + updated_at: now, + status: MessageStatus::Sent, + is_edited: false, + reactions: Vec::new(), + }, + } + } + + /// Add a recipient to this message + pub fn add_recipient(&mut self, recipient: String) { + self.recipients.push(recipient); + } + + /// Add an attachment to this message + pub fn add_attachment(&mut self, attachment: String) { + self.attachments.push(attachment); + } + + /// Add a group to this message + pub fn add_group(&mut self, group_id: u32) { + if !self.groups.contains(&group_id) { + self.groups.push(group_id); + } + } + + /// Remove a group from this message + pub fn remove_group(&mut self, group_id: u32) { + self.groups.retain(|&id| id != group_id); + } + + /// Filter by groups - returns true if this message belongs to any of the specified groups + pub fn filter_by_groups(&self, groups: &[u32]) -> bool { + groups.iter().any(|g| self.groups.contains(g)) + } + + /// Search by content - returns true if the content contains the query (case-insensitive) + pub fn search_by_content(&self, query: &str) -> bool { + self.content.to_lowercase().contains(&query.to_lowercase()) + } + + /// Update message status + pub fn update_status(&mut self, status: MessageStatus) { + self.meta.status = status; + self.meta.updated_at = Utc::now(); + } + + /// Edit message content + pub fn edit_content(&mut self, new_content: String) { + self.content = new_content; + self.meta.is_edited = true; + self.meta.updated_at = Utc::now(); + } + + /// Add a reaction to the message + pub fn add_reaction(&mut self, reaction: String) { + self.meta.reactions.push(reaction); + self.meta.updated_at = Utc::now(); + } + + /// Get all messages in the same thread + pub fn get_thread_messages(&self, db: &SledDB) -> SledDBResult> { + let all_messages = db.list()?; + let thread_messages = all_messages + .into_iter() + .filter(|msg| msg.thread_id == self.thread_id) + .collect(); + + Ok(thread_messages) + } +} + +// Implement Storable trait (provides default dump/load) +impl Storable for Message {} + +// Implement SledModel trait +impl SledModel for Message { + fn get_id(&self) -> String { + self.id.to_string() + } + + fn db_prefix() -> &'static str { + "message" + } +} \ No newline at end of file diff --git a/herodb/src/models/mcc/mod.rs b/herodb/src/models/mcc/mod.rs index 86cd3d7..11ae716 100644 --- a/herodb/src/models/mcc/mod.rs +++ b/herodb/src/models/mcc/mod.rs @@ -2,12 +2,14 @@ pub mod calendar; pub mod event; pub mod mail; pub mod contacts; +pub mod message; // Re-export all model types for convenience pub use calendar::Calendar; pub use event::{Event, EventMeta}; pub use mail::{Email, Attachment, Envelope}; pub use contacts::Contact; +pub use message::{Message, MessageMeta, MessageStatus}; -// Re-export database components from core module -pub use crate::core::{SledDB, SledDBError, SledDBResult, Storable, SledModel, DB}; \ No newline at end of file +// Re-export database components from db module +pub use crate::db::{SledDB, SledDBError, SledDBResult, Storable, SledModel, DB}; \ No newline at end of file diff --git a/herodb/src/models/mod.rs b/herodb/src/models/mod.rs index 574868c..bc6d16a 100644 --- a/herodb/src/models/mod.rs +++ b/herodb/src/models/mod.rs @@ -1 +1,4 @@ -pub mod biz; \ No newline at end of file +pub mod biz; +pub mod mcc; +pub mod circle; +pub mod governance; \ No newline at end of file diff --git a/herodb/tmp/dbexample2/currency/conf b/herodb/tmp/dbexample2/currency/conf deleted file mode 100644 index 4154d7c..0000000 --- a/herodb/tmp/dbexample2/currency/conf +++ /dev/null @@ -1,4 +0,0 @@ -segment_size: 524288 -use_compression: false -version: 0.34 -vQÁ \ No newline at end of file diff --git a/herodb/tmp/dbexample2/currency/db b/herodb/tmp/dbexample2/currency/db deleted file mode 100644 index d580733..0000000 Binary files a/herodb/tmp/dbexample2/currency/db and /dev/null differ diff --git a/herodb/tmp/dbexample2/product/conf b/herodb/tmp/dbexample2/product/conf deleted file mode 100644 index 4154d7c..0000000 --- a/herodb/tmp/dbexample2/product/conf +++ /dev/null @@ -1,4 +0,0 @@ -segment_size: 524288 -use_compression: false -version: 0.34 -vQÁ \ No newline at end of file diff --git a/herodb/tmp/dbexample2/product/db b/herodb/tmp/dbexample2/product/db deleted file mode 100644 index 3e682e3..0000000 Binary files a/herodb/tmp/dbexample2/product/db and /dev/null differ diff --git a/herodb/tmp/dbexample2/sale/conf b/herodb/tmp/dbexample2/sale/conf deleted file mode 100644 index 4154d7c..0000000 --- a/herodb/tmp/dbexample2/sale/conf +++ /dev/null @@ -1,4 +0,0 @@ -segment_size: 524288 -use_compression: false -version: 0.34 -vQÁ \ No newline at end of file diff --git a/herodb/tmp/dbexample2/sale/db b/herodb/tmp/dbexample2/sale/db deleted file mode 100644 index a2c7114..0000000 Binary files a/herodb/tmp/dbexample2/sale/db and /dev/null differ