merge branches and cleanup db

This commit is contained in:
timurgordon
2025-06-27 12:11:04 +03:00
parent 5563d7e27e
commit 1f9ec01934
177 changed files with 1202 additions and 174 deletions

BIN
heromodels/.DS_Store vendored

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,266 @@
# Migration Plan: Restructuring herodb to Use ourdb as Backend
This document outlines the plan to restructure herodb to use ourdb as the backend, completely removing all sled references and better aligning with ourdb's design patterns.
## Overview
```mermaid
graph TD
A[Current herodb with sled] --> B[Define new core traits]
B --> C[Implement ourdb backend]
C --> D[Create new DB manager]
D --> E[Implement transaction system]
E --> F[Update model implementations]
F --> G[Final restructured herodb with ourdb]
```
## New Architecture
```mermaid
classDiagram
class Model {
+get_id() u32
+db_prefix() &'static str
}
class Storable {
+serialize() Result<Vec<u8>>
+deserialize() Result<Self>
}
class DB {
-path: PathBuf
-type_map: HashMap<TypeId, Arc<dyn DbOperations>>
-transaction: Arc<RwLock<Option<TransactionState>>>
+new(config: DbConfig) Result<Self>
+begin_transaction() Result<()>
+commit_transaction() Result<()>
+rollback_transaction() Result<()>
+set<T: Model>(model: &T) Result<()>
+get<T: Model>(id: u32) Result<T>
+delete<T: Model>(id: u32) Result<()>
+list<T: Model>() Result<Vec<T>>
+register<T: Model>() Result<()>
+get_history<T: Model>(id: u32, depth: u8) Result<Vec<T>>
}
class DbOperations {
<<interface>>
+delete(id: u32) Result<()>
+get(id: u32) Result<Box<dyn Any>>
+list() Result<Box<dyn Any>>
+insert(model: &dyn Any) Result<()>
+get_history(id: u32, depth: u8) Result<Vec<Box<dyn Any>>>
}
class OurDbStore~T~ {
-db: OurDB
-model_type: PhantomData<T>
+new(config: OurDBConfig) Result<Self>
+insert(model: &T) Result<()>
+get(id: u32) Result<T>
+delete(id: u32) Result<()>
+list() Result<Vec<T>>
+get_history(id: u32, depth: u8) Result<Vec<T>>
}
Model --|> Storable
OurDbStore ..|> DbOperations
DB o-- DbOperations
```
## Detailed Restructuring Steps
### 1. Define New Core Traits and Types
1. Create a new `Model` trait to replace `SledModel`
2. Create a new `Storable` trait for serialization/deserialization
3. Define a new error type hierarchy based on ourdb's error types
4. Create a `DbOperations` trait for database operations
### 2. Implement ourdb Backend
1. Create an `OurDbStore<T>` type that wraps ourdb
2. Implement the `DbOperations` trait for `OurDbStore<T>`
3. Add support for history tracking
### 3. Create New DB Manager
1. Create a new `DB` struct that manages multiple model types
2. Implement a builder pattern for configuration
3. Add methods for CRUD operations
### 4. Implement Transaction System
1. Create a transaction system that works with ourdb
2. Implement transaction operations (begin, commit, rollback)
3. Handle transaction state tracking
### 5. Update Model Implementations
1. Update all models to use `u32` IDs
2. Implement the new `Model` trait for all models
3. Update model constructors and builders
## Implementation Details
### 1. Core Traits and Types
```rust
// Error types
pub enum DbError {
IoError(std::io::Error),
SerializationError(bincode::Error),
NotFound(u32),
TransactionError(String),
// Map to ourdb error types
OurDbError(ourdb::Error),
// Other error types as needed
}
// Result type alias
pub type DbResult<T> = Result<T, DbError>;
// Storable trait
pub trait Storable: Serialize + for<'de> Deserialize<'de> + Sized {
fn serialize(&self) -> DbResult<Vec<u8>> {
// Default implementation using bincode
Ok(bincode::serialize(self)?)
}
fn deserialize(data: &[u8]) -> DbResult<Self> {
// Default implementation using bincode
Ok(bincode::deserialize(data)?)
}
}
// Model trait
pub trait Model: Storable + Debug + Clone + Send + Sync + 'static {
fn get_id(&self) -> u32;
fn db_prefix() -> &'static str;
}
```
### 2. ourdb Backend Implementation
```rust
pub struct OurDbStore<T: Model> {
db: OurDB,
_phantom: PhantomData<T>,
}
impl<T: Model> OurDbStore<T> {
pub fn new(config: OurDBConfig) -> DbResult<Self> {
let db = OurDB::new(config)?;
Ok(Self {
db,
_phantom: PhantomData,
})
}
// Implementation of CRUD operations
}
impl<T: Model> DbOperations for OurDbStore<T> {
// Implementation of DbOperations trait
}
```
### 3. DB Manager Implementation
```rust
pub struct DB {
path: PathBuf,
type_map: HashMap<TypeId, Arc<dyn DbOperations>>,
transaction: Arc<RwLock<Option<TransactionState>>>,
}
impl DB {
pub fn new(config: DbConfig) -> DbResult<Self> {
// Implementation
}
// CRUD operations and other methods
}
```
### 4. Transaction System
```rust
pub struct TransactionState {
operations: Vec<DbOperation>,
active: bool,
}
enum DbOperation {
Set {
model_type: TypeId,
serialized: Vec<u8>,
},
Delete {
model_type: TypeId,
id: u32,
},
}
impl DB {
pub fn begin_transaction(&self) -> DbResult<()> {
// Implementation
}
pub fn commit_transaction(&self) -> DbResult<()> {
// Implementation
}
pub fn rollback_transaction(&self) -> DbResult<()> {
// Implementation
}
}
```
### 5. Model Implementation Updates
```rust
// Example for Product model
impl Model for Product {
fn get_id(&self) -> u32 {
self.id
}
fn db_prefix() -> &'static str {
"product"
}
}
impl Storable for Product {}
```
## Key Technical Considerations
1. **Clean Architecture**: The new design provides a cleaner separation of concerns.
2. **Incremental IDs**: All models will use `u32` IDs, and ourdb will be configured in incremental mode.
3. **History Tracking**: The new API will expose ourdb's history tracking capabilities.
4. **Transaction Support**: We'll implement a custom transaction system on top of ourdb.
5. **Error Handling**: New error types will map directly to ourdb's error types.
6. **Serialization**: We'll use bincode for serialization/deserialization by default.
## Migration Risks and Mitigations
| Risk | Mitigation |
|------|------------|
| Breaking API changes | Create a compatibility layer if needed |
| Data migration complexity | Develop a data migration utility |
| Performance impact | Benchmark before and after |
| Implementation complexity | Implement in phases with thorough testing |
| Integration issues | Create comprehensive integration tests |
## Implementation Phases
1. **Phase 1**: Define core traits and types
2. **Phase 2**: Implement ourdb backend
3. **Phase 3**: Create DB manager
4. **Phase 4**: Implement transaction system
5. **Phase 5**: Update model implementations
6. **Phase 6**: Create tests and benchmarks
7. **Phase 7**: Develop data migration utility

View File

@@ -0,0 +1,161 @@
# MCC Models Standalone Implementation Plan
## Overview
This document outlines the plan to make the MCC models in `herodb/src/models/mcc` completely standalone without dependencies on the database implementation, while ensuring that examples like `herodb/src/cmd/dbexample_mcc` can still work.
## Current Architecture Analysis
```mermaid
graph TD
subgraph "Before"
A1[MCC Models] -->|Depends on| B1[DB Implementation]
A1 -->|Implements| C1[Model Trait]
A1 -->|Implements| D1[Storable Trait]
E1[dbexample_mcc] -->|Uses| A1
E1 -->|Uses| B1
end
subgraph "After"
A2[Standalone MCC Models] -.->|No dependency on| B2[DB Implementation]
B2 -->|Works with| F2[Any Serializable Struct]
B2 -->|Optional trait| G2[ModelAdapter Trait]
E2[dbexample_mcc] -->|Uses| A2
E2 -->|Uses| B2
end
```
## Implementation Plan
### Phase 1: Make MCC Models Standalone
1. For each MCC model file (calendar.rs, event.rs, mail.rs, contacts.rs, message.rs):
- Remove `use crate::db::{Model, Storable, DB, DbError, DbResult}`
- Remove `impl Model for X` and `impl Storable for X` blocks
- Replace methods that use DB (like `get_events(&self, db: &DB)`) with standalone methods
2. For mod.rs and lib.rs:
- Remove `pub use crate::db::{DB, DBBuilder, Model, Storable, DbError, DbResult}`
### Phase 2: Modify DB Implementation
1. Create a new ModelAdapter trait in db/model.rs:
```rust
pub trait ModelAdapter {
fn get_id(&self) -> u32;
fn db_prefix() -> &'static str;
fn db_keys(&self) -> Vec<IndexKey> { Vec::new() }
}
```
2. Modify DB methods in db.rs to work with any serializable struct:
```rust
impl DB {
// Generic set method for any serializable type
pub fn set<T: Serialize + DeserializeOwned + 'static>(&self, item: &T, id: u32, prefix: &str) -> DbResult<()> {
// Implementation
}
// Enhanced version for types implementing ModelAdapter
pub fn set_model<T: Serialize + DeserializeOwned + ModelAdapter + 'static>(&self, item: &T) -> DbResult<()> {
self.set(item, item.get_id(), T::db_prefix())
}
// Similar changes for get, delete, list methods
}
```
3. Update DBBuilder to register any serializable type:
```rust
impl DBBuilder {
pub fn register_type<T: Serialize + DeserializeOwned + 'static>(&mut self, prefix: &'static str) -> &mut Self {
// Implementation
}
pub fn register_model<T: Serialize + DeserializeOwned + ModelAdapter + 'static>(&mut self) -> &mut Self {
self.register_type::<T>(T::db_prefix())
}
}
```
### Phase 3: Update Examples and Tests
1. Update dbexample_mcc/main.rs:
```rust
let db = DBBuilder::new(&db_path)
.register_type::<Calendar>("calendar")
.register_type::<Event>("event")
// etc.
.build()?;
```
2. Create a new standalone example for MCC models similar to circle_standalone.rs
## Detailed Changes Required
### MCC Models Changes
#### calendar.rs
- Remove database-related imports
- Remove Model and Storable trait implementations
- Replace `get_events(&self, db: &DB)` with a standalone method like:
```rust
pub fn filter_events(&self, events: &[Event]) -> Vec<&Event> {
events.iter()
.filter(|event| event.calendar_id == self.id)
.collect()
}
```
#### event.rs
- Remove database-related imports
- Remove Model and Storable trait implementations
- Add standalone methods for event operations
#### mail.rs
- Remove database-related imports
- Remove Model and Storable trait implementations
- Add standalone methods for mail operations
#### contacts.rs
- Remove database-related imports
- Remove Model and Storable trait implementations
- Add standalone methods for contact operations
#### message.rs
- Remove database-related imports
- Remove Model and Storable trait implementations
- Add standalone methods for message operations
#### mod.rs and lib.rs
- Remove re-exports of database components
### DB Implementation Changes
#### model.rs
- Create a new ModelAdapter trait
- Keep existing Model and Storable traits for backward compatibility
- Add helper methods for working with serializable structs
#### db.rs
- Modify DB methods to work with any serializable struct
- Add overloaded methods for ModelAdapter types
- Ensure backward compatibility with existing code
#### DBBuilder
- Update to register any serializable type
- Keep existing methods for backward compatibility
## Testing Strategy
1. Ensure all existing tests pass with the modified DB implementation
2. Create new tests for standalone MCC models
3. Verify dbexample_mcc works with the new implementation
4. Create a new standalone example for MCC models
## Benefits
1. MCC models become more reusable and can be used without database dependencies
2. DB implementation becomes more flexible and can work with any serializable struct
3. Cleaner separation of concerns between models and database operations
4. Easier to test models in isolation

View File

@@ -0,0 +1,277 @@
# Model Trait Unification Plan
## Introduction
This document outlines the plan to unify the `BaseModel` and `ModelBuilder` traits in the heromodels crate into a single `Model` trait. The goal is to simplify the codebase, reduce boilerplate, and make the API more intuitive while maintaining all existing functionality.
## Current Structure
Currently, the codebase has two separate traits:
1. **BaseModel** - Provides database-related functionality:
- `db_prefix()` - Returns a string prefix for database operations
- `db_keys()` - Returns index keys for the model
- `get_id()` - Returns the model's unique ID
2. **ModelBuilder** - Provides builder pattern functionality:
- `base_data_mut()` - Gets a mutable reference to the base data
- `id()` - Sets the ID for the model
- `build()` - Finalizes the model by updating timestamps
Each model (like User and Comment) implements both traits separately, and there are two separate macros for implementing these traits:
```rust
// For BaseModel
impl_base_model!(User, "user");
// For ModelBuilder
impl_model_builder!(User);
```
This leads to duplication and cognitive overhead when working with models.
## Proposed Structure
We will create a unified `Model` trait that combines all the functionality from both existing traits:
```mermaid
classDiagram
class Model {
<<trait>>
+db_prefix() static str
+db_keys() Vec~IndexKey~
+get_id() u32
+base_data_mut() &mut BaseModelData
+id(u32) Self
+build() Self
}
class User {
+base_data BaseModelData
+username String
+email String
+full_name String
+is_active bool
}
class Comment {
+base_data BaseModelData
+user_id u32
+content String
}
Model <|-- User
Model <|-- Comment
```
## Implementation Steps
### 1. Update model.rs
Create the new `Model` trait that combines all methods from both existing traits:
```rust
/// Unified trait for all models
pub trait Model: Debug + Clone + Serialize + for<'de> Deserialize<'de> + Send + Sync + 'static {
/// Get the database prefix for this model type
fn db_prefix() -> &'static str where Self: Sized;
/// Returns a list of index keys for this model instance
/// These keys will be used to create additional indexes in the TST
fn db_keys(&self) -> Vec<IndexKey> {
Vec::new()
}
/// Get the unique ID for this model
fn get_id(&self) -> u32;
/// Get a mutable reference to the base_data field
fn base_data_mut(&mut self) -> &mut BaseModelData;
/// Set the ID for this model
fn id(mut self, id: u32) -> Self where Self: Sized {
self.base_data_mut().id = id;
self
}
/// Build the model, updating the modified timestamp
fn build(mut self) -> Self where Self: Sized {
self.base_data_mut().update_modified();
self
}
}
```
Create a new implementation macro that implements all required methods:
```rust
/// Macro to implement Model for a struct that contains a base_data field of type BaseModelData
#[macro_export]
macro_rules! impl_model {
($type:ty, $prefix:expr) => {
impl $crate::core::model::Model for $type {
fn db_prefix() -> &'static str {
$prefix
}
fn get_id(&self) -> u32 {
self.base_data.id
}
fn base_data_mut(&mut self) -> &mut $crate::core::model::BaseModelData {
&mut self.base_data
}
// Other methods have default implementations
}
};
}
```
Mark the old traits and macros as deprecated (or remove them entirely):
```rust
#[deprecated(
since = "0.2.0",
note = "Use the unified Model trait instead"
)]
pub trait BaseModel { /* ... */ }
#[deprecated(
since = "0.2.0",
note = "Use the unified Model trait instead"
)]
pub trait ModelBuilder { /* ... */ }
#[deprecated(
since = "0.2.0",
note = "Use impl_model instead"
)]
#[macro_export]
macro_rules! impl_base_model { /* ... */ }
#[deprecated(
since = "0.2.0",
note = "Use impl_model instead"
)]
#[macro_export]
macro_rules! impl_model_builder { /* ... */ }
```
### 2. Update User and Comment Implementations
For User:
```rust
// Implement Model for User
impl_model!(User, "user");
// Custom implementation of db_keys
impl Model for User {
fn db_keys(&self) -> Vec<IndexKey> {
vec![
IndexKey {
name: "username",
value: self.username.clone(),
},
IndexKey {
name: "email",
value: self.email.clone(),
},
IndexKey {
name: "is_active",
value: self.is_active.to_string(),
},
]
}
}
```
For Comment:
```rust
// Implement Model for Comment
impl_model!(Comment, "comment");
// Custom implementation of db_keys
impl Model for Comment {
fn db_keys(&self) -> Vec<IndexKey> {
vec![
IndexKey {
name: "user_id",
value: self.user_id.to_string(),
},
]
}
}
```
### 3. Update Imports and References
Update the lib.rs file to export the new trait and macro:
```rust
// Export core module
pub mod core;
// Export userexample module
pub mod userexample;
// Re-export key types for convenience
pub use core::model::{Model, BaseModelData, IndexKey};
pub use core::Comment;
pub use userexample::User;
// Re-export macros
pub use crate::impl_model;
```
Update the example code to use the new trait:
```rust
use heromodels::{Model, Comment, User};
fn main() {
println!("Hero Models - Basic Usage Example");
println!("================================");
// Create a new user using the fluent interface
let user = User::new(1)
.username("johndoe")
.email("john.doe@example.com")
.full_name("John Doe")
.build();
println!("Created user: {:?}", user);
println!("User ID: {}", user.get_id());
println!("User DB Prefix: {}", User::db_prefix());
// Create a comment for the user
let comment = Comment::new(1)
.user_id(2) // commenter's user ID
.content("This is a comment on the user")
.build();
println!("\nCreated comment: {:?}", comment);
println!("Comment ID: {}", comment.get_id());
println!("Comment DB Prefix: {}", Comment::db_prefix());
}
```
### 4. Testing
Run the example to ensure it works as expected:
```bash
cargo run --example basic_user_example
```
Verify that all functionality is preserved and that the output matches the expected output.
## Benefits of This Approach
1. **Simplified Code Structure**: One trait instead of two means less cognitive overhead
2. **Reduced Boilerplate**: A single macro implementation reduces repetitive code
3. **Improved Usability**: No need to import multiple traits or worry about which trait provides which method
4. **Maintained Functionality**: All existing functionality is preserved
5. **Better Encapsulation**: The model concept is more cohesive as a single trait

