merge branches and cleanup db
This commit is contained in:
BIN
heromodels/.DS_Store
vendored
BIN
heromodels/.DS_Store
vendored
Binary file not shown.
Binary file not shown.
Binary file not shown.
266
heromodels/docs/herodb_ourdb_migration_plan.md
Normal file
266
heromodels/docs/herodb_ourdb_migration_plan.md
Normal 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
|
161
heromodels/docs/mcc_models_standalone_plan.md
Normal file
161
heromodels/docs/mcc_models_standalone_plan.md
Normal 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
|
277
heromodels/docs/model_trait_unification_plan.md
Normal file
277
heromodels/docs/model_trait_unification_plan.md
Normal 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
|
151
heromodels/docs/prompts/new_rhai_rs_gen.md
Normal file
151
heromodels/docs/prompts/new_rhai_rs_gen.md
Normal 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
|
192
heromodels/docs/prompts/rhai_rs_generation_prompt.md
Normal file
192
heromodels/docs/prompts/rhai_rs_generation_prompt.md
Normal 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.
|
||||
// }
|
||||
```
|
401
heromodels/docs/sigsocket_architecture.md
Normal file
401
heromodels/docs/sigsocket_architecture.md
Normal 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
|
365
heromodels/docs/tst_implementation_plan.md
Normal file
365
heromodels/docs/tst_implementation_plan.md
Normal 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.
|
451
heromodels/docs/tst_integration_plan.md
Normal file
451
heromodels/docs/tst_integration_plan.md
Normal 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.
|
@@ -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");
|
||||
|
@@ -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");
|
||||
|
@@ -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.
@@ -1 +0,0 @@
|
||||
74
|
Binary file not shown.
@@ -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 {
|
||||
|
65
heromodels/src/models/access/README.md
Normal file
65
heromodels/src/models/access/README.md
Normal 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.
|
@@ -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()) {
|
||||
|
58
heromodels/src/models/biz/README.md
Normal file
58
heromodels/src/models/biz/README.md
Normal 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.
|
@@ -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;
|
||||
|
@@ -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
|
||||
|
70
heromodels/src/models/calendar/README.md
Normal file
70
heromodels/src/models/calendar/README.md
Normal 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
|
||||
```
|
@@ -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
|
||||
}
|
||||
|
@@ -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};
|
||||
|
59
heromodels/src/models/circle/README.md
Normal file
59
heromodels/src/models/circle/README.md
Normal 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.
|
66
heromodels/src/models/contact/README.md
Normal file
66
heromodels/src/models/contact/README.md
Normal 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.
|
50
heromodels/src/models/core/README.md
Normal file
50
heromodels/src/models/core/README.md
Normal 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.
|
89
heromodels/src/models/finance/README.md
Normal file
89
heromodels/src/models/finance/README.md
Normal 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);
|
||||
```
|
60
heromodels/src/models/flow/README.md
Normal file
60
heromodels/src/models/flow/README.md
Normal 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);
|
||||
|
||||
```
|
@@ -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.
|
||||
|
@@ -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.
|
||||
|
@@ -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;
|
||||
|
@@ -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.
|
||||
|
64
heromodels/src/models/gov/README.md
Normal file
64
heromodels/src/models/gov/README.md
Normal 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.
|
79
heromodels/src/models/governance/README.md
Normal file
79
heromodels/src/models/governance/README.md
Normal 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
|
||||
);
|
||||
|
||||
```
|
@@ -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,
|
||||
|
@@ -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};
|
||||
|
@@ -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
|
||||
|
79
heromodels/src/models/legal/README.md
Normal file
79
heromodels/src/models/legal/README.md
Normal 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);
|
||||
```
|
49
heromodels/src/models/library/README.md
Normal file
49
heromodels/src/models/library/README.md
Normal 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);
|
||||
|
||||
```
|
@@ -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 {
|
||||
|
@@ -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.
|
@@ -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};
|
||||
|
@@ -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.
|
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
76
heromodels/src/models/projects/README.md
Normal file
76
heromodels/src/models/projects/README.md
Normal 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());
|
||||
```
|
55
heromodels/src/models/userexample/README.md
Normal file
55
heromodels/src/models/userexample/README.md
Normal 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
|
||||
```
|
@@ -1 +0,0 @@
|
||||
1
|
Binary file not shown.
Binary file not shown.
@@ -1 +0,0 @@
|
||||
2
|
Binary file not shown.
@@ -1 +0,0 @@
|
||||
1
|
Binary file not shown.
Binary file not shown.
@@ -1 +0,0 @@
|
||||
2
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1 +0,0 @@
|
||||
2
|
Binary file not shown.
Binary file not shown.
@@ -1 +0,0 @@
|
||||
2
|
Reference in New Issue
Block a user