View File

@@ -0,0 +1,151 @@
# AI Prompt: Generate Rhai Module Integration for Rust Models
## Context
You are tasked with creating a Rhai scripting integration for Rust models in the heromodels crate. The integration should follow the builder pattern and provide a comprehensive set of functions for interacting with the models through Rhai scripts.
## Key Concepts to Understand
1. **Builder Pattern vs. Rhai Mutation**: Rust models use builder pattern (methods consume self and return Self), but Rhai requires functions that mutate in place (&mut self).
2. **Database Access**: Database functions are exposed as global functions in a 'db' module namespace (e.g., `db::save_flow`, not `db.save_flow`).
3. **ID Handling**: New objects should use ID 0 to allow database assignment of real IDs.
4. **Function vs. Method Calls**: Rhai scripts must use function calls (e.g., `status(flow, STATUS_DRAFT)`) not method chaining (e.g., `flow.status(STATUS_DRAFT)`).
## Requirements
1. **Module Structure and Imports**:
- Create a `rhai.rs` file that exports a function named `register_[model]_rhai_module` which takes an Engine and an Arc<OurDB>
- **IMPORTANT**: Import `heromodels_core::Model` trait for access to methods like `get_id`
- Use the `#[export_module]` macro to define the Rhai module
- Register the module globally with the engine using the correct module registration syntax
- Create separate database functions that capture the database reference
2. **Builder Pattern Integration**:
- For each model struct marked with `#[model]`, create constructor functions (e.g., `new_[model]`) that accept ID 0 for new objects
- For each builder method in the model, create a corresponding Rhai function that:
- Takes a mutable reference to the model as first parameter
- Uses `std::mem::take` to get ownership (if the model implements Default) or `std::mem::replace` (if it doesn't)
- Calls the builder method on the owned object
- Assigns the result back to the mutable reference
- Returns a clone of the modified object
3. **Field Access**:
- **IMPORTANT**: Do NOT create getter methods for fields that can be accessed directly
- Use direct field access in example code (e.g., `flow.name`, not `flow.get_name()`)
- Only create getter functions for computed properties or when special conversion is needed
- Handle special types appropriately (e.g., convert timestamps, IDs, etc.)
4. **Database Functions**:
- For each model, implement the following database functions in the `db` module namespace:
- `db::save_[model]`: Save the model to the database and return the saved model with assigned ID
- `db::get_[model]_by_id`: Retrieve a model by ID
- `db::delete_[model]`: Delete a model from the database
- `db::list_[model]s`: List all models of this type
- **IMPORTANT**: Use `db::function_name` syntax in scripts, not `db.function_name`
- Handle errors properly with detailed error messages using debug format `{:?}` for error objects
- Convert between Rhai's i64 and Rust's u32/u64 types for IDs and timestamps
- Return saved objects from save functions to ensure proper ID handling
5. **Error Handling**:
- Use `Box<EvalAltResult>` for error returns
- Provide detailed error messages with proper position information
- **IMPORTANT**: Use `context.call_position()` not the deprecated `context.position()`
- Handle type conversions safely
- Format error messages with debug format for error objects: `format!("Error: {:?}", err)`
6. **Type Conversions**:
- Create helper functions for converting between Rhai and Rust types
- Handle special cases like enums with string representations
## Implementation Pattern
```rust
// Helper functions for type conversion
fn i64_to_u32(val: i64, position: Position, param_name: &str, function_name: &str) -> Result<u32, Box<EvalAltResult>> {
u32::try_from(val).map_err(|_| {
Box::new(EvalAltResult::ErrorArithmetic(
format!("{} must be a positive integer less than 2^32 in {}", param_name, function_name).into(),
position
))
})
}
// Model constructor
#[rhai_fn(name = "new_[model]")]
pub fn new_model() -> Model {
Model::new()
}
// Builder method integration
#[rhai_fn(name = "[method_name]", return_raw, global, pure)]
pub fn model_method(model: &mut Model, param: Type) -> Result<Model, Box<EvalAltResult>> {
let owned_model = std::mem::take(model); // or replace if Default not implemented
*model = owned_model.method_name(param);
Ok(model.clone())
}
// Getter method
#[rhai_fn(name = "[field]", global)]
pub fn get_model_field(model: &mut Model) -> Type {
model.field.clone()
}
// Database function
db_module.set_native_fn("save_[model]", move |model: Model| -> Result<Model, Box<EvalAltResult>> {
db_clone.set(&model)
.map_err(|e| Box::new(EvalAltResult::ErrorRuntime(format!("DB Error: {}", e).into(), Position::NONE)))
.map(|(_, model)| model)
});
```
## Important Notes
1. All models must follow the builder pattern where methods take ownership of `self` and return `Self`
2. If a model doesn't implement `Default`, use `std::mem::replace` instead of `std::mem::take`
3. Ensure proper error handling for all operations
4. Register all functions with appropriate Rhai names
5. Follow the naming conventions established in existing modules
6. Ensure all database operations are properly wrapped with error handling
7. **CRITICAL**: When creating example scripts, use function calls not method chaining
8. **CRITICAL**: Always use ID 0 for new objects in scripts to allow database assignment
9. **CRITICAL**: After saving objects, use the returned objects with assigned IDs for further operations
10. **CRITICAL**: In Rust examples, include the Model trait import and use direct field access
## Example Rhai Script
```rhai
// Create a new flow with ID 0 (database will assign real ID)
let flow = new_flow(0, "12345678-1234-1234-1234-123456789012");
name(flow, "Document Approval Flow");
status(flow, STATUS_DRAFT);
// Save flow to database and get saved object with assigned ID
let saved_flow = db::save_flow(flow);
print(`Flow saved to database with ID: ${get_flow_id(saved_flow)}`);
// Retrieve flow from database using assigned ID
let retrieved_flow = db::get_flow_by_id(get_flow_id(saved_flow));
```
## Example Rust Code
```rust
use heromodels_core::Model; // Import Model trait for get_id()
// Create flow using Rhai
let flow: Flow = engine.eval("new_flow(0, \"12345678-1234-1234-1234-123456789012\")").unwrap();
// Access fields directly
println!("Flow name: {}", flow.name);
// Save to database using Rhai function
let save_script = "fn save_it(f) { return db::save_flow(f); }";
let save_ast = engine.compile(save_script).unwrap();
let saved_flow: Flow = engine.call_fn(&mut scope, &save_ast, "save_it", (flow,)).unwrap();
println!("Saved flow ID: {}", saved_flow.get_id());
```
## Reference Implementations
See the existing implementations for examples:
1. `heromodels/src/models/calendar/rhai.rs` - Calendar module Rhai bindings
2. `heromodels/src/models/flow/rhai.rs` - Flow module Rhai bindings
3. `heromodels_rhai/examples/flow/flow_script.rhai` - Example Rhai script
4. `heromodels_rhai/examples/flow/example.rs` - Example Rust code

View File

@@ -0,0 +1,192 @@
## AI Prompt: Generate `rhai.rs` for a new Rust Model
**Objective:**
Create a `rhai.rs` file to expose the Rust model `[YourModelName]` (and any related owned sub-models like `[YourSubModelName]`) to the Rhai scripting engine. This file should allow Rhai scripts to create, manipulate, and retrieve instances of `[YourModelName]`.
**Input Requirements (Provide this information for your specific model):**
1. **Rust Struct Definition(s):**
```rust
// Example for [YourModelName]
#[derive(Clone, Debug, Serialize, Deserialize)] // Ensure necessary derives
pub struct [YourModelName] {
pub base_data: BaseModelData, // Common field for ID
pub unique_id_field: String, // Example: like flow_uuid
pub name: String,
pub status: String,
// If it owns a collection of sub-models:
pub sub_items: Vec<[YourSubModelName]>,
// Other fields...
}
impl [YourModelName] {
// Constructor
pub fn new(id: u32, unique_id_field: String /*, other essential params */) -> Self {
Self {
base_data: BaseModelData::new(id),
unique_id_field,
name: "".to_string(), // Default or passed in
status: "".to_string(), // Default or passed in
sub_items: Vec::new(),
// ...
}
}
// Builder methods
pub fn name(mut self, name: String) -> Self {
self.name = name;
self
}
pub fn status(mut self, status: String) -> Self {
self.status = status;
self
}
// Method to add sub-items
pub fn add_sub_item(mut self, item: [YourSubModelName]) -> Self {
self.sub_items.push(item);
self
}
// Other methods to expose...
}
// Example for [YourSubModelName] (if applicable)
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct [YourSubModelName] {
pub id: u32,
pub description: String,
// ...
}
impl [YourSubModelName] {
pub fn new(id: u32, description: String) -> Self {
Self { id, description }
}
// Builder methods for sub-model...
}
```
2. **Key ID fields that need `i64` (Rhai) to `u32` (Rust) conversion:** (e.g., `base_data.id`, `[YourSubModelName].id`, any foreign key IDs).
**Implementation Guidelines for `rhai.rs`:**
1. **File Structure:**
* Start with necessary imports: `rhai::{Dynamic, Engine, EvalAltResult, NativeCallContext, Position}`, `std::sync::Arc`, your model structs, `BaseModelData`, `OurDB`.
* Include the `i64_to_u32` helper function.
* Define `pub fn register_[your_model_name]_rhai_module(engine: &mut Engine, db: Arc<OurDB>) { ... }`.
2. **Helper Function for ID Conversion:**
```rust
fn i64_to_u32(val: i64, context_pos: Position, field_name: &str, object_name: &str) -> Result<u32, Box<EvalAltResult>> {
val.try_into().map_err(|_e| {
Box::new(EvalAltResult::ErrorArithmetic(
format!("Conversion error for {} in {} from i64 to u32", field_name, object_name),
context_pos,
))
})
}
```
3. **Constructors (e.g., `new_[your_model_name]`):**
* Use `NativeCallContext` for manual argument parsing and `i64_to_u32` conversion for ID fields.
* Example:
```rust
engine.register_fn("new_[your_model_name]",
move |context: NativeCallContext, id_i64: i64, unique_id_str: String /*, other_args... */|
-> Result<[YourModelName], Box<EvalAltResult>> {
let id_u32 = i64_to_u32(id_i64, context.position(), "id", "new_[your_model_name]")?;
Ok([YourModelName]::new(id_u32, unique_id_str /*, ... */))
});
```
* Do the same for `new_[your_sub_model_name]` if applicable.
* **Note on `adapter_macros`**: `adapt_rhai_i64_input_fn!` is generally NOT suitable for constructors with multiple arguments or mixed types (e.g., `u32` and `String`). Prefer `NativeCallContext`.
4. **Builder Methods:**
* Register functions that take ownership of the model, modify it, and return it.
* Example:
```rust
engine.register_fn("name", |model: [YourModelName], name_val: String| -> [YourModelName] { model.name(name_val) });
engine.register_fn("status", |model: [YourModelName], status_val: String| -> [YourModelName] { model.status(status_val) });
// For adding sub-items (if applicable)
engine.register_fn("add_sub_item", |model: [YourModelName], item: [YourSubModelName]| -> [YourModelName] { model.add_sub_item(item) });
```
5. **Getters:**
* Use `engine.register_get("field_name", |model: &mut [YourModelName]| -> Result<FieldType, Box<EvalAltResult>> { Ok(model.field.clone()) });`
* For `base_data.id` (u32), cast to `i64` for Rhai: `Ok(model.base_data.id as i64)`
* **For `Vec<[YourSubModelName]>` fields (e.g., `sub_items`):** Convert to `rhai::Array`.
```rust
engine.register_get("sub_items", |model: &mut [YourModelName]| -> Result<rhai::Array, Box<EvalAltResult>> {
let rhai_array = model.sub_items.iter().cloned().map(Dynamic::from).collect::<rhai::Array>();
Ok(rhai_array)
});
```
(Ensure `[YourSubModelName]` is `Clone` and works with `Dynamic::from`).
6. **Setters (Less common if using a full builder pattern, but can be useful):**
* Use `engine.register_set("field_name", |model: &mut [YourModelName], value: FieldType| { model.field = value; Ok(()) });`
* If a setter method in Rust takes an ID (e.g., `set_related_id(id: u32)`), and you want to call it from Rhai with an `i64`:
* You *could* use `adapter_macros::adapt_rhai_i64_input_method!(YourModelType::set_related_id, u32)` if `YourModelType::set_related_id` takes `&mut self, u32`.
* Or, handle manually with `NativeCallContext` if the method signature is more complex or doesn't fit the macro.
7. **Other Custom Methods:**
* Register any other public methods from your Rust struct that should be callable.
* Example: `engine.register_fn("custom_method_name", |model: &mut [YourModelName]| model.custom_method_in_rust());`
8. **Database Interaction (Using actual `OurDB` methods like `set` and `get_by_id`):**
* The `Arc<OurDB>` instance passed to `register_[your_model_name]_rhai_module` should be cloned and *captured* by the closures for DB functions.
* The Rhai script will call these functions without explicitly passing the DB instance (e.g., `set_my_model(my_model_instance);`, `let m = get_my_model_by_id(123);`).
* Ensure proper error handling, converting DB errors and `Option::None` (for getters) to `Box<EvalAltResult::ErrorRuntime(...)`.
* **Example for `set_[your_model_name]`:**
```rust
// In register_[your_model_name]_rhai_module(engine: &mut Engine, db: Arc<OurDB>)
let captured_db_for_set = Arc::clone(&db);
engine.register_fn("set_[your_model_name]",
move |model: [YourModelName]| -> Result<(), Box<EvalAltResult>> {
captured_db_for_set.set(&model).map_err(|e| {
Box::new(EvalAltResult::ErrorRuntime(
format!("Failed to set [YourModelName] (ID: {}): {}", model.base_data.id, e).into(),
Position::NONE,
))
})
});
```
* **Example for `get_[your_model_name]_by_id`:**
```rust
// In register_[your_model_name]_rhai_module(engine: &mut Engine, db: Arc<OurDB>)
let captured_db_for_get = Arc::clone(&db);
engine.register_fn("get_[your_model_name]_by_id",
move |context: NativeCallContext, id_i64: i64| -> Result<[YourModelName], Box<EvalAltResult>> {
let id_u32 = i64_to_u32(id_i64, context.position(), "id", "get_[your_model_name]_by_id")?;
captured_db_for_get.get_by_id(id_u32) // Assumes OurDB directly implements Collection<_, [YourModelName]>
.map_err(|e| Box::new(EvalAltResult::ErrorRuntime(
format!("Error getting [YourModelName] (ID: {}): {}", id_u32, e).into(),
Position::NONE,
)))?
.ok_or_else(|| Box::new(EvalAltResult::ErrorRuntime(
format!("[YourModelName] with ID {} not found", id_u32).into(),
Position::NONE,
)))
});
```
**Example Output Snippet (Illustrating a few parts):**
```rust
// #[...]
// pub struct MyItem { /* ... */ }
// impl MyItem { /* ... */ }
// pub fn register_my_item_rhai_module(engine: &mut Engine, db: Arc<OurDB>) {
// fn i64_to_u32(...) { /* ... */ }
//
// engine.register_fn("new_my_item", |ctx: NativeCallContext, id: i64, name: String| { /* ... */ });
// engine.register_fn("name", |item: MyItem, name: String| -> MyItem { /* ... */ });
// engine.register_get("id", |item: &mut MyItem| Ok(item.base_data.id as i64));
// engine.register_get("sub_elements", |item: &mut MyItem| {
// Ok(item.sub_elements.iter().cloned().map(Dynamic::from).collect::<rhai::Array>())
// });
// // ... etc.
// }
```

View File

@@ -0,0 +1,401 @@
# SigSocket: WebSocket Signing Server Architecture
Based on my analysis of the existing Actix application structure, I've designed a comprehensive architecture for implementing a WebSocket server that handles signing operations. This server will integrate seamlessly with the existing hostbasket application.
## 1. Overview
SigSocket will:
- Accept WebSocket connections from clients
- Allow clients to identify themselves with a public key
- Provide a `send_to_sign()` function that takes a public key and a message
- Forward the message to the appropriate client for signing
- Wait for a signed response (with a 1-minute timeout)
- Verify the signature using the client's public key
- Return the response message and signature
## 2. Component Architecture
```mermaid
graph TD
A[Actix Web Server] --> B[SigSocket Manager]
B --> C[Connection Registry]
B --> D[Message Handler]
D --> E[Signature Verifier]
F[Client] <--> B
G[Application Code] --> H[SigSocketService]
H --> B
```
### Key Components:
1. **SigSocket Manager**
- Handles WebSocket connections
- Manages connection lifecycle
- Routes messages to appropriate handlers
2. **Connection Registry**
- Maps public keys to active WebSocket connections
- Handles connection tracking and cleanup
- Provides lookup functionality
3. **Message Handler**
- Processes incoming messages
- Implements the message protocol
- Manages timeouts for responses
4. **Signature Verifier**
- Verifies signatures using public keys
- Implements cryptographic operations
- Ensures security of the signing process
5. **SigSocket Service**
- Provides a clean API for the application to use
- Abstracts WebSocket complexity from business logic
- Handles error cases and timeouts
## 3. Directory Structure
```
src/
├── lib.rs # Main library exports
├── manager.rs # WebSocket connection manager
├── registry.rs # Connection registry
├── handler.rs # Message handling logic
├── protocol.rs # Message protocol definitions
├── crypto.rs # Cryptographic operations
└── service.rs # Service API
```
## 4. Data Flow
```mermaid
sequenceDiagram
participant Client
participant SigSocketManager
participant Registry
participant Application
participant SigSocketService
Client->>SigSocketManager: Connect
Client->>SigSocketManager: Introduce(public_key)
SigSocketManager->>Registry: Register(connection, public_key)
Application->>SigSocketService: send_to_sign(public_key, message)
SigSocketService->>Registry: Lookup(public_key)
Registry-->>SigSocketService: connection
SigSocketService->>SigSocketManager: Send message to connection
SigSocketManager->>Client: Message to sign
Client->>SigSocketManager: Signed response
SigSocketManager->>SigSocketService: Forward response
SigSocketService->>SigSocketService: Verify signature
SigSocketService-->>Application: Return verified response
```
## 5. Message Protocol
We'll define a minimalist protocol for communication:
```
// Client introduction (first message upon connection)
<base64_encoded_public_key>
// Sign request (sent from server to client)
<base64_encoded_message>
// Sign response (sent from client to server)
<base64_encoded_message>.<base64_encoded_signature>
```
This simplified protocol reduces overhead and complexity:
- The introduction is always the first message, containing only the public key
- Sign requests contain only the message to be signed
- Responses use a simple "message.signature" format
## 6. Required Dependencies
We'll need to add the following dependencies to the project:
```toml
# WebSocket support
actix-web-actors = "4.2.0"
# Cryptography
secp256k1 = "0.28.0" # For secp256k1 signatures (used in Bitcoin/Ethereum)
sha2 = "0.10.8" # For hashing before signing
hex = "0.4.3" # For hex encoding/decoding
base64 = "0.21.0" # For base64 encoding/decoding
rand = "0.8.5" # For generating random data
```
## 7. Implementation Details
### 7.1 SigSocket Manager
The SigSocket Manager will handle the lifecycle of WebSocket connections:
```rust
pub struct SigSocketManager {
registry: Arc<RwLock<ConnectionRegistry>>,
}
impl Actor for SigSocketManager {
type Context = ws::WebsocketContext<Self>;
fn started(&mut self, ctx: &mut Self::Context) {
// Handle connection start
}
fn stopped(&mut self, ctx: &mut Self::Context) {
// Handle connection close and cleanup
}
}
impl StreamHandler<Result<ws::Message, ws::ProtocolError>> for SigSocketManager {
fn handle(&mut self, msg: Result<ws::Message, ws::ProtocolError>, ctx: &mut Self::Context) {
// Handle different types of WebSocket messages
}
}
```
### 7.2 Connection Registry
The Connection Registry will maintain a mapping of public keys to active connections:
```rust
pub struct ConnectionRegistry {
connections: HashMap<String, Addr<SigSocketManager>>,
}
impl ConnectionRegistry {
pub fn register(&mut self, public_key: String, addr: Addr<SigSocketManager>) {
self.connections.insert(public_key, addr);
}
pub fn unregister(&mut self, public_key: &str) {
self.connections.remove(public_key);
}
pub fn get(&self, public_key: &str) -> Option<&Addr<SigSocketManager>> {
self.connections.get(public_key)
}
}
```
### 7.3 SigSocket Service
The SigSocket Service will provide a clean API for controllers:
```rust
pub struct SigSocketService {
registry: Arc<RwLock<ConnectionRegistry>>,
}
impl SigSocketService {
pub async fn send_to_sign(&self, public_key: &str, message: &[u8])
-> Result<(Vec<u8>, Vec<u8>), SigSocketError> {
// 1. Find the connection for the public key
let connection = self.registry.read().await.get(public_key).cloned();
if let Some(conn) = connection {
// 2. Create a response channel
let (tx, rx) = oneshot::channel();
// 3. Register the response channel with the connection
self.pending_requests.write().await.insert(conn.clone(), tx);
// 4. Send the message to the client (just the raw message)
conn.do_send(base64::encode(message));
// 5. Wait for the response with a timeout
match tokio::time::timeout(Duration::from_secs(60), rx).await {
Ok(Ok(response)) => {
// 6. Parse the response in format "message.signature"
let parts: Vec<&str> = response.split('.').collect();
if parts.len() != 2 {
return Err(SigSocketError::InvalidResponseFormat);
}
let message_b64 = parts[0];
let signature_b64 = parts[1];
// 7. Decode the message and signature
let message_bytes = match base64::decode(message_b64) {
Ok(m) => m,
Err(_) => return Err(SigSocketError::DecodingError),
};
let signature_bytes = match base64::decode(signature_b64) {
Ok(s) => s,
Err(_) => return Err(SigSocketError::DecodingError),
};
// 8. Verify the signature
if self.verify_signature(&signature_bytes, &message_bytes, public_key) {
Ok((message_bytes, signature_bytes))
} else {
Err(SigSocketError::InvalidSignature)
}
},
Ok(Err(_)) => Err(SigSocketError::ChannelClosed),
Err(_) => Err(SigSocketError::Timeout),
}
} else {
Err(SigSocketError::ConnectionNotFound)
}
}
}
```
## 8. Error Handling
We'll define a comprehensive error type for the SigSocket service:
```rust
#[derive(Debug, thiserror::Error)]
pub enum SigSocketError {
#[error("Connection not found for the provided public key")]
ConnectionNotFound,
#[error("Timeout waiting for signature")]
Timeout,
#[error("Invalid signature")]
InvalidSignature,
#[error("Channel closed unexpectedly")]
ChannelClosed,
#[error("Invalid response format, expected 'message.signature'")]
InvalidResponseFormat,
#[error("Error decoding base64 message or signature")]
DecodingError,
#[error("Invalid public key format")]
InvalidPublicKey,
#[error("Invalid signature format")]
InvalidSignature,
#[error("Internal cryptographic error")]
InternalError,
#[error("WebSocket error: {0}")]
WebSocketError(#[from] ws::ProtocolError),
#[error("Base64 decoding error: {0}")]
Base64Error(#[from] base64::DecodeError),
#[error("Hex decoding error: {0}")]
HexError(#[from] hex::FromHexError),
}
```
## 9. Cryptographic Operations
The SigSocket service uses the secp256k1 elliptic curve for cryptographic operations, which is the same curve used in Bitcoin and Ethereum. This makes it compatible with many blockchain applications and wallets.
```rust
use secp256k1::{Secp256k1, Message, PublicKey, Signature};
use sha2::{Sha256, Digest};
pub fn verify_signature(public_key_hex: &str, message: &[u8], signature_hex: &str) -> Result<bool, SigSocketError> {
// 1. Parse the public key
let public_key_bytes = hex::decode(public_key_hex)
.map_err(|_| SigSocketError::InvalidPublicKey)?;
let public_key = PublicKey::from_slice(&public_key_bytes)
.map_err(|_| SigSocketError::InvalidPublicKey)?;
// 2. Parse the signature
let signature_bytes = hex::decode(signature_hex)
.map_err(|_| SigSocketError::InvalidSignature)?;
let signature = Signature::from_compact(&signature_bytes)
.map_err(|_| SigSocketError::InvalidSignature)?;
// 3. Hash the message (secp256k1 requires a 32-byte hash)
let mut hasher = Sha256::new();
hasher.update(message);
let message_hash = hasher.finalize();
// 4. Create a secp256k1 message from the hash
let secp_message = Message::from_slice(&message_hash)
.map_err(|_| SigSocketError::InternalError)?;
// 5. Verify the signature
let secp = Secp256k1::verification_only();
match secp.verify(&secp_message, &signature, &public_key) {
Ok(_) => Ok(true),
Err(_) => Ok(false),
}
}
```
This implementation:
1. Decodes the hex-encoded public key and signature
2. Hashes the message using SHA-256 (required for secp256k1)
3. Verifies the signature using the secp256k1 library
4. Returns a boolean indicating whether the signature is valid
## 10. Security Considerations
1. **Public Key Validation**: Validate public keys upon connection to ensure they are properly formatted secp256k1 keys
2. **Message Authentication**: Consider adding a nonce or timestamp to prevent replay attacks
3. **Rate Limiting**: Implement rate limiting to prevent DoS attacks
4. **Connection Timeouts**: Automatically close inactive connections
5. **Error Logging**: Log errors but avoid exposing sensitive information
6. **Input Validation**: Validate all inputs to prevent injection attacks
7. **Secure Hashing**: Always hash messages before signing to prevent length extension attacks
## 11. Testing Strategy
1. **Unit Tests**: Test individual components in isolation
2. **Integration Tests**: Test the interaction between components
3. **End-to-End Tests**: Test the complete flow from client connection to signature verification
4. **Load Tests**: Test the system under high load to ensure stability
5. **Security Tests**: Test for common security vulnerabilities
## 12. Integration with Existing Code
The SigSocketService can be used by any part of the application that needs signing functionality:
```rust
// Create and initialize the service
let registry = Arc::new(RwLock::new(ConnectionRegistry::new()));
let sigsocket_service = Arc::new(SigSocketService::new(registry.clone()));
// Use the service to send a message for signing
async fn sign_message(service: Arc<SigSocketService>, public_key: String, message: Vec<u8>) -> Result<(Vec<u8>, Vec<u8>), SigSocketError> {
service.send_to_sign(&public_key, &message).await
}
// Example usage in an HTTP handler
async fn handle_sign_request(
service: web::Data<Arc<SigSocketService>>,
req: web::Json<SignRequest>,
) -> HttpResponse {
match service.send_to_sign(&req.public_key, &req.message).await {
Ok((response, signature)) => {
HttpResponse::Ok().json(json!({
"response": base64::encode(response),
"signature": base64::encode(signature),
}))
},
Err(e) => {
HttpResponse::InternalServerError().json(json!({
"error": e.to_string(),
}))
}
}
}
```
## 13. Deployment Considerations
1. **Scalability**: The SigSocket server should be designed to scale horizontally
2. **Monitoring**: Implement metrics for connection count, message throughput, and error rates
3. **Logging**: Log important events for debugging and auditing
4. **Documentation**: Document the API and protocol for client implementers

View File

@@ -0,0 +1,365 @@
# Ternary Search Tree (TST) Implementation Plan
## 1. Overview
A Ternary Search Tree (TST) is a type of trie where each node has three children: left, middle, and right. Unlike a RadixTree which compresses common prefixes, a TST stores one character per node and uses a binary search tree-like structure for efficient traversal.
```mermaid
graph TD
A[Root Node 'r'] --> B[Left Child 'a']
A --> C[Middle Child 'o']
A --> D[Right Child 't']
C --> E[Middle Child 'o']
E --> F[Middle Child 'm' - End of Key]
E --> G[Middle Child 't' - End of Key]
```
The TST implementation will use OurDB as the backend for persistent storage, similar to the existing RadixTree implementation. The goal is to provide a more balanced tree structure that offers consistent performance across all operations (set, get, delete, list).
## 2. Core Data Structures
### 2.1 TST Node Structure
```rust
pub struct TSTNode {
// The character stored at this node
pub character: char,
// Value stored at this node (empty if not end of key)
pub value: Vec<u8>,
// Whether this node represents the end of a key
pub is_end_of_key: bool,
// References to child nodes
pub left_id: Option<u32>, // For characters < current character
pub middle_id: Option<u32>, // For characters == current character (next character in key)
pub right_id: Option<u32>, // For characters > current character
}
```
### 2.2 TST Structure
```rust
pub struct TST {
// Database for persistent storage
db: OurDB,
// Database ID of the root node
root_id: Option<u32>,
}
```
## 3. API Design
The TST will maintain similar core functionality to RadixTree but with an API that better suits its structure:
```rust
impl TST {
// Creates a new TST with the specified database path
pub fn new(path: &str, reset: bool) -> Result<Self, Error>;
// Sets a key-value pair in the tree
pub fn set(&mut self, key: &str, value: Vec<u8>) -> Result<(), Error>;
// Gets a value by key from the tree
pub fn get(&mut self, key: &str) -> Result<Vec<u8>, Error>;
// Deletes a key from the tree
pub fn delete(&mut self, key: &str) -> Result<(), Error>;
// Lists all keys with a given prefix
pub fn list(&mut self, prefix: &str) -> Result<Vec<String>, Error>;
// Gets all values for keys with a given prefix
pub fn getall(&mut self, prefix: &str) -> Result<Vec<Vec<u8>>, Error>;
}
```
## 4. Implementation Strategy
### 4.1 Phase 1: Core Data Structures and Serialization
```mermaid
graph TD
A[Define TSTNode and TST structs] --> B[Implement serialization/deserialization]
B --> C[Implement Error handling]
C --> D[Implement OurDB integration]
```
1. Define the `TSTNode` and `TST` structs
2. Implement serialization and deserialization for `TSTNode`
3. Define error types for TST-specific errors
4. Implement OurDB integration for node storage and retrieval
### 4.2 Phase 2: Basic Tree Operations
```mermaid
graph TD
A[Implement new] --> B[Implement set]
B --> C[Implement get]
C --> D[Implement helper functions]
```
1. Implement the `new()` function for creating a new TST
2. Implement the `set()` function for inserting key-value pairs
3. Implement the `get()` function for retrieving values
4. Implement helper functions for node traversal and manipulation
### 4.3 Phase 3: Advanced Tree Operations
```mermaid
graph TD
A[Implement delete] --> B[Implement list]
B --> C[Implement getall]
C --> D[Optimize operations]
```
1. Implement the `delete()` function for removing keys
2. Implement the `list()` function for prefix-based key listing
3. Implement the `getall()` function for retrieving all values with a prefix
4. Optimize operations for balanced performance
### 4.4 Phase 4: Testing and Performance Evaluation
```mermaid
graph TD
A[Create unit tests] --> B[Create integration tests]
B --> C[Create performance tests]
C --> D[Compare with RadixTree]
D --> E[Optimize based on results]
```
1. Create unit tests for each component
2. Create integration tests for the complete system
3. Create performance tests similar to RadixTree's
4. Compare performance with RadixTree
5. Optimize based on performance results
## 5. Implementation Details
### 5.1 Node Structure and Storage
Each TST node will store a single character and have three child pointers (left, middle, right). The nodes will be serialized and stored in OurDB, with node references using OurDB record IDs.
### 5.2 Key Operations
#### 5.2.1 Insertion (set)
```mermaid
graph TD
A[Start at root] --> B{Root exists?}
B -- No --> C[Create root node]
B -- Yes --> D[Compare current char with node char]
D -- Less than --> E[Go to left child]
D -- Equal to --> F[Go to middle child]
D -- Greater than --> G[Go to right child]
E --> H{Child exists?}
F --> H
G --> H
H -- No --> I[Create new node]
H -- Yes --> J[Continue with next char]
I --> J
J --> K{End of key?}
K -- Yes --> L[Set value and mark as end of key]
K -- No --> D
```
1. Start at the root node
2. For each character in the key:
- If the character is less than the current node's character, go to the left child
- If the character is equal to the current node's character, go to the middle child
- If the character is greater than the current node's character, go to the right child
- If the child doesn't exist, create a new node
3. When the end of the key is reached, set the value and mark the node as end of key
#### 5.2.2 Lookup (get)
1. Start at the root node
2. For each character in the key:
- If the character is less than the current node's character, go to the left child
- If the character is equal to the current node's character, go to the middle child
- If the character is greater than the current node's character, go to the right child
- If the child doesn't exist, the key is not found
3. When the end of the key is reached, check if the node is marked as end of key
- If yes, return the value
- If no, the key is not found
#### 5.2.3 Deletion (delete)
1. Find the node corresponding to the end of the key
2. If the node has no children, remove it and update its parent
3. If the node has children, mark it as not end of key and clear its value
4. Recursively clean up any nodes that are no longer needed
#### 5.2.4 Prefix Operations (list, getall)
1. Find the node corresponding to the end of the prefix
2. Perform a traversal of the subtree rooted at that node
3. Collect all keys (for list) or values (for getall) from nodes marked as end of key
### 5.3 Serialization and OurDB Integration
#### 5.3.1 Node Structure for Serialization
Each TSTNode will be serialized with the following logical structure:
1. Version marker (for future format evolution)
2. Character data
3. Is-end-of-key flag
4. Value (if is-end-of-key is true)
5. Child node references (left, middle, right)
#### 5.3.2 OurDB Integration
The TST will use OurDB for node storage and retrieval:
1. **Node Storage**: Each node will be serialized and stored as a record in OurDB.
```rust
fn save_node(&mut self, node_id: Option<u32>, node: &TSTNode) -> Result<u32, Error> {
let data = node.serialize();
let args = OurDBSetArgs {
id: node_id,
data: &data,
};
Ok(self.db.set(args)?)
}
```
2. **Node Retrieval**: Nodes will be retrieved from OurDB and deserialized.
```rust
fn get_node(&mut self, node_id: u32) -> Result<TSTNode, Error> {
let data = self.db.get(node_id)?;
TSTNode::deserialize(&data)
}
```
3. **Root Node Management**: The TST will maintain a root node ID for traversal.
#### 5.3.3 Handling Large Datasets
For large datasets, we'll implement a batching approach similar to the RadixTree's large-scale tests:
1. **Batch Processing**: Process large datasets in manageable batches to avoid OurDB size limitations.
2. **Database Partitioning**: Create separate database instances for very large datasets.
3. **Memory Management**: Implement efficient memory usage patterns to avoid excessive memory consumption.
## 6. Project Structure
```
tst/
├── Cargo.toml
├── src/
│ ├── lib.rs # Public API and re-exports
│ ├── node.rs # TSTNode implementation
│ ├── serialize.rs # Serialization and deserialization
│ ├── error.rs # Error types
│ └── operations.rs # Tree operations implementation
├── tests/
│ ├── basic_test.rs # Basic operations tests
│ ├── prefix_test.rs # Prefix operations tests
│ └── edge_cases.rs # Edge case tests
└── examples/
├── basic_usage.rs # Basic usage example
├── prefix_ops.rs # Prefix operations example
└── performance.rs # Performance benchmark
```
## 7. Performance Considerations
### 7.1 Advantages of TST over RadixTree
1. **Balanced Structure**: TST naturally maintains a more balanced structure, which can lead to more consistent performance across operations.
2. **Character-by-Character Comparison**: TST performs character-by-character comparisons, which can be more efficient for certain workloads.
3. **Efficient Prefix Operations**: TST can efficiently handle prefix operations by traversing the middle child path.
### 7.2 Potential Optimizations
1. **Node Caching**: Cache frequently accessed nodes to reduce database operations.
2. **Balancing Techniques**: Implement balancing techniques to ensure the tree remains balanced.
3. **Batch Operations**: Support batch operations for improved performance.
4. **Memory Management**: Implement efficient memory usage patterns to avoid excessive memory consumption.
## 8. Testing Strategy
### 8.1 Unit Tests
1. Test `TSTNode` serialization/deserialization
2. Test character comparison operations
3. Test error handling
### 8.2 Integration Tests
1. Test basic CRUD operations
2. Test prefix operations
3. Test edge cases (empty keys, very long keys, etc.)
4. Test with large datasets
### 8.3 Performance Tests
1. Measure throughput for set/get operations
2. Measure latency for different operations
3. Test with different tree sizes and key distributions
4. Compare performance with RadixTree
#### 8.3.1 Performance Benchmarking
We'll create comprehensive benchmarks to compare the TST implementation with RadixTree:
```rust
// Example benchmark structure
fn benchmark_set_operations(tree_type: &str, num_records: usize) -> Duration {
let start_time = Instant::now();
// Create tree (TST or RadixTree)
let mut tree = match tree_type {
"tst" => create_tst(),
"radix" => create_radix_tree(),
_ => panic!("Unknown tree type"),
};
// Insert records
for i in 0..num_records {
let key = format!("key:{:08}", i);
let value = format!("val{}", i).into_bytes();
tree.set(&key, value).unwrap();
}
start_time.elapsed()
}
```
We'll benchmark the following operations:
- Set (insertion)
- Get (lookup)
- Delete
- List (prefix search)
- GetAll (prefix values)
For each operation, we'll measure:
- Throughput (operations per second)
- Latency (time per operation)
- Memory usage
- Database size
We'll test with various dataset characteristics:
- Small datasets (100-1,000 keys)
- Medium datasets (10,000-100,000 keys)
- Large datasets (1,000,000+ keys)
- Keys with common prefixes
- Keys with random distribution
- Long keys vs. short keys
## 9. Timeline and Milestones
1. **Week 1**: Core data structures and serialization
2. **Week 2**: Basic tree operations
3. **Week 3**: Advanced tree operations
4. **Week 4**: Testing and performance evaluation
5. **Week 5**: Optimization and documentation
## 10. Conclusion
This implementation plan provides a roadmap for creating a Ternary Search Tree (TST) as an alternative to the RadixTree implementation. The TST will maintain the same core functionality while providing a more balanced tree structure and aiming for balanced performance across all operations.
The implementation will leverage OurDB for persistent storage, similar to RadixTree, but with a different node structure and traversal algorithm that better suits the TST approach.

View File

@@ -0,0 +1,451 @@
# TST Integration Plan for HeroDB
## Overview
This document outlines the plan for adding generic functionality to the `herodb/src/db` module to use the Ternary Search Tree (TST) for storing objects with prefixed IDs and implementing a generic list function to retrieve all objects with a specific prefix.
## Current Architecture
Currently:
- Each model has a `db_prefix()` method that returns a string prefix (e.g., "vote" for Vote objects)
- Objects are stored in OurDB with numeric IDs
- The `list()` method in `OurDbStore` is not implemented
## Implementation Plan
### 1. Create a TST-based Index Manager (herodb/src/db/tst_index.rs)
Create a new module that manages TST instances for different model prefixes:
```rust
use crate::db::error::{DbError, DbResult};
use std::path::{Path, PathBuf};
use tst::TST;
/// Manages TST-based indexes for model objects
pub struct TSTIndexManager {
/// Base path for TST databases
base_path: PathBuf,
/// Map of model prefixes to their TST instances
tst_instances: std::collections::HashMap<String, TST>,
}
impl TSTIndexManager {
/// Creates a new TST index manager
pub fn new<P: AsRef<Path>>(base_path: P) -> DbResult<Self> {
let base_path = base_path.as_ref().to_path_buf();
// Create directory if it doesn't exist
std::fs::create_dir_all(&base_path).map_err(DbError::IoError)?;
Ok(Self {
base_path,
tst_instances: std::collections::HashMap::new(),
})
}
/// Gets or creates a TST instance for a model prefix
pub fn get_tst(&mut self, prefix: &str) -> DbResult<&mut TST> {
if !self.tst_instances.contains_key(prefix) {
// Create a new TST instance for this prefix
let tst_path = self.base_path.join(format!("{}_tst", prefix));
let tst_path_str = tst_path.to_string_lossy().to_string();
// Create the TST
let tst = TST::new(&tst_path_str, false)
.map_err(|e| DbError::GeneralError(format!("TST error: {:?}", e)))?;
// Insert it into the map
self.tst_instances.insert(prefix.to_string(), tst);
}
// Return a mutable reference to the TST
Ok(self.tst_instances.get_mut(prefix).unwrap())
}
/// Adds or updates an object in the TST index
pub fn set(&mut self, prefix: &str, id: u32, data: Vec<u8>) -> DbResult<()> {
// Get the TST for this prefix
let tst = self.get_tst(prefix)?;
// Create the key in the format prefix_id
let key = format!("{}_{}", prefix, id);
// Set the key-value pair in the TST
tst.set(&key, data)
.map_err(|e| DbError::GeneralError(format!("TST error: {:?}", e)))?;
Ok(())
}
/// Removes an object from the TST index
pub fn delete(&mut self, prefix: &str, id: u32) -> DbResult<()> {
// Get the TST for this prefix
let tst = self.get_tst(prefix)?;
// Create the key in the format prefix_id
let key = format!("{}_{}", prefix, id);
// Delete the key from the TST
tst.delete(&key)
.map_err(|e| DbError::GeneralError(format!("TST error: {:?}", e)))?;
Ok(())
}
/// Lists all objects with a given prefix
pub fn list(&mut self, prefix: &str) -> DbResult<Vec<(u32, Vec<u8>)>> {
// Get the TST for this prefix
let tst = self.get_tst(prefix)?;
// Get all keys with this prefix
let keys = tst.list(prefix)
.map_err(|e| DbError::GeneralError(format!("TST error: {:?}", e)))?;
// Get all values for these keys
let mut result = Vec::with_capacity(keys.len());
for key in keys {
// Extract the ID from the key (format: prefix_id)
let id_str = key.split('_').nth(1).ok_or_else(|| {
DbError::GeneralError(format!("Invalid key format: {}", key))
})?;
let id = id_str.parse::<u32>().map_err(|_| {
DbError::GeneralError(format!("Invalid ID in key: {}", key))
})?;
// Get the value from the TST
let data = tst.get(&key)
.map_err(|e| DbError::GeneralError(format!("TST error: {:?}", e)))?;
result.push((id, data));
}
Ok(result)
}
}
```
### 2. Update DB Module (herodb/src/db/mod.rs)
Add the new module to the db module:
```rust
pub mod db;
pub mod error;
pub mod macros;
pub mod model;
pub mod model_methods;
pub mod store;
pub mod tst_index; // Add the new module
pub use db::DB;
pub use db::DBBuilder;
pub use error::{DbError, DbResult};
pub use model::Model;
pub use model::Storable;
```
### 3. Modify DB Struct (herodb/src/db/db.rs)
Update the DB struct to include the TST index manager:
```rust
/// Main DB manager that automatically handles all models
#[derive(Clone, CustomType)]
pub struct DB {
db_path: PathBuf,
// Type map for generic operations
type_map: HashMap<TypeId, Arc<RwLock<dyn DbOperations>>>,
// TST index manager
tst_index: Arc<RwLock<TSTIndexManager>>,
// Transaction state
transaction: Arc<RwLock<Option<TransactionState>>>,
}
```
### 4. Extend Transaction Handling
Extend the `DbOperation` enum to include model prefix and ID information:
```rust
#[derive(Debug, Clone)]
enum DbOperation {
Set {
model_type: TypeId,
serialized: Vec<u8>,
model_prefix: String, // Add model prefix
model_id: u32, // Add model ID
},
Delete {
model_type: TypeId,
id: u32,
model_prefix: String, // Add model prefix
},
}
```
### 5. Update Transaction Recording
Modify the `set` and `delete` methods to record model prefix and ID in the transaction:
```rust
pub fn set<T: Model>(&self, model: &T) -> DbResult<()> {
// Try to acquire a write lock on the transaction
let mut tx_guard = self.transaction.write().unwrap();
// Check if there's an active transaction
if let Some(tx_state) = tx_guard.as_mut() {
if tx_state.active {
// Serialize the model for later use
let serialized = model.to_bytes()?;
// Record a Set operation in the transaction with prefix and ID
tx_state.operations.push(DbOperation::Set {
model_type: TypeId::of::<T>(),
serialized: serialized.clone(),
model_prefix: T::db_prefix().to_string(),
model_id: model.get_id(),
});
return Ok(());
}
}
// ... rest of the method ...
}
pub fn delete<T: Model>(&self, id: u32) -> DbResult<()> {
// Try to acquire a write lock on the transaction
let mut tx_guard = self.transaction.write().unwrap();
// Check if there's an active transaction
if let Some(tx_state) = tx_guard.as_mut() {
if tx_state.active {
// Record a Delete operation in the transaction with prefix
tx_state.operations.push(DbOperation::Delete {
model_type: TypeId::of::<T>(),
id,
model_prefix: T::db_prefix().to_string(),
});
return Ok(());
}
}
// ... rest of the method ...
}
```
### 6. Update Transaction Commit
Modify the `commit_transaction` method to update both OurDB and the TST index:
```rust
pub fn commit_transaction(&self) -> DbResult<()> {
let mut tx_guard = self.transaction.write().unwrap();
if let Some(tx_state) = tx_guard.take() {
if !tx_state.active {
return Err(DbError::TransactionError("Transaction not active".into()));
}
// Create a backup of the transaction state in case we need to rollback
let backup = tx_state.clone();
// Try to execute all operations
let result = (|| {
for op in &tx_state.operations {
match op {
DbOperation::Set {
model_type,
serialized,
model_prefix,
model_id,
} => {
// Apply to OurDB
self.apply_set_operation(*model_type, serialized)?;
// Apply to TST index
let mut tst_index = self.tst_index.write().unwrap();
tst_index.set(model_prefix, *model_id, serialized.clone())?;
}
DbOperation::Delete {
model_type,
id,
model_prefix,
} => {
// Apply to OurDB
let db_ops = self
.type_map
.get(model_type)
.ok_or_else(|| DbError::TypeError)?;
let mut db_ops_guard = db_ops.write().unwrap();
db_ops_guard.delete(*id)?;
// Apply to TST index
let mut tst_index = self.tst_index.write().unwrap();
tst_index.delete(model_prefix, *id)?;
}
}
}
Ok(())
})();
// If any operation failed, restore the transaction state
if result.is_err() {
*tx_guard = Some(backup);
return result;
}
Ok(())
} else {
Err(DbError::TransactionError("No active transaction".into()))
}
}
```
### 7. Implement List Method
Implement the `list` method to use the TST's prefix search:
```rust
pub fn list<T: Model>(&self) -> DbResult<Vec<T>> {
// Get the prefix for this model type
let prefix = T::db_prefix();
// Use the TST index to get all objects with this prefix
let mut tst_index = self.tst_index.write().unwrap();
let items = tst_index.list(prefix)?;
// Deserialize the objects
let mut result = Vec::with_capacity(items.len());
for (_, data) in items {
let model = T::from_bytes(&data)?;
result.push(model);
}
Ok(result)
}
```
### 8. Add Recovery Mechanism
Add a method to synchronize the TST index with OurDB in case they get out of sync:
```rust
pub fn synchronize_tst_index<T: Model>(&self) -> DbResult<()> {
// Get all models from OurDB
let models = self.list_from_ourdb::<T>()?;
// Clear the TST index for this model type
let mut tst_index = self.tst_index.write().unwrap();
let prefix = T::db_prefix();
// Rebuild the TST index
for model in models {
let id = model.get_id();
let data = model.to_bytes()?;
tst_index.set(prefix, id, data)?;
}
Ok(())
}
// Helper method to list models directly from OurDB (not using TST)
fn list_from_ourdb<T: Model>(&self) -> DbResult<Vec<T>> {
match self.type_map.get(&TypeId::of::<T>()) {
Some(db_ops) => {
let db_ops_guard = db_ops.read().unwrap();
let result_any = db_ops_guard.list()?;
match result_any.downcast::<Vec<T>>() {
Ok(vec_t) => Ok(*vec_t),
Err(_) => Err(DbError::TypeError),
}
}
None => Err(DbError::TypeError),
}
}
```
## Implementation Flow
```mermaid
sequenceDiagram
participant Client
participant DB
participant TransactionState
participant OurDbStore
participant TSTIndexManager
participant TST
Client->>DB: begin_transaction()
DB->>TransactionState: create new transaction
Client->>DB: set(model)
DB->>TransactionState: record Set operation with prefix and ID
Client->>DB: delete(model)
DB->>TransactionState: record Delete operation with prefix and ID
Client->>DB: commit_transaction()
DB->>TransactionState: get all operations
loop For each operation
alt Set operation
DB->>OurDbStore: apply_set_operation()
DB->>TSTIndexManager: set(prefix, id, data)
TSTIndexManager->>TST: set(key, data)
else Delete operation
DB->>OurDbStore: delete(id)
DB->>TSTIndexManager: delete(prefix, id)
TSTIndexManager->>TST: delete(key)
end
end
alt Success
DB-->>Client: Ok(())
else Error
DB->>TransactionState: restore transaction state
DB-->>Client: Err(error)
end
Client->>DB: list<T>()
DB->>TSTIndexManager: list(prefix)
TSTIndexManager->>TST: list(prefix)
TST-->>TSTIndexManager: keys
TSTIndexManager->>TST: get(key) for each key
TST-->>TSTIndexManager: data
TSTIndexManager-->>DB: (id, data) pairs
DB->>DB: deserialize data to models
DB-->>Client: Vec<T>
```
## Testing Strategy
1. Create unit tests for the TST index manager
2. Test the list functionality with different model types
3. Test transaction handling (commit and rollback)
4. Test error recovery mechanisms
5. Test edge cases (empty database, large datasets)
## Implementation Steps
1. Add TST dependency to herodb/Cargo.toml
2. Create the tst_index.rs module
3. Update the DB module to include the TST index manager
4. Extend the transaction handling
5. Implement the list method
6. Add tests for the new functionality
7. Update documentation
## Considerations
1. **Performance**: The TST operations add overhead to insert/delete operations, but provide efficient list functionality.
2. **Consistency**: The enhanced transaction handling ensures consistency between OurDB and the TST index.
3. **Error Handling**: Proper error handling and recovery mechanisms are essential for maintaining data integrity.
4. **Backward Compatibility**: The implementation should maintain backward compatibility with existing code.

View File

@@ -1,7 +1,7 @@
use chrono::{Duration, Utc};
use heromodels::db::{Collection, Db};
use heromodels::models::finance::marketplace::{Bid, Listing, ListingType};
use heromodels::models::finance::asset::AssetType;
use heromodels::models::finance::marketplace::{Bid, Listing, ListingType};
use heromodels_core::Model;
// Helper function to print listing details
@@ -16,32 +16,32 @@ fn print_listing_details(listing: &Listing) {
println!("Price: {} {}", listing.price, listing.currency);
println!("Listing Type: {:?}", listing.listing_type);
println!("Status: {:?}", listing.status);
if let Some(expires_at) = listing.expires_at {
println!("Expires At: {}", expires_at);
} else {
println!("Expires At: Never");
}
if let Some(sold_at) = listing.sold_at {
println!("Sold At: {}", sold_at);
}
if let Some(buyer_id) = &listing.buyer_id {
println!("Buyer ID: {}", buyer_id);
}
if let Some(sale_price) = listing.sale_price {
println!("Sale Price: {} {}", sale_price, listing.currency);
}
println!("Bids: {}", listing.bids.len());
println!("Tags: {:?}", listing.tags);
if let Some(image_url) = &listing.image_url {
println!("Image URL: {}", image_url);
}
println!("Created At: {}", listing.base_data.created_at);
println!("Modified At: {}", listing.base_data.modified_at);
}
@@ -77,7 +77,11 @@ fn main() {
"USD",
ListingType::FixedPrice,
Some(Utc::now() + Duration::days(30)), // Expires in 30 days
vec!["music".to_string(), "instrument".to_string(), "vintage".to_string()],
vec![
"music".to_string(),
"instrument".to_string(),
"vintage".to_string(),
],
Some("https://example.com/images/vintage_guitar.jpg"),
);
@@ -93,7 +97,11 @@ fn main() {
"USD",
ListingType::Auction,
Some(Utc::now() + Duration::days(7)), // Auction ends in 7 days
vec!["art".to_string(), "painting".to_string(), "antique".to_string()],
vec![
"art".to_string(),
"painting".to_string(),
"antique".to_string(),
],
Some("https://example.com/images/rare_painting.jpg"),
);
@@ -114,9 +122,21 @@ fn main() {
);
// Save all listings to database and get their assigned IDs and updated models
let (fixed_price_id, db_fixed_price) = db.collection().expect("can open listing collection").set(&fixed_price_listing).expect("can set listing");
let (auction_id, db_auction) = db.collection().expect("can open listing collection").set(&auction_listing).expect("can set listing");
let (exchange_id, db_exchange) = db.collection().expect("can open listing collection").set(&exchange_listing).expect("can set listing");
let (fixed_price_id, db_fixed_price) = db
.collection()
.expect("can open listing collection")
.set(&fixed_price_listing)
.expect("can set listing");
let (auction_id, db_auction) = db
.collection()
.expect("can open listing collection")
.set(&auction_listing)
.expect("can set listing");
let (exchange_id, db_exchange) = db
.collection()
.expect("can open listing collection")
.set(&exchange_listing)
.expect("can set listing");
println!("Fixed Price Listing assigned ID: {}", fixed_price_id);
println!("Auction Listing assigned ID: {}", auction_id);
@@ -138,17 +158,13 @@ fn main() {
// Create bids for the auction listing
let bid1 = Bid::new(
auction_id,
101, // Bidder ID
5200.0,
"USD",
auction_id, 101, // Bidder ID
5200.0, "USD",
);
let bid2 = Bid::new(
auction_id,
102, // Bidder ID
5500.0,
"USD",
auction_id, 102, // Bidder ID
5500.0, "USD",
);
// Print the bids
@@ -166,7 +182,8 @@ fn main() {
.expect("can add second bid");
// Save the updated auction listing
let (_, db_updated_auction) = db.collection()
let (_, db_updated_auction) = db
.collection()
.expect("can open listing collection")
.set(&updated_auction)
.expect("can set updated auction");
@@ -189,7 +206,8 @@ fn main() {
.expect("can complete sale");
// Save the updated listing
let (_, db_completed_fixed_price) = db.collection()
let (_, db_completed_fixed_price) = db
.collection()
.expect("can open listing collection")
.set(&completed_fixed_price)
.expect("can set completed listing");
@@ -204,14 +222,15 @@ fn main() {
// Store the bidder_id and amount before moving db_updated_auction
let bidder_id = db_updated_auction.highest_bid().unwrap().bidder_id;
let amount = db_updated_auction.highest_bid().unwrap().amount;
// Now complete the sale
let completed_auction = db_updated_auction
.complete_sale(bidder_id.to_string(), amount)
.expect("can complete auction");
// Save the updated auction listing
let (_, db_completed_auction) = db.collection()
let (_, db_completed_auction) = db
.collection()
.expect("can open listing collection")
.set(&completed_auction)
.expect("can set completed auction");
@@ -223,12 +242,11 @@ fn main() {
println!("\n--- Cancelling a Listing ---");
// Cancel the exchange listing
let cancelled_exchange = db_exchange
.cancel()
.expect("can cancel listing");
let cancelled_exchange = db_exchange.cancel().expect("can cancel listing");
// Save the updated listing
let (_, db_cancelled_exchange) = db.collection()
let (_, db_cancelled_exchange) = db
.collection()
.expect("can open listing collection")
.set(&cancelled_exchange)
.expect("can set cancelled listing");
@@ -256,7 +274,8 @@ fn main() {
);
// Save the expired listing
let (expired_id, db_expired) = db.collection()
let (expired_id, db_expired) = db
.collection()
.expect("can open listing collection")
.set(&expired_listing)
.expect("can set expired listing");
@@ -267,7 +286,8 @@ fn main() {
let checked_expired = db_expired.check_expiration();
// Save the checked listing
let (_, db_checked_expired) = db.collection()
let (_, db_checked_expired) = db
.collection()
.expect("can open listing collection")
.set(&checked_expired)
.expect("can set checked listing");
@@ -295,7 +315,8 @@ fn main() {
);
// Save the listing
let (update_id, db_to_update) = db.collection()
let (update_id, db_to_update) = db
.collection()
.expect("can open listing collection")
.set(&listing_to_update)
.expect("can set listing to update");
@@ -315,7 +336,8 @@ fn main() {
.add_tags(vec!["updated".to_string(), "premium".to_string()]);
// Save the updated listing
let (_, db_updated_listing) = db.collection()
let (_, db_updated_listing) = db
.collection()
.expect("can open listing collection")
.set(&updated_listing)
.expect("can set updated listing");

View File

@@ -19,7 +19,7 @@ fn main() {
"Test User".to_string(),
"test@example.com".to_string(),
);
println!(" Signer created: {}", signer.name);
println!(" Last reminder: {:?}", signer.last_reminder_mail_sent_at);
assert_eq!(signer.last_reminder_mail_sent_at, None);
@@ -44,7 +44,10 @@ fn main() {
println!("Test 4: Mark reminder as sent");
signer.mark_reminder_sent(current_time);
println!(" Reminder marked as sent at: {}", current_time);
println!(" Last reminder timestamp: {:?}", signer.last_reminder_mail_sent_at);
println!(
" Last reminder timestamp: {:?}",
signer.last_reminder_mail_sent_at
);
assert_eq!(signer.last_reminder_mail_sent_at, Some(current_time));
println!(" ✓ Reminder timestamp updated correctly\n");
@@ -86,9 +89,15 @@ fn main() {
.comments("Test signer with reminder");
println!(" Signer: {}", signer_with_reminder.name);
println!(" Last reminder: {:?}", signer_with_reminder.last_reminder_mail_sent_at);
println!(" Can send reminder: {}", signer_with_reminder.can_send_reminder(current_time));
println!(
" Last reminder: {:?}",
signer_with_reminder.last_reminder_mail_sent_at
);
println!(
" Can send reminder: {}",
signer_with_reminder.can_send_reminder(current_time)
);
let remaining = signer_with_reminder.reminder_cooldown_remaining(current_time);
println!(" Cooldown remaining: {:?} seconds", remaining);
assert_eq!(remaining, Some(10 * 60)); // 10 minutes remaining
@@ -97,8 +106,14 @@ fn main() {
// Test 9: Test clear reminder timestamp
println!("Test 9: Clear reminder timestamp");
let cleared_signer = signer_with_reminder.clear_last_reminder_mail_sent_at();
println!(" Last reminder after clear: {:?}", cleared_signer.last_reminder_mail_sent_at);
println!(" Can send reminder: {}", cleared_signer.can_send_reminder(current_time));
println!(
" Last reminder after clear: {:?}",
cleared_signer.last_reminder_mail_sent_at
);
println!(
" Can send reminder: {}",
cleared_signer.can_send_reminder(current_time)
);
assert_eq!(cleared_signer.last_reminder_mail_sent_at, None);
assert!(cleared_signer.can_send_reminder(current_time));
println!(" ✓ Clear reminder timestamp works correctly\n");

View File

@@ -11,7 +11,7 @@ fn main() {
"Test User".to_string(),
"test@example.com".to_string(),
);
println!(" Signer created: {}", signer.name);
println!(" Status: {:?}", signer.status);
println!(" Signature data: {:?}", signer.signature_data);
@@ -23,14 +23,17 @@ fn main() {
println!("Test 2: Sign with signature data");
let signature_data = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==".to_string();
let comments = "I agree to all terms and conditions.".to_string();
signer.sign(Some(signature_data.clone()), Some(comments.clone()));
println!(" Status after signing: {:?}", signer.status);
println!(" Signed at: {:?}", signer.signed_at);
println!(" Comments: {:?}", signer.comments);
println!(" Signature data length: {}", signer.signature_data.as_ref().unwrap().len());
println!(
" Signature data length: {}",
signer.signature_data.as_ref().unwrap().len()
);
assert_eq!(signer.status, SignerStatus::Signed);
assert!(signer.signed_at.is_some());
assert_eq!(signer.signature_data, Some(signature_data));
@@ -44,13 +47,16 @@ fn main() {
"Test User 2".to_string(),
"test2@example.com".to_string(),
);
signer2.sign(None, Some("Electronic signature without visual data".to_string()));
signer2.sign(
None,
Some("Electronic signature without visual data".to_string()),
);
println!(" Status: {:?}", signer2.status);
println!(" Signature data: {:?}", signer2.signature_data);
println!(" Comments: {:?}", signer2.comments);
assert_eq!(signer2.status, SignerStatus::Signed);
assert_eq!(signer2.signature_data, None);
assert!(signer2.comments.is_some());
@@ -63,13 +69,13 @@ fn main() {
"Test User 3".to_string(),
"test3@example.com".to_string(),
);
signer3.sign(None, None);
println!(" Status: {:?}", signer3.status);
println!(" Signature data: {:?}", signer3.signature_data);
println!(" Comments: {:?}", signer3.comments);
assert_eq!(signer3.status, SignerStatus::Signed);
assert_eq!(signer3.signature_data, None);
assert_eq!(signer3.comments, None);
@@ -88,16 +94,25 @@ fn main() {
println!(" Signer: {}", signer_with_signature.name);
println!(" Status: {:?}", signer_with_signature.status);
println!(" Signature data: {:?}", signer_with_signature.signature_data);
println!(
" Signature data: {:?}",
signer_with_signature.signature_data
);
println!(" Comments: {:?}", signer_with_signature.comments);
assert_eq!(signer_with_signature.signature_data, Some("data:image/png;base64,example".to_string()));
assert_eq!(
signer_with_signature.signature_data,
Some("data:image/png;base64,example".to_string())
);
println!(" ✓ Builder pattern with signature data works correctly\n");
// Test 6: Clear signature data
println!("Test 6: Clear signature data");
let cleared_signer = signer_with_signature.clear_signature_data();
println!(" Signature data after clear: {:?}", cleared_signer.signature_data);
println!(
" Signature data after clear: {:?}",
cleared_signer.signature_data
);
assert_eq!(cleared_signer.signature_data, None);
println!(" ✓ Clear signature data works correctly\n");
@@ -114,14 +129,24 @@ fn main() {
// Serialize to JSON
let json = serde_json::to_string(&original_signer).expect("Failed to serialize");
println!(" Serialized JSON length: {} characters", json.len());
// Deserialize from JSON
let deserialized_signer: ContractSigner = serde_json::from_str(&json).expect("Failed to deserialize");
println!(" Original signature data: {:?}", original_signer.signature_data);
println!(" Deserialized signature data: {:?}", deserialized_signer.signature_data);
assert_eq!(original_signer.signature_data, deserialized_signer.signature_data);
let deserialized_signer: ContractSigner =
serde_json::from_str(&json).expect("Failed to deserialize");
println!(
" Original signature data: {:?}",
original_signer.signature_data
);
println!(
" Deserialized signature data: {:?}",
deserialized_signer.signature_data
);
assert_eq!(
original_signer.signature_data,
deserialized_signer.signature_data
);
assert_eq!(original_signer.name, deserialized_signer.name);
assert_eq!(original_signer.email, deserialized_signer.email);
println!(" ✓ Serialization/Deserialization works correctly\n");
@@ -138,17 +163,21 @@ fn main() {
"comments": null,
"last_reminder_mail_sent_at": null
}"#;
let old_signer: ContractSigner = serde_json::from_str(old_json).expect("Failed to deserialize old format");
let old_signer: ContractSigner =
serde_json::from_str(old_json).expect("Failed to deserialize old format");
println!(" Old signer name: {}", old_signer.name);
println!(" Old signer signature data: {:?}", old_signer.signature_data);
println!(
" Old signer signature data: {:?}",
old_signer.signature_data
);
assert_eq!(old_signer.signature_data, None);
println!(" ✓ Backward compatibility works correctly\n");
println!("All tests passed! ✅");
println!("ContractSigner signature functionality is working correctly.");
// Summary
println!("\n📋 Summary of Features Tested:");
println!(" ✅ New signer creation (signature_data: None)");

Binary file not shown.

View File

@@ -1 +0,0 @@
74

Binary file not shown.

View File

@@ -323,16 +323,6 @@ where
assigned_id
};
// Always create a primary key index entry for this model type
// This ensures get_all() can find all objects of this type, even if they have no explicit indexed fields
let primary_index_key = format!("{}::primary", M::db_prefix());
let mut primary_ids: HashSet<u32> =
Self::get_tst_value(&mut index_db, &primary_index_key)?
.unwrap_or_else(HashSet::new);
primary_ids.insert(assigned_id);
let raw_primary_ids = bincode::serde::encode_to_vec(&primary_ids, BINCODE_CONFIG)?;
index_db.set(&primary_index_key, raw_primary_ids)?;
// Now add the new indices
for index_key in indices_to_add {
let key = Self::index_key(M::db_prefix(), index_key.name, &index_key.value);
@@ -430,22 +420,6 @@ where
}
}
// Also remove from the primary key index
let primary_index_key = format!("{}::primary", M::db_prefix());
if let Some(mut primary_ids) =
Self::get_tst_value::<HashSet<u32>>(&mut index_db, &primary_index_key)?
{
primary_ids.remove(&id);
if primary_ids.is_empty() {
// This was the last object of this type, remove the primary index entirely
index_db.delete(&primary_index_key)?;
} else {
// There are still other objects of this type, write back updated set
let raw_primary_ids = bincode::serde::encode_to_vec(&primary_ids, BINCODE_CONFIG)?;
index_db.set(&primary_index_key, raw_primary_ids)?;
}
}
// Finally delete the object itself
Ok(data_db.delete(id)?)
}
@@ -476,7 +450,18 @@ where
}
}
}
};
}
Err(tst::Error::PrefixNotFound(_)) => {
// No index entries found for this prefix, meaning no objects of this type exist.
// Note: tst::getall might return Ok(vec![]) in this case instead of PrefixNotFound.
// Depending on tst implementation, this arm might be redundant if getall returns empty vec.
return Ok(Vec::new());
}
Err(e) => {
// Other TST errors.
return Err(super::Error::DB(e));
}
}
let mut results: Vec<M> = Vec::with_capacity(all_object_ids.len());
for obj_id in all_object_ids {

View File

@@ -0,0 +1,65 @@
# Access Control Model
The `access` model provides a system for managing permissions, defining which users or groups can access specific resources (objects) within the application.
## `Access` Struct
The core of this module is the `Access` struct, which acts as an Access Control Entry (ACE). It creates a link between a resource and an entity being granted permission.
### Fields
- `base_data`: Standard `BaseModelData` for a unique ID and timestamps.
- `object_type`: A `String` identifying the type of the resource (e.g., `"Project"`, `"Document"`).
- `object_id`: The `u32` unique ID of the resource instance.
- `circle_pk`: The public key (`String`) of the user or entity being granted access.
- `contact_id`: The ID of a `Contact` being granted access.
- `group_id`: The ID of a `Group` being granted access.
- `expires_at`: An optional `u64` timestamp for when the access grant expires.
All key fields are indexed for efficient lookups.
## Core Functions
The module provides functions to check permissions based on the created `Access` records.
- `can_access_resource(db, public_key, object_id, object_type) -> bool`:
This is the primary function for permission checking. It determines if a user, identified by their `public_key`, can access a given object. The current logic is as follows:
1. It first checks if the `public_key` belongs to a member of a globally defined `Circle`. If so, access is granted (this acts as a super-admin or owner role).
2. If the user is not a global member, it queries for all `Access` records associated with the `object_id`.
3. It returns `true` if it finds any `Access` record where the `circle_pk` matches the user's `public_key`.
- `is_circle_member(db, public_key) -> bool`:
A helper function that checks if a user is part of the global `Circle`, effectively checking for super-admin privileges.
## Usage Example
Here is a conceptual walkthrough of how to grant and check access:
1. **A resource is created**, for example, a `Project` with ID `789`.
2. **To grant access** to a user with the public key `"pubkey_of_user_b"`, you create an `Access` record:
```rust
use heromodels::models::access::Access;
let access_grant = Access::new()
.object_type("Project".to_string())
.object_id(789)
.circle_pk("pubkey_of_user_b".to_string());
// This record would then be saved to the database.
```
3. **To check access**, when `user_b` attempts to view the project, the application would call `can_access_resource`:
```rust
// let can_access = can_access_resource(
// db_connection,
// "pubkey_of_user_b",
// 789,
// "Project"
// );
// assert!(can_access);
```
This system allows for flexible, object-by-object permission management.

View File

@@ -1,8 +1,8 @@
use std::sync::Arc;
use crate::db::{Collection, Db, hero::OurDB};
use crate::models::Circle;
use crate::db::{hero::OurDB, Collection, Db};
use heromodels_core::BaseModelData;
use heromodels_derive::model;
use std::sync::Arc;
// Temporarily removed to fix compilation issues
// use rhai_autobind_macros::rhai_model_export;
use rhai::{CustomType, TypeBuilder};
@@ -71,17 +71,16 @@ impl Access {
}
}
/// Checks if a caller has permission to access a specific resource.
/// Access is granted if the caller is a super admin or if an `Access` record exists
/// granting them `can_access = true` for the given resource type and ID.
///
///
/// # Arguments
/// * `db`: An `Arc<OurDB>` for database interaction.
/// * `public_key`: The public key of the caller.
/// * `_resource_id_to_check`: The ID of the resource being accessed (now unused).
/// * `_resource_type_to_check`: The type of the resource (e.g., "Collection", "Image") (now unused).
///
///
/// # Errors
/// Returns `Err(EvalAltResult::ErrorRuntime)` if there's a database error during the check.
pub fn can_access_resource(
@@ -94,7 +93,8 @@ pub fn can_access_resource(
.collection::<Circle>()
.expect("Failed to get Circle collection")
.get_all()
.unwrap()[0].clone();
.unwrap()[0]
.clone();
// Circle members can access everything
if circle.members.contains(&public_key.to_string()) {
@@ -121,18 +121,18 @@ pub fn can_access_resource(
println!("Access records: {:#?}", access_records);
// if circle_pk is in access records true
return access_records.iter().any(|record| record.circle_pk == public_key)
return access_records
.iter()
.any(|record| record.circle_pk == public_key);
}
pub fn is_circle_member(
db: Arc<OurDB>,
public_key: &str,
) -> bool {
pub fn is_circle_member(db: Arc<OurDB>, public_key: &str) -> bool {
let circle = db
.collection::<Circle>()
.expect("Failed to get Circle collection")
.get_all()
.unwrap()[0].clone();
.unwrap()[0]
.clone();
// Circle members can access everything
if circle.members.contains(&public_key.to_string()) {

View File

@@ -0,0 +1,58 @@
# Business Models (`biz`)
The `biz` module provides a suite of models for handling core business operations, including company management, product catalogs, sales, payments, and shareholder records.
## Core Models
### `Company`
The `Company` struct is the central model, representing a business entity.
- **Key Fields**: `name`, `registration_number`, `incorporation_date`, `address`, `business_type`, and `status`.
- **Enums**:
- `CompanyStatus`: Tracks the company's state (`PendingPayment`, `Active`, `Suspended`, `Inactive`).
- `BusinessType`: Categorizes the company (e.g., `Coop`, `Single`, `Global`).
- **Functionality**: Provides a foundation for linking other business models like products, sales, and shareholders.
### `Product`
The `Product` model defines goods or services offered by a company.
- **Key Fields**: `name`, `description`, `price`, `category`, `status`, and `components`.
- **Nested Struct**: `ProductComponent` allows for defining complex products with sub-parts.
- **Enums**:
- `ProductType`: Differentiates between a `Product` and a `Service`.
- `ProductStatus`: Indicates if a product is `Available` or `Unavailable`.
### `Sale`
The `Sale` struct records a transaction, linking a buyer to products.
- **Key Fields**: `company_id`, `buyer_id`, `total_amount`, `sale_date`, and `status`.
- **Nested Struct**: `SaleItem` captures a snapshot of each product at the time of sale, including `product_id`, `quantity`, and `unit_price`.
- **Enum**: `SaleStatus` tracks the state of the sale (`Pending`, `Completed`, `Cancelled`).
### `Payment`
The `Payment` model handles financial transactions, often linked to sales or subscriptions.
- **Key Fields**: `payment_intent_id` (e.g., for Stripe), `company_id`, `total_amount`, `currency`, and `status`.
- **Functionality**: Includes methods to manage the payment lifecycle (`process_payment`, `complete_payment`, `fail_payment`, `refund_payment`).
- **Enum**: `PaymentStatus` provides a detailed state of the payment (`Pending`, `Processing`, `Completed`, `Failed`, `Refunded`).
### `Shareholder`
The `Shareholder` model tracks ownership of a company.
- **Key Fields**: `company_id`, `user_id`, `name`, `shares`, and `percentage`.
- **Enum**: `ShareholderType` distinguishes between `Individual` and `Corporate` shareholders.
## Workflow Example
1. A `Company` is created.
2. The company defines several `Product` models representing its offerings.
3. A customer (buyer) initiates a purchase, which creates a `Sale` record containing multiple `SaleItem`s.
4. A `Payment` record is generated to process the transaction for the `Sale`'s total amount.
5. As the company grows, `Shareholder` records are created to track equity distribution.
All models use the builder pattern for easy and readable instance creation.

View File

@@ -17,10 +17,3 @@ pub use shareholder::{Shareholder, ShareholderType};
pub mod sale;
pub use sale::{Sale, SaleItem, SaleStatus};
// pub use user::{User}; // Assuming a simple User model for now
#[cfg(feature = "rhai")]
pub mod rhai;
#[cfg(feature = "rhai")]
pub use rhai::register_biz_rhai_module;

View File

@@ -1,6 +1,6 @@
use heromodels_core::BaseModelData;
use rhai::{CustomType, TypeBuilder};
use heromodels_derive::model;
use rhai::{CustomType, TypeBuilder};
use serde::{Deserialize, Serialize};
// ProductType represents the type of a product

View File

@@ -0,0 +1,70 @@
# Calendar Model
The `calendar` model provides the data structures for managing calendars, events, and attendees.
## Core Components
### 1. `Calendar`
Represents a calendar, which is a collection of events. Each calendar has a name, an optional description, and a list of event IDs.
- `name`: The name of the calendar (e.g., "Work Calendar", "Personal Calendar").
- `description`: An optional text description for the calendar.
- `events`: A `Vec<i64>` containing the IDs of the `Event` models associated with this calendar.
- `owner_id`: The ID of the user who owns the calendar.
- `is_public`: A boolean indicating if the calendar is visible to others.
### 2. `Event`
Represents a single event within a calendar. It contains all the details for a specific appointment or occasion.
- `title`: The title of the event.
- `description`: An optional detailed description.
- `start_time` & `end_time`: Unix timestamps for when the event begins and ends.
- `attendees`: A `Vec<Attendee>` listing who is invited to the event and their status.
- `location`: The physical or virtual location of the event.
- `status`: The current state of the event, defined by the `EventStatus` enum.
### 3. `Attendee`
Represents a person invited to an `Event`.
- `contact_id`: The ID of the user or contact who is the attendee.
- `status`: The attendee's response to the invitation, defined by the `AttendanceStatus` enum.
## Enums
### `EventStatus`
Defines the lifecycle of an `Event`:
- `Draft`: The event is being planned and is not yet visible to attendees.
- `Published`: The event is confirmed and visible.
- `Cancelled`: The event has been cancelled.
### `AttendanceStatus`
Defines the status of an `Attendee` for an event:
- `Accepted`: The attendee has confirmed they will attend.
- `Declined`: The attendee has declined the invitation.
- `Tentative`: The attendee is unsure if they will attend.
- `NoResponse`: The attendee has not yet responded.
## Usage
The `Calendar` model uses a builder pattern for creating and modifying instances. You can create a new `Calendar` or `Event` and chain methods to set its properties.
```rust
use heromodels::models::calendar::{Calendar, Event, Attendee, AttendanceStatus};
// Create a new event
let event = Event::new()
.title("Team Meeting")
.description("Weekly sync-up.")
.reschedule(1672531200, 1672534800) // Set start and end times
.add_attendee(Attendee::new(101).status(AttendanceStatus::Accepted));
// Create a new calendar and add the event to it (assuming event has been saved and has an ID)
let calendar = Calendar::new(None, "Work Events")
.owner_id(1)
.add_event(event.base_data.id); // Add event by ID
```

View File

@@ -1,12 +1,12 @@
use heromodels_core::BaseModelData;
use heromodels_derive::model;
use rhai::{CustomType, TypeBuilder};
use rhai_autobind_macros::rhai_model_export;
use serde::{Deserialize, Serialize};
/// Represents the status of an attendee for an event
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
pub enum AttendanceStatus {
#[default]
Accepted = 0,
Declined = 1,
Tentative = 2,
@@ -14,8 +14,9 @@ pub enum AttendanceStatus {
}
/// Represents the status of an event
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
pub enum EventStatus {
#[default]
Draft = 0,
Published = 1,
Cancelled = 2,
@@ -87,7 +88,7 @@ impl Attendee {
/// Represents an event in a calendar
#[model]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, CustomType)]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, CustomType, Default)]
pub struct Event {
/// Base model data
pub base_data: BaseModelData,
@@ -224,10 +225,14 @@ impl Event {
}
/// Adds an attendee ID to the event
pub fn add_attendee(mut self, attendee_id: u32) -> Self {
pub fn add_attendee(mut self, attendee: Attendee) -> Self {
// Prevent duplicate attendees by ID
if !self.attendees.iter().any(|&a_id| a_id == attendee_id) {
self.attendees.push(attendee_id);
if !self
.attendees
.iter()
.any(|a| a.contact_id == attendee.contact_id)
{
self.attendees.push(attendee);
}
self
}

View File

@@ -2,4 +2,4 @@
pub mod calendar;
// Re-export Calendar, Event, Attendee, AttendanceStatus, and EventStatus from the inner calendar module (calendar.rs) within src/models/calendar/mod.rs
pub use self::calendar::{AttendanceStatus, Attendee, Calendar, Event, EventStatus};
pub use self::calendar::{AttendanceStatus, Attendee, Calendar, Event, EventStatus};

View File

@@ -0,0 +1,59 @@
# Circle Model
The `circle` model defines a `Circle` struct, which represents a group or community of members. It includes metadata for customization and relationship mapping between different circles.
## `Circle` Struct
The `Circle` struct is the primary model in this module.
### Fields
- `base_data`: Standard `BaseModelData` for a unique ID and timestamps.
- `title`: The name of the circle.
- `ws_url`: A WebSocket URL associated with the circle.
- `description`: An optional, longer description of the circle's purpose.
- `logo`: An optional URL or symbol for the circle's logo.
- `members`: A `Vec<String>` containing the public keys of the members of the circle.
- `circles`: A `Vec<String>` containing the titles or IDs of other related circles, allowing for a network of circles.
- `theme`: A `ThemeData` struct for customizing the visual appearance of the circle.
### `ThemeData` Struct
This struct holds visual customization options for a circle:
- `primary_color`: The primary color for the circle's theme.
- `background_color`: The background color.
- `background_pattern`: A pattern for the background.
- `logo_symbol`: A symbol to use for the logo.
- `logo_url`: A URL for the logo image.
- `nav_dashboard_visible`: A boolean to control the visibility of the dashboard navigation.
- `nav_timeline_visible`: A boolean to control the visibility of the timeline navigation.
## Usage Example
Here's how you might create a new circle and add members to it:
```rust
use heromodels::models::circle::{Circle, ThemeData};
let mut my_circle = Circle::new()
.title("My Awesome Circle".to_string())
.description("A circle for awesome people.".to_string())
.ws_url("wss://example.com/my_circle".to_string());
my_circle = my_circle.add_member("pubkey_of_member_1".to_string());
my_circle = my_circle.add_member("pubkey_of_member_2".to_string());
let theme = ThemeData {
primary_color: "#FF5733".to_string(),
background_color: "#FFFFFF".to_string(),
// ... other theme fields
..Default::default()
};
my_circle = my_circle.theme(theme);
// The circle is now ready to be saved to the database.
```
The `Circle` model is useful for creating social groups, teams, or any other collection of users who need to be grouped together.

View File

@@ -0,0 +1,66 @@
# Contact and Group Models
The `contact` module provides models for managing a personal or organizational address book. It includes the `Contact` struct for individual entries and the `Group` struct for organizing contacts.
## `Contact` Struct
The `Contact` model stores detailed information about a single contact.
### Fields
- `base_data`: Standard `BaseModelData` for a unique ID and timestamps.
- `name`: The contact's name (indexed for easy searching).
- `description`: An optional, longer description.
- `address`: The physical or mailing address.
- `phone`: The contact's phone number.
- `email`: The contact's email address.
- `notes`: Optional field for any additional notes.
- `circle`: A `String` to associate the contact with a specific `Circle` or social group.
## `Group` Struct
The `Group` model allows for the creation of contact lists, making it easy to manage related contacts together.
### Fields
- `base_data`: Standard `BaseModelData`.
- `name`: The name of the group (e.g., "Family", "Work Colleagues").
- `description`: An optional description of the group.
- `contacts`: A `Vec<u32>` containing the unique IDs of the `Contact` models that belong to this group.
## Usage Example
Here is a conceptual example of how to create contacts and organize them into a group:
1. **Create individual contacts**:
```rust
use heromodels::models::contact::Contact;
let contact1 = Contact::new()
.name("Alice")
.email("alice@example.com");
let contact2 = Contact::new()
.name("Bob")
.email("bob@example.com");
// Save contact1 and contact2 to the database and get their IDs (e.g., 1 and 2).
```
2. **Create a group and add the contacts**:
```rust
use heromodels::models::contact::Group;
let mut friends_group = Group::new()
.name("Friends")
.description("My closest friends.");
friends_group = friends_group.add_contact(1); // Add Alice's ID
friends_group = friends_group.add_contact(2); // Add Bob's ID
// Save the group to the database.
```
Both models use the builder pattern, providing a fluent and readable way to construct instances.

View File

@@ -0,0 +1,50 @@
# Core Model
The `core` model contains fundamental, reusable components that are shared across various other models in the `heromodels` library. The primary component in this module is the `Comment` struct.
## `Comment` Struct
The `Comment` struct is designed to provide a generic commenting functionality that can be associated with any other model. It supports threaded conversations.
### Fields
- `base_data`: The standard `BaseModelData`, which provides a unique ID and timestamps for each comment.
- `user_id`: The ID of the user who posted the comment. This field is indexed.
- `content`: The text content of the comment.
- `parent_comment_id`: An `Option<u32>` that holds the ID of the parent comment. If this is `None`, the comment is a top-level comment. If it contains an ID, it is a reply to another comment.
## Usage
The `Comment` model uses a builder pattern for easy instantiation. You can create top-level comments or replies.
### Creating a Top-Level Comment
```rust
use heromodels::models::core::Comment;
let top_level_comment = Comment::new()
.user_id(101) // ID of the user posting
.content("This is the first comment on the topic.");
assert!(top_level_comment.parent_comment_id.is_none());
```
### Creating a Threaded Reply
To create a reply, you set the `parent_comment_id` to the ID of the comment you are replying to.
```rust
use heromodels::models::core::Comment;
// Assume the top_level_comment from the previous example was saved and has ID 1
let top_level_comment_id = 1;
let reply_comment = Comment::new()
.user_id(102)
.content("This is a reply to the first comment.")
.parent_comment_id(Some(top_level_comment_id));
assert_eq!(reply_comment.parent_comment_id, Some(1));
```
This `Comment` model can be linked from other models (like `User`, `Article`, `Project`, etc.) by storing a `Vec<u32>` of comment IDs within them, as demonstrated by the `add_comment` method in the `userexample` model.

View File

@@ -0,0 +1,89 @@
# Finance Model
The `finance` model provides a suite of data structures for managing financial accounts, digital assets, and a marketplace for trading them.
## Core Components
### 1. `Account`
Represents a financial account, typically owned by a user. It acts as a container for various assets.
- `name`: An internal name for the account (e.g., "My Savings").
- `user_id`: The ID of the user who owns the account.
- `ledger`: The blockchain or financial system where the account exists (e.g., "Ethereum").
- `address`: The account's public address.
- `assets`: A `Vec<u32>` of asset IDs associated with this account.
### 2. `Asset`
Represents a digital or tokenized asset.
- `name`: The name of the asset (e.g., "Bitcoin", "MyToken").
- `amount`: The quantity of the asset held.
- `asset_type`: The type of the asset, defined by the `AssetType` enum.
- `address`: The contract address of the token (if applicable).
### 3. `Marketplace`
The marketplace components facilitate the trading of assets.
- **`Listing`**: Represents an asset listed for sale. It can be a fixed-price sale, an auction, or an exchange.
- `title`: The title of the listing.
- `asset_id`: The ID of the asset being sold.
- `seller_id`: The ID of the user selling the asset.
- `price`: The asking price or starting bid.
- `listing_type`: The type of sale, defined by `ListingType`.
- `status`: The current state of the listing, defined by `ListingStatus`.
- **`Bid`**: Represents a bid made on an auction-style `Listing`.
- `bidder_id`: The ID of the user placing the bid.
- `amount`: The value of the bid.
- `status`: The current state of the bid, defined by `BidStatus`.
## Enums
### `AssetType`
- `Erc20`, `Erc721`, `Erc1155`: Standard Ethereum token types.
- `Native`: The native currency of a blockchain (e.g., ETH).
### `ListingType`
- `FixedPrice`: The asset is sold for a set price.
- `Auction`: The asset is sold to the highest bidder.
- `Exchange`: The asset is offered in trade for other assets.
### `ListingStatus`
- `Active`, `Sold`, `Cancelled`, `Expired`: Defines the lifecycle of a listing.
### `BidStatus`
- `Active`, `Accepted`, `Rejected`, `Cancelled`: Defines the lifecycle of a bid.
## Usage
The models use a builder pattern for easy instantiation.
```rust
use heromodels::models::finance::{Account, Asset, Listing, ListingType};
// 1. Create a user account
let account = Account::new()
.name("Trading Account")
.user_id(101)
.ledger("Ethereum")
.address("0x123...");
// 2. Create an asset (assuming it's saved and has an ID)
let asset = Asset::new()
.name("Hero Token")
.amount(1000.0);
// In a real scenario, you would save the asset to get an ID.
let asset_id = asset.base_data.id.to_string();
// 3. Create a marketplace listing for the asset
let listing = Listing::new()
.title("1000 Hero Tokens for Sale")
.asset_id(asset_id)
.seller_id(account.user_id.to_string())
.price(0.5)
.currency("USD")
.listing_type(ListingType::FixedPrice);
```

View File

@@ -0,0 +1,60 @@
# Flow Model
The `flow` model provides a framework for creating and managing multi-step workflows, particularly those requiring digital signatures. It is designed to orchestrate a sequence of actions that must be completed in a specific order.
## Core Components
### 1. `Flow`
The top-level container for a workflow.
- `flow_uuid`: A unique identifier for the entire flow, used for external references.
- `name`: A human-readable name for the flow (e.g., "Document Approval Process").
- `status`: The overall status of the flow (e.g., "Pending", "InProgress", "Completed").
- `steps`: A `Vec<FlowStep>` that defines the sequence of steps in the workflow.
### 2. `FlowStep`
Represents a single, distinct step within a `Flow`.
- `step_order`: A `u32` that determines the position of this step in the sequence.
- `description`: An optional text description of what this step entails.
- `status`: The status of this individual step.
### 3. `SignatureRequirement`
Defines a requirement for a digital signature within a `FlowStep`. A single step can have multiple signature requirements.
- `flow_step_id`: A foreign key linking the requirement to its parent `FlowStep`.
- `public_key`: The public key of the entity that is required to sign.
- `message`: The plaintext message that needs to be signed.
- `signature`: The resulting signature, once provided.
- `status`: The status of the signature requirement (e.g., "Pending", "Signed", "Failed").
## Usage
The models use a builder pattern to construct complex flows. You create a `Flow`, add `FlowStep`s to it, and associate `SignatureRequirement`s with each step.
```rust
use heromodels::models::flow::{Flow, FlowStep, SignatureRequirement};
use uuid::Uuid;
// 1. Define a signature requirement
let requirement = SignatureRequirement::new(
0, // ID is managed by the database
1, // Belongs to flow step 1
"0xPublicKey1...",
"I approve this document."
);
// 2. Create a flow step
// In a real application, you would add the signature requirement to the step.
let step1 = FlowStep::new(0, 1) // ID, step_order
.description("Initial review and approval");
// 3. Create the main flow and add the step
let flow = Flow::new(Uuid::new_v4().to_string())
.name("Contract Signing Flow")
.add_step(step1);
```

View File

@@ -9,7 +9,7 @@ use serde::{Deserialize, Serialize};
#[model]
pub struct Flow {
/// Base model data (id, created_at, updated_at).
#[rhai_type(skip)]
#[rhai_type(skip)]
pub base_data: BaseModelData,
/// A unique UUID for the flow, for external reference.

View File

@@ -9,7 +9,7 @@ use std::default::Default;
#[model]
pub struct FlowStep {
/// Base model data.
#[rhai_type(skip)]
#[rhai_type(skip)]
pub base_data: BaseModelData,
/// Optional description for the step.

View File

@@ -6,4 +6,4 @@ pub mod signature_requirement;
// Re-export key types for convenience
pub use flow::Flow;
pub use flow_step::FlowStep;
pub use signature_requirement::SignatureRequirement;
pub use signature_requirement::SignatureRequirement;

View File

@@ -9,7 +9,7 @@ use std::default::Default;
#[model]
pub struct SignatureRequirement {
/// Base model data.
#[rhai_type(skip)]
#[rhai_type(skip)]
pub base_data: BaseModelData,
/// Foreign key to the FlowStep this requirement belongs to.

View File

@@ -0,0 +1,64 @@
# Corporate Governance (`gov`) Model
The `gov` module provides a comprehensive suite of models for managing corporate governance structures and processes. It allows for the detailed representation of companies, their ownership, governing bodies, and decision-making workflows.
## Core Models
### `Company`
The `Company` struct is the central entity in this module. It is similar to the `Company` model in the `biz` module but is specifically tailored for governance, with direct implementation of the `Model` trait for database interaction.
- **Key Fields**: `name`, `registration_number`, `incorporation_date`, `status`, `business_type`.
- **Enums**: `CompanyStatus`, `BusinessType`.
### `Shareholder`
The `Shareholder` model is used to track ownership of a company.
- **Key Fields**: `company_id`, `name`, `shares`, `percentage`, `shareholder_type`.
- **Enums**: `ShareholderType` (e.g., Individual, Corporate).
### `Committee`
Companies can have `Committee`s to oversee specific functions (e.g., Audit Committee, Compensation Committee). Each committee is composed of `CommitteeMember`s.
- **`Committee` Fields**: `company_id`, `name`, `description`, `members`.
- **`CommitteeMember` Fields**: `user_id`, `name`, `role`.
- **Enums**: `CommitteeRole` (e.g., Chair, Member, Advisor).
### `Meeting`
The `Meeting` model is used to schedule and document official meetings for a company or its committees.
- **Key Fields**: `company_id`, `title`, `meeting_type`, `status`, `start_time`, `end_time`, `agenda`, `minutes`, `attendees`.
- **`Attendee` Fields**: `user_id`, `name`, `status`.
- **Enums**: `MeetingStatus`, `MeetingType`, `AttendanceStatus`.
### `Resolution`
A `Resolution` represents a formal proposal or decision that requires a vote.
- **Key Fields**: `company_id`, `title`, `description`, `resolution_type`, `status`, `proposed_date`, `effective_date`.
- **Enums**: `ResolutionStatus`, `ResolutionType` (e.g., Ordinary, Special).
### `Vote` and `Ballot`
The `Vote` model facilitates the voting process for a specific `Resolution`. Each `Vote` consists of multiple `Ballot`s cast by voters.
- **`Vote` Fields**: `company_id`, `resolution_id`, `title`, `status`, `start_date`, `end_date`, `ballots`.
- **`Ballot` Fields**: `user_id`, `option`, `weight`, `cast_at`.
- **Enums**: `VoteStatus`, `VoteOption` (Yes, No, Abstain).
## Workflow Example
A typical governance workflow might look like this:
1. A `Company` is established with several `Shareholder`s.
2. A `Committee` (e.g., the Board of Directors) is formed by adding `CommitteeMember`s.
3. The committee proposes a `Resolution` to approve the annual budget.
4. A `Vote` is created for this resolution, with a defined start and end date.
5. A `Meeting` is scheduled to discuss the resolution.
6. During the voting period, shareholders or committee members cast their `Ballot`s.
7. Once the `Vote` is closed, the results are tallied, and the `Resolution` status is updated to `Approved` or `Rejected`.
This module provides the foundational data structures for building robust corporate governance applications.

View File

@@ -0,0 +1,79 @@
# Governance Model
The `governance` model provides a robust framework for managing decentralized governance processes, including proposals, voting, and activity tracking.
## Core Components
### 1. `Proposal`
The central element of the governance model. A `Proposal` represents a formal suggestion submitted to the community for a vote.
- `title` & `description`: The substance of the proposal.
- `creator_id`: The ID of the user who submitted the proposal.
- `status`: The current state of the proposal (e.g., `Draft`, `Active`, `Approved`), defined by the `ProposalStatus` enum.
- `options`: A `Vec<VoteOption>` defining the choices voters can select (e.g., "For", "Against", "Abstain").
- `vote_start_date` & `vote_end_date`: Timestamps that define the voting period.
### 2. `Ballot`
Represents a single vote cast by a user on a specific `Proposal`.
- `user_id`: The ID of the voter.
- `vote_option_id`: The specific `VoteOption` the user selected.
- `shares_count`: The voting power or weight of the vote.
- `comment`: An optional comment from the voter.
### 3. `GovernanceActivity`
A detailed record of every significant event that occurs within the governance system. This is crucial for transparency and auditing.
- `activity_type`: The type of event that occurred (e.g., `ProposalCreated`, `VoteCast`), defined by the `ActivityType` enum.
- `actor_id` & `actor_name`: Who performed the action.
- `target_id` & `target_type`: The object the action was performed on (e.g., a `Proposal`).
- `title` & `description`: A summary of the activity.
### 4. `AttachedFile`
A simple struct to link external documents or files to a proposal, such as technical specifications or legal drafts.
## Enums
The model includes several enums to manage the state of proposals, voting, and activities:
- `ProposalStatus`: Tracks the lifecycle of a proposal (`Draft`, `Active`, `Approved`, `Rejected`).
- `VoteEventStatus`: Tracks the status of the voting period (`Upcoming`, `Open`, `Closed`).
- `ActivityType`: Categorizes different governance actions.
- `ActivityStatus`: Tracks the status of a recorded activity (`Pending`, `Completed`, `Failed`).
## Usage
```rust
use heromodels::models::governance::{Proposal, Ballot, VoteOption, ProposalStatus};
use chrono::Utc;
// 1. Create a new proposal
let mut proposal = Proposal::new(
None, // ID is managed by the database
"user-123".to_string(),
"Alice".to_string(),
"Adopt New Logo".to_string(),
"Proposal to update the community logo.".to_string(),
ProposalStatus::Draft,
vec![],
None
);
// 2. Add voting options
proposal = proposal.add_option(1, "Approve New Logo", None);
proposal = proposal.add_option(2, "Reject New Logo", None);
// 3. An eligible user casts a vote
// This would typically be done by finding the proposal and then calling cast_vote.
let proposal_after_vote = proposal.cast_vote(
None, // Ballot ID
456, // Voter's user_id
1, // Voting for option 1
100 // With 100 shares/votes
);
```

View File

@@ -3,7 +3,6 @@
use chrono::{DateTime, Utc};
use heromodels_derive::model;
use rhai::{CustomType, TypeBuilder};
use rhai_autobind_macros::rhai_model_export;
use serde::{Deserialize, Serialize};
// use std::collections::HashMap;
@@ -47,7 +46,6 @@ impl Default for ActivityStatus {
/// GovernanceActivity represents a single activity or event in the governance system
/// This model tracks all significant actions and changes for audit and transparency purposes
#[derive(Debug, Clone, Serialize, Deserialize, CustomType)]
#[rhai_model_export(db_type = "std::sync::Arc<crate::db::hero::OurDB>")]
#[model]
pub struct GovernanceActivity {
pub base_data: BaseModelData,

View File

@@ -1,7 +1,9 @@
// heromodels/src/models/governance/mod.rs
// This module will contain the Proposal model and related types.
pub mod activity;
pub mod attached_file;
pub mod proposal;
pub use self::activity::{ActivityStatus, ActivityType, GovernanceActivity};
pub use self::attached_file::AttachedFile;
pub use self::proposal::{Ballot, Proposal, ProposalStatus, VoteEventStatus, VoteOption};

View File

@@ -343,7 +343,6 @@ impl ToString for ActivityType {
/// Represents a governance activity in the system
#[derive(Debug, Clone, Serialize, Deserialize, CustomType)]
#[rhai_model_export(db_type = "std::sync::Arc<crate::db::hero::OurDB>")]
#[model] // Has base.Base in V spec
pub struct Activity {
/// Base model data

View File

@@ -0,0 +1,79 @@
# Legal Model
The `legal` model provides a structured way to create, manage, and track the lifecycle of digital contracts.
## Core Components
### 1. `Contract`
The main struct representing a legal agreement. It serves as a container for all contract-related data.
- `contract_id`: A unique identifier for the contract.
- `title` & `description`: A summary of the contract's purpose.
- `status`: The current state of the contract (e.g., `Draft`, `Active`), managed by the `ContractStatus` enum.
- `signers`: A `Vec<ContractSigner>` listing all parties required to sign.
- `revisions`: A `Vec<ContractRevision>` that provides a history of the contract's content, allowing for version control.
- `current_version`: The active version of the contract.
### 2. `ContractSigner`
Represents an individual or entity required to sign the contract.
- `id`, `name`, `email`: Identifying information for the signer.
- `status`: The signer's current status (`Pending`, `Signed`, `Rejected`), defined by the `SignerStatus` enum.
- `signed_at`: A timestamp indicating when the signature was provided.
- `signature_data`: Stores the actual signature, for example, as a Base64 encoded image.
### 3. `ContractRevision`
Represents a specific version of the contract's text.
- `version`: A number identifying the revision.
- `content`: The full text of the contract for that version.
- `created_at` & `created_by`: Audit fields to track who created the revision and when.
## Enums
The model uses two key enums to manage state:
- `ContractStatus`: Defines the lifecycle of the entire contract, from `Draft` to `PendingSignatures`, `Active`, `Expired`, or `Cancelled`.
- `SignerStatus`: Tracks the state of each individual signer (`Pending`, `Signed`, `Rejected`).
## Usage
The models are constructed using a builder pattern, allowing for clear and flexible creation of complex contracts.
```rust
use heromodels::models::legal::{Contract, ContractSigner, ContractRevision, ContractStatus};
use std::time::{SystemTime, UNIX_EPOCH};
fn current_timestamp_secs() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs()
}
// 1. Create a signer
let signer1 = ContractSigner::new(
"signer-uuid-1".to_string(),
"Alice".to_string(),
"alice@example.com".to_string()
);
// 2. Create a revision
let revision1 = ContractRevision::new(
1,
"This is the first version of the contract...".to_string(),
current_timestamp_secs(),
"creator-uuid-1".to_string()
);
// 3. Create the contract
let contract = Contract::new(1, "contract-uuid-1".to_string())
.title("Service Agreement")
.status(ContractStatus::PendingSignatures)
.add_signer(signer1)
.add_revision(revision1)
.current_version(1);
```

View File

@@ -0,0 +1,49 @@
# Library Model
The `library` model provides a flexible system for managing various types of digital assets and organizing them into collections.
## Library Item Types
The model supports several distinct types of library items, each with its own specific metadata:
- `Image`: Represents an image file with properties like `title`, `url`, `width`, and `height`.
- `Pdf`: Represents a PDF document with a `title`, `url`, and `page_count`.
- `Markdown`: Represents a text document written in Markdown, with `title` and `content`.
- `Book`: A more complex item that consists of a `title`, a `table_of_contents` (a nested structure of `TocEntry` items), and a `Vec<String>` of pages (in Markdown).
- `Slideshow`: Represents a presentation, containing a `title` and a `Vec<Slide>`, where each slide has an `image_url` and optional text.
All items share a common `BaseModelData` field, providing them with a unique ID and timestamps.
## `Collection`
The `Collection` struct is used to group various library items together. It does not store the items directly but rather holds vectors of their unique IDs.
- `title` & `description`: To name and describe the collection.
- `images`: A `Vec<u32>` of `Image` item IDs.
- `pdfs`: A `Vec<u32>` of `Pdf` item IDs.
- `markdowns`: A `Vec<u32>` of `Markdown` item IDs.
- `books`: A `Vec<u32>` of `Book` item IDs.
- `slides`: A `Vec<u32>` of `Slideshow` item IDs.
## Usage
First, you create individual library items. Then, you create a collection and add the IDs of those items to it.
```rust
use heromodels::models::library::{Image, Collection};
// 1. Create a library item (e.g., an Image)
let image1 = Image::new()
.title("Company Logo")
.url("https://example.com/logo.png");
// In a real app, this would be saved to a database, and we'd get an ID.
let image1_id = image1.id(); // Assuming this ID is now 1
// 2. Create a collection
let mut marketing_assets = Collection::new()
.title("Marketing Assets");
// 3. Add the item's ID to the collection
marketing_assets = marketing_assets.add_image(image1_id);
```

View File

@@ -112,10 +112,10 @@ impl Pdf {
Self::default()
}
/// Gets the ID of the image.
pub fn id(&self) -> u32 {
self.base_data.id
}
/// Gets the ID of the image.
pub fn id(&self) -> u32 {
self.base_data.id
}
/// Sets the title of the PDF.
pub fn title(mut self, title: impl Into<String>) -> Self {
@@ -163,10 +163,10 @@ impl Markdown {
Self::default()
}
/// Gets the ID of the image.
pub fn id(&self) -> u32 {
self.base_data.id
}
/// Gets the ID of the image.
pub fn id(&self) -> u32 {
self.base_data.id
}
/// Sets the title of the document.
pub fn title(mut self, title: impl Into<String>) -> Self {

View File

@@ -1,3 +1,36 @@
## Object Model
# Log Model
This is a generic object model mostly used for testing purposes.
The `log` model provides a generic `Log` struct for creating audit trails, recording events, or tracking activities within the system. It is designed to be flexible, linking a subject (who performed the action) to an object (what was affected).
## `Log` Struct
The `Log` struct is the core of this module.
### Fields
- `base_data`: Standard `BaseModelData` for a unique ID and timestamps.
- `title`: A short, descriptive title for the log entry (e.g., "User Login", "File Deletion"). This field is indexed.
- `description`: A more detailed description of the event.
- `subject_pk`: The public key of the user or entity that initiated the event. This field is indexed.
- `object_id`: The unique ID of the object or resource that was the target of the event. This field is indexed.
## Usage Example
Here is how you might create a log entry when a user updates a contact:
```rust
use heromodels::models::log::Log;
let user_public_key = "pubkey_of_acting_user";
let updated_contact_id = 123;
let log_entry = Log::new()
.title("Contact Updated".to_string())
.description(format!("User {} updated contact with ID {}.", user_public_key, updated_contact_id))
.subject_pk(user_public_key.to_string())
.object_id(updated_contact_id);
// Save the log_entry to the database.
```
By indexing `title`, `subject_pk`, and `object_id`, the `Log` model allows for efficient querying of activities, such as retrieving all actions performed by a specific user or all events related to a particular object.

View File

@@ -21,15 +21,15 @@ pub use userexample::User;
// pub use productexample::Product; // Temporarily remove
pub use biz::{Payment, PaymentStatus, Sale, SaleItem, SaleStatus};
pub use calendar::{AttendanceStatus, Attendee, Calendar, Event};
pub use circle::{Circle, ThemeData};
pub use finance::marketplace::{Bid, BidStatus, Listing, ListingStatus, ListingType};
pub use finance::{Account, Asset, AssetType};
pub use flow::{Flow, FlowStep, SignatureRequirement};
pub use legal::{Contract, ContractRevision, ContractSigner, ContractStatus, SignerStatus};
pub use library::collection::Collection;
pub use library::items::{Image, Markdown, Pdf};
pub use projects::{Project, ProjectStatus};
pub use governance::{
ActivityStatus, ActivityType, Ballot, GovernanceActivity, Proposal, ProposalStatus,
VoteEventStatus, VoteOption,
};
pub use circle::{Circle, ThemeData};
pub use legal::{Contract, ContractRevision, ContractSigner, ContractStatus, SignerStatus};
pub use library::collection::Collection;
pub use library::items::{Image, Markdown, Pdf};
pub use projects::{Project, Status};

View File

@@ -1,3 +1,27 @@
## Object Model
# Object Model
This is a generic object model mostly used for testing purposes.
The `object` model provides a simple, generic `Object` struct. It is intended for use in situations where a basic, identifiable data container is needed, such as for testing, prototyping, or representing simple items that do not require a more complex, specific model.
## `Object` Struct
The `Object` struct contains the following fields:
- `base_data`: Standard `BaseModelData` for a unique ID and timestamps.
- `title`: A string for the object's title or name. This field is indexed for efficient lookups.
- `description`: A string for a more detailed description of the object.
## Usage Example
Creating a new `Object` is straightforward using the builder pattern:
```rust
use heromodels::models::object::Object;
let my_object = Object::new()
.title("My Test Object".to_string())
.description("This is an object created for a test scenario.".to_string());
// The object is now ready to be saved to the database.
```
Due to its simplicity, the `Object` model is a versatile tool for various development and testing needs.

View File

@@ -1,9 +1,9 @@
use heromodels_core::BaseModelData;
use heromodels_derive::model;
use rhai::CustomType;
use rhai::TypeBuilder;
use rhailib_derive::RhaiApi;
use serde::{Deserialize, Serialize};
use rhai::TypeBuilder;
/// Represents an event in a contact
#[model]
@@ -13,7 +13,7 @@ pub struct Object {
pub base_data: BaseModelData,
#[index]
pub title: String,
pub description: String
pub description: String,
}
impl Object {
@@ -28,7 +28,7 @@ impl Object {
pub fn id(&self) -> u32 {
self.base_data.id
}
pub fn title(mut self, title: String) -> Self {
self.title = title;
self
@@ -38,4 +38,4 @@ impl Object {
self.description = description;
self
}
}
}

View File

@@ -0,0 +1,76 @@
# Projects Model
The `projects` model provides a comprehensive suite of tools for managing software development projects, based on common agile methodologies.
## Core Components
The model is built around a hierarchy of work items:
- **`Project`**: The highest-level container. A `Project` holds information about its members, and contains lists of IDs for associated epics, sprints, and boards.
- **`Epic`**: Represents a large body of work or a major feature. An `Epic` is broken down into smaller tasks and can be associated with a project. It tracks its own status, start/due dates, and a list of `child_task_ids`.
- **`Sprint`**: A time-boxed iteration (e.g., two weeks) during which a team works to complete a set of tasks. A `Sprint` has a goal, a start and end date, its own status (`Planned`, `Active`, `Completed`), and a list of `task_ids`.
- **`Task`**: The most granular unit of work. A `Task` has a title, description, status, priority, and can be assigned to a user. It can be linked to a parent `Project`, `Epic`, and `Sprint`. Tasks can also be nested using the `parent_task_id` field.
- **`Label`**: A simple struct for creating tags with a name and a color, which can be used to categorize items.
## Enums and Statuses
The model uses several enums to manage the state of work items:
- **`Priority`**: A general enum (`Critical`, `High`, `Medium`, `Low`) used across different models.
- **`Status`**: A general status enum (`Todo`, `InProgress`, `Done`, etc.) for projects.
- **`ItemType`**: Describes the type of work item (`Epic`, `Story`, `Task`, `Bug`).
- **`SprintStatus`**: Specific statuses for sprints (`Planned`, `Active`, `Completed`).
- **`TaskStatus`** and **`TaskPriority`**: Specific enums for the detailed states and priorities of individual tasks.
## Usage
The typical workflow involves creating a `Project`, then populating it with `Epic`s and `Sprint`s. `Task`s are then created and associated with these epics and sprints.
```rust
use heromodels::models::projects::{Project, Epic, Sprint, Task, task_enums::{TaskStatus, TaskPriority}};
// 1. Create a Project
let project = Project::new(1, "New Website".to_string(), "Build a new company website".to_string(), 101);
// 2. Create an Epic for a major feature
let mut epic = Epic::new(
"User Authentication".to_string(),
Some("Implement login, registration, and profile management".to_string()),
Default::default(),
Some(project.get_id()),
None, None, vec![]
);
// 3. Create a Sprint
let mut sprint = Sprint::new(
"Sprint 1".to_string(),
None,
Default::default(),
Some("Focus on core auth endpoints".to_string()),
Some(project.get_id()),
None, None
);
// 4. Create a Task and link it to the Epic and Sprint
let task = Task::new(
"Create login endpoint".to_string(),
None,
TaskStatus::Todo,
TaskPriority::High,
Some(102), // assignee_id
Some(101), // reporter_id
None, // parent_task_id
Some(epic.get_id()),
Some(sprint.get_id()),
Some(project.get_id()),
None, None, None, vec!["backend".to_string()]
);
// 5. Add the task ID to the epic and sprint
epic = epic.add_task_id(task.get_id());
sprint = sprint.add_task_id(task.get_id());
```

View File

@@ -0,0 +1,55 @@
# User Example Model
The `userexample` model provides a basic but complete example of a model within the `heromodels` ecosystem. It defines a `User` struct that can be used as a template or reference for creating more complex models.
## `User` Struct
The `User` struct represents a user in the system and contains the following fields:
- `base_data`: The standard `BaseModelData` struct, providing a unique ID, timestamps, and comment tracking.
- `username`: The user's unique username.
- `email`: The user's email address.
- `full_name`: The user's full name.
- `is_active`: A boolean flag to indicate if the user's account is active.
The `username`, `email`, and `is_active` fields are indexed for efficient database lookups.
## Usage
The `User` model uses the builder pattern for easy and readable instance creation and modification.
### Creating a User
You can create a new user and set their properties fluently.
```rust
use heromodels::models::userexample::User;
// Create a new user and set their details
let mut user = User::new()
.username("jdoe")
.email("jdoe@example.com")
.full_name("John Doe");
// The user is active by default
assert_eq!(user.is_active, true);
// Deactivate the user
user.deactivate();
assert_eq!(user.is_active, false);
// Activate the user again
user.activate();
assert_eq!(user.is_active, true);
```
### Adding Comments
The model also demonstrates how to interact with the underlying `BaseModelData` to add associated comment IDs.
```rust
use heromodels::models::userexample::User;
let mut user = User::new().username("jdoe");
user = user.add_comment(101); // Add the ID of a comment
```

View File

@@ -1 +0,0 @@
1

View File

@@ -1 +0,0 @@
2

View File

@@ -1 +0,0 @@
1

Binary file not shown.

View File

@@ -1 +0,0 @@
2

View File

@@ -1 +0,0 @@
2