merge branches and cleanup db

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

1916
_archive/acldb/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

26
_archive/acldb/Cargo.toml Normal file
View File

@@ -0,0 +1,26 @@
[package]
name = "acldb"
version = "0.1.0"
edition = "2021"
description = "HeroDB ACL Layer: Implements ACL logic, data ops, and Actix RPC server as specified in instructions.md."
[dependencies]
ourdb = { path = "../ourdb" }
tst = { path = "../tst" }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
actix-web = "4"
actix-rt = "2"
actix-cors = "0.7"
utoipa = { version = "5.3.1", features = ["actix_extras"] }
utoipa-redoc = { version = "6.0", features = ["actix-web"] }
tokio = { version = "1", features = ["full"] }
log = "0.4"
env_logger = "0.11"
thiserror = "2.0"
sha2 = "0.10"
hex = "0.4"
base64 = "0.22"
dirs = "6.0"
async-trait = "0.1"

154
_archive/acldb/README.md Normal file
View File

@@ -0,0 +1,154 @@
# ACLDB - Access Control Database
ACLDB is a secure, permission-based database system that provides fine-grained access control for data storage and retrieval. It's designed to work with the HeroDB ecosystem, offering a robust solution for managing data with complex access control requirements.
## Overview
ACLDB organizes data into "circles" and "topics" with comprehensive access control lists (ACLs) that govern who can read, write, delete, or administer different pieces of data. It's built on top of OurDB and TST (Ternary Search Tree) for efficient storage and retrieval.
## Key Features
- **Fine-grained Access Control**: Define who can access what data with a hierarchical permission system
- **Circle-based Organization**: Group data by circles (e.g., organizations, teams, projects)
- **Topic-based Categorization**: Organize data within circles by topics
- **Permission Levels**: Supports Read, Write, Delete, Execute, and Admin permission levels
- **RPC API**: Access all functionality through a well-defined RPC interface
- **REST API Server**: Includes a built-in HTTP server with Swagger/OpenAPI documentation
- **Async/Await Support**: Built with Rust's async/await for efficient concurrency
## Architecture
ACLDB consists of several key components:
1. **ACLDB**: The main database instance for a specific circle
2. **ACLDBTopic**: A database instance for a specific topic within a circle
3. **ACL**: Access Control List for managing permissions
4. **Server**: HTTP server for exposing the RPC API
5. **RpcInterface**: Interface for handling RPC requests
Data is stored using:
- **OurDB**: For efficient data storage and retrieval
- **TST**: For key-to-id mapping and prefix searches
## Permission System
ACLDB implements a hierarchical permission system with the following levels:
- **Read**: Allows reading data
- **Write**: Includes Read permission and allows writing data
- **Delete**: Includes Write permission and allows deleting data
- **Execute**: Includes Delete permission and allows executing operations
- **Admin**: Includes all permissions and allows managing ACLs
## API Methods
The RPC API provides the following methods:
### ACL Management
- **aclupdate**: Update or create an ACL with specified permissions
- **aclremove**: Remove specific public keys from an existing ACL
- **acldel**: Delete an entire ACL
### Data Operations
- **set**: Store data with optional ACL protection
- **get**: Retrieve data with ACL verification
- **del**: Delete data with ACL verification
- **prefix**: Search for keys with a specific prefix
## Usage Examples
### Starting the Server
```bash
# Start the server on localhost:8080
cargo run
# Start the server on a specific host and port
cargo run -- 0.0.0.0 9000
```
### API Documentation
Once the server is running, you can access the API documentation at:
```
http://localhost:8080/redoc
```
### Using the API
#### Creating an ACL
```json
{
"method": "aclupdate",
"params": {
"caller_pubkey": "user_public_key",
"circle_id": "my_circle",
"name": "project_data",
"pubkeys": ["user1_pubkey", "user2_pubkey"],
"right": "write"
},
"signature": "signature_here"
}
```
#### Storing Data with ACL Protection
```json
{
"method": "set",
"params": {
"caller_pubkey": "user_public_key",
"circle_id": "my_circle",
"topic": "documents",
"key": "doc1",
"value": "base64_encoded_data",
"acl_id": 1
},
"signature": "signature_here"
}
```
#### Retrieving Data
```json
{
"method": "get",
"params": {
"caller_pubkey": "user_public_key",
"circle_id": "my_circle",
"topic": "documents",
"key": "doc1"
},
"signature": "signature_here"
}
```
## Integration with Other Systems
ACLDB is designed to work seamlessly with other components of the HeroDB ecosystem. It can be used as:
1. A standalone database with access control
2. A backend for applications requiring fine-grained permissions
3. A component in a larger distributed system
## Development
### Prerequisites
- Rust 1.56 or later
- Cargo
### Building
```bash
cargo build
```
### Running Tests
```bash
cargo test
```

97
_archive/acldb/src/acl.rs Normal file
View File

@@ -0,0 +1,97 @@
use std::collections::HashMap;
use serde::{Serialize, Deserialize};
/// Represents permission levels in the ACL system
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub enum ACLRight {
/// Read permission
Read,
/// Write permission (includes Read)
Write,
/// Delete permission (includes Write and Read)
Delete,
/// Execute permission (includes Delete, Write, and Read)
Execute,
/// Admin permission (includes all other permissions)
Admin,
}
/// Access Control List for managing permissions
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ACL {
/// Unique name for the ACL within a circle
pub name: String,
/// Map of public keys to their permission levels
permissions: HashMap<String, ACLRight>,
}
impl ACL {
/// Creates a new ACL with the given name
pub fn new(name: &str) -> Self {
ACL {
name: name.to_string(),
permissions: HashMap::new(),
}
}
/// Sets a permission for a public key
pub fn set_permission(&mut self, pubkey: &str, right: ACLRight) {
self.permissions.insert(pubkey.to_string(), right);
}
/// Removes a permission for a public key
pub fn remove_permission(&mut self, pubkey: &str) {
self.permissions.remove(pubkey);
}
/// Checks if a public key has at least the specified permission level
pub fn has_permission(&self, pubkey: &str, right: ACLRight) -> bool {
if let Some(assigned_right) = self.permissions.get(pubkey) {
return *assigned_right >= right;
}
false
}
/// Gets all public keys with their associated permissions
pub fn get_all_permissions(&self) -> &HashMap<String, ACLRight> {
&self.permissions
}
/// Gets the permission level for a specific public key
pub fn get_permission(&self, pubkey: &str) -> Option<ACLRight> {
self.permissions.get(pubkey).copied()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_acl_permissions() {
let mut acl = ACL::new("test_acl");
// Set permissions
acl.set_permission("user1", ACLRight::Read);
acl.set_permission("user2", ACLRight::Write);
acl.set_permission("user3", ACLRight::Admin);
// Check permissions
assert!(acl.has_permission("user1", ACLRight::Read));
assert!(!acl.has_permission("user1", ACLRight::Write));
assert!(acl.has_permission("user2", ACLRight::Read));
assert!(acl.has_permission("user2", ACLRight::Write));
assert!(!acl.has_permission("user2", ACLRight::Delete));
assert!(acl.has_permission("user3", ACLRight::Read));
assert!(acl.has_permission("user3", ACLRight::Write));
assert!(acl.has_permission("user3", ACLRight::Delete));
assert!(acl.has_permission("user3", ACLRight::Execute));
assert!(acl.has_permission("user3", ACLRight::Admin));
// Remove permission
acl.remove_permission("user2");
assert!(!acl.has_permission("user2", ACLRight::Read));
}
}

View File

@@ -0,0 +1,47 @@
use thiserror::Error;
/// Error types for the ACLDB module
#[derive(Error, Debug)]
pub enum Error {
/// Permission denied error
#[error("Permission denied")]
PermissionDenied,
/// Record not found error
#[error("Record not found")]
NotFound,
/// Invalid operation error
#[error("Invalid operation: {0}")]
InvalidOperation(String),
/// Path error
#[error("Path error: {0}")]
PathError(String),
/// OurDB error
#[error("OurDB error: {0}")]
OurDBError(#[from] ourdb::Error),
/// TST error
#[error("TST error: {0}")]
TSTError(#[from] tst::Error),
/// IO error
#[error("IO error: {0}")]
IOError(#[from] std::io::Error),
/// Serialization error
#[error("Serialization error: {0}")]
SerializationError(#[from] serde_json::Error),
/// Signature verification error
#[error("Signature verification error: {0}")]
SignatureError(String),
/// Invalid request error
#[error("Invalid request: {0}")]
InvalidRequest(String),
}

271
_archive/acldb/src/lib.rs Normal file
View File

@@ -0,0 +1,271 @@
mod acl;
mod error;
mod topic;
mod rpc;
pub mod server;
mod utils;
pub use acl::{ACL, ACLRight};
pub use error::Error;
pub use topic::ACLDBTopic;
pub use rpc::RpcInterface;
pub use server::Server;
use std::path::Path;
use std::sync::Arc;
use tokio::sync::RwLock;
use std::collections::HashMap;
use ourdb::OurDB;
use tst::TST;
/// ACLDB represents an access-controlled database instance for a specific circle
pub struct ACLDB {
/// Circle ID
circle_id: String,
/// Base directory path
base_path: String,
/// OurDB instance for the circle
db: Arc<RwLock<OurDB>>,
/// TST instance for key-to-id mapping
tst: Arc<RwLock<TST>>,
/// Cache of loaded ACLs
acl_cache: HashMap<String, ACL>,
/// Topic instances
topics: HashMap<String, Arc<RwLock<ACLDBTopic>>>,
}
impl ACLDB {
/// Creates a new ACLDB instance for the specified circle
pub fn new(circle_id: &str) -> Result<Self, Error> {
let home_dir = dirs::home_dir().ok_or_else(|| Error::PathError("Home directory not found".to_string()))?;
let base_path = home_dir.join("hero/var/ourdb").join(circle_id);
// Create directory if it doesn't exist
std::fs::create_dir_all(&base_path)?;
// Initialize OurDB for the circle
let ourdb_path = base_path.join("data");
std::fs::create_dir_all(&ourdb_path)?;
let db_config = ourdb::OurDBConfig {
path: ourdb_path,
incremental_mode: true,
file_size: None,
keysize: None,
reset: Some(false),
};
let db = OurDB::new(db_config)?;
// Initialize TST for key-to-id mapping
let tst_path = base_path.join("tst").to_string_lossy().to_string();
let tst = TST::new(&tst_path, false)?;
Ok(ACLDB {
circle_id: circle_id.to_string(),
base_path: base_path.to_string_lossy().to_string(),
db: Arc::new(RwLock::new(db)),
tst: Arc::new(RwLock::new(tst)),
acl_cache: HashMap::new(),
topics: HashMap::new(),
})
}
/// Gets a topic instance, creating it if it doesn't exist
pub fn topic(&mut self, topic_name: &str) -> Arc<RwLock<ACLDBTopic>> {
if let Some(topic) = self.topics.get(topic_name) {
return Arc::clone(topic);
}
// Since OurDB and TST don't implement Clone, we'll create new instances
// In a real implementation, we would use a connection pool or similar
let topic = Arc::new(RwLock::new(ACLDBTopic::new(
self.circle_id.clone(),
topic_name.to_string(),
Arc::new(RwLock::new(OurDB::new(ourdb::OurDBConfig {
path: Path::new(&self.base_path).join("data").join(topic_name),
incremental_mode: true,
file_size: None,
keysize: None,
reset: Some(false),
}).unwrap())),
Arc::new(RwLock::new(TST::new(
&Path::new(&self.base_path).join("tst").join(topic_name).to_string_lossy(),
false
).unwrap())),
)));
self.topics.insert(topic_name.to_string(), Arc::clone(&topic));
topic
}
/// Updates or creates an ACL with specified permissions
pub async fn acl_update(&mut self, caller_pubkey: &str, name: &str, pubkeys: &[String], right: ACLRight) -> Result<(), Error> {
// Check if caller has admin rights
self.check_admin_rights(caller_pubkey).await?;
// Get or create the ACL
let mut acl = self.get_or_create_acl(name).await?;
// Update permissions for each public key
for pubkey in pubkeys {
acl.set_permission(pubkey, right);
}
// Save the updated ACL
self.save_acl(&acl).await?;
// Update cache
self.acl_cache.insert(name.to_string(), acl);
Ok(())
}
/// Removes specific public keys from an existing ACL
pub async fn acl_remove(&mut self, caller_pubkey: &str, name: &str, pubkeys: &[String]) -> Result<(), Error> {
// Check if caller has admin rights
self.check_admin_rights(caller_pubkey).await?;
// Get the ACL
let mut acl = self.get_acl(name).await?;
// Remove permissions for each public key
for pubkey in pubkeys {
acl.remove_permission(pubkey);
}
// Save the updated ACL
self.save_acl(&acl).await?;
// Update cache
self.acl_cache.insert(name.to_string(), acl);
Ok(())
}
/// Deletes an entire ACL
pub async fn acl_del(&mut self, caller_pubkey: &str, name: &str) -> Result<(), Error> {
// Check if caller has admin rights
self.check_admin_rights(caller_pubkey).await?;
// Get the ACL to ensure it exists
let _acl = self.get_acl(name).await?;
// Get the ACL topic
let topic = self.topic("acl");
let topic = topic.write().await;
// Delete the ACL
topic.delete(name).await?;
// Remove from cache
self.acl_cache.remove(name);
Ok(())
}
/// Gets an ACL by name
pub async fn get_acl(&mut self, name: &str) -> Result<ACL, Error> {
// Check cache first
if let Some(acl) = self.acl_cache.get(name) {
return Ok(acl.clone());
}
// Get the ACL topic
let topic = self.topic("acl");
let topic = topic.read().await;
// Get the ACL data
let acl_data = topic.get(name).await?;
// Deserialize the ACL
let acl: ACL = serde_json::from_slice(&acl_data)?;
// Update cache
self.acl_cache.insert(name.to_string(), acl.clone());
Ok(acl)
}
/// Gets or creates an ACL
async fn get_or_create_acl(&mut self, name: &str) -> Result<ACL, Error> {
match self.get_acl(name).await {
Ok(acl) => Ok(acl),
Err(_) => {
// Create a new ACL
let acl = ACL::new(name);
// Save the ACL
self.save_acl(&acl).await?;
Ok(acl)
}
}
}
/// Saves an ACL to the database
async fn save_acl(&mut self, acl: &ACL) -> Result<(), Error> {
// Get the ACL topic
let topic = self.topic("acl");
let topic = topic.write().await;
// Serialize the ACL
let acl_data = serde_json::to_vec(acl)?;
// Save the ACL
topic.set_with_acl(&acl.name, &acl_data, 0).await?;
Ok(())
}
/// Checks if a user has admin rights
async fn check_admin_rights(&mut self, pubkey: &str) -> Result<(), Error> {
// Try to get the admin ACL
match self.get_acl("admin").await {
Ok(acl) => {
// Check if the user has admin rights
if acl.has_permission(pubkey, ACLRight::Admin) {
Ok(())
} else {
Err(Error::PermissionDenied)
}
},
Err(_) => {
// If the admin ACL doesn't exist, create it with the caller as admin
let mut acl = ACL::new("admin");
acl.set_permission(pubkey, ACLRight::Admin);
// Save the ACL
self.save_acl(&acl).await?;
// Update cache
self.acl_cache.insert("admin".to_string(), acl);
Ok(())
}
}
}
/// Checks if a caller is the circle owner
fn is_circle_owner(&self, caller_pubkey: &str) -> bool {
// In a real implementation, this would check against the circle's owner
// For now, we'll use a simple check based on the circle ID
// This should be replaced with proper circle ownership verification
let circle_owner_file = Path::new(&self.base_path).join("owner");
if circle_owner_file.exists() {
if let Ok(owner) = std::fs::read_to_string(circle_owner_file) {
return owner.trim() == caller_pubkey;
}
}
// If no owner file exists, check if this is the first admin operation
self.acl_cache.is_empty()
}
}
#[cfg(test)]
mod tests {
use super::*;
// Tests will be added here
}

View File

@@ -0,0 +1,43 @@
use acldb::{Server, Error};
use std::env;
use log::{info, error, LevelFilter};
#[actix_web::main]
async fn main() -> Result<(), Error> {
// Initialize logger
env_logger::Builder::new()
.filter_level(LevelFilter::Info)
.init();
info!("Starting ACLDB server...");
// Parse command line arguments
let args: Vec<String> = env::args().collect();
let host = args.get(1).map_or("127.0.0.1".to_string(), |s| s.clone());
let port = args.get(2)
.map_or(8080, |s| s.parse::<u16>().unwrap_or(8080));
// Create server configuration
let config = acldb::server::ServerConfig {
host,
port,
};
// Create and start server
info!("Server listening on {}:{}", config.host, config.port);
info!("API documentation available at http://{}:{}/redoc", config.host, config.port);
let server = Server::new(config);
// Start the server
match server.start().await {
Ok(_) => {
info!("Server stopped");
Ok(())
}
Err(e) => {
error!("Server error: {}", e);
Err(Error::IOError(e))
}
}
}

258
_archive/acldb/src/rpc.rs Normal file
View File

@@ -0,0 +1,258 @@
use serde::{Serialize, Deserialize};
use crate::error::Error;
use crate::acl::ACLRight;
use std::collections::HashMap;
use utoipa::ToSchema;
/// RPC request structure
///
/// This structure represents an RPC request to the ACLDB API.
/// It contains the method name, parameters, and a signature for authentication.
#[derive(Debug, Clone, Deserialize, ToSchema)]
pub struct RpcRequest {
/// Method name
pub method: String,
/// JSON-encoded arguments
pub params: serde_json::Value,
/// Signature of the JSON data
pub signature: String,
}
/// RPC response structure
///
/// This structure represents the response from an RPC request.
/// It contains either a result or an error message.
#[derive(Debug, Clone, Serialize, ToSchema)]
pub struct RpcResponse {
/// Result of the operation
pub result: Option<serde_json::Value>,
/// Error message if any
pub error: Option<String>,
}
/// ACL update request parameters
///
/// Parameters for updating or creating an ACL with specified permissions.
/// Used with the `acl_update` method.
#[derive(Debug, Deserialize, ToSchema)]
pub struct AclUpdateParams {
/// Public key of the requesting user
pub caller_pubkey: String,
/// ID of the circle where the ACL exists
pub circle_id: String,
/// Unique name for the ACL within the circle
pub name: String,
/// Array of public keys to grant permissions to
pub pubkeys: Vec<String>,
/// Permission level
pub right: String,
}
/// ACL remove request parameters
///
/// Parameters for removing specific public keys from an existing ACL.
/// Used with the `acl_remove` method.
#[derive(Debug, Deserialize, ToSchema)]
pub struct AclRemoveParams {
/// Public key of the requesting user
pub caller_pubkey: String,
/// ID of the circle where the ACL exists
pub circle_id: String,
/// Name of the ACL to modify
pub name: String,
/// Array of public keys to remove from the ACL
pub pubkeys: Vec<String>,
}
/// ACL delete request parameters
///
/// Parameters for deleting an entire ACL.
/// Used with the `acl_del` method.
#[derive(Debug, Deserialize, ToSchema)]
pub struct AclDelParams {
/// Public key of the requesting user
pub caller_pubkey: String,
/// ID of the circle where the ACL exists
pub circle_id: String,
/// Name of the ACL to delete
pub name: String,
}
/// Set request parameters
///
/// Parameters for storing data with optional ACL protection.
/// Used with the `set` method.
#[derive(Debug, Deserialize, ToSchema)]
pub struct SetParams {
/// Public key of the requesting user
pub caller_pubkey: String,
/// ID of the circle where the data belongs
pub circle_id: String,
/// String identifier for the database category
pub topic: String,
/// Optional string key for the record
pub key: Option<String>,
/// Optional numeric ID for direct access
pub id: Option<u32>,
/// Base64-encoded data to store
pub value: String,
/// ID of the ACL to protect this record (0 for public access)
pub acl_id: Option<u32>,
}
/// Delete request parameters
///
/// Parameters for deleting data with ACL verification.
/// Used with the `del` method.
#[derive(Debug, Deserialize, ToSchema)]
pub struct DelParams {
/// Public key of the requesting user
pub caller_pubkey: String,
/// ID of the circle where the data belongs
pub circle_id: String,
/// String identifier for the database category
pub topic: String,
/// Optional string key for the record
pub key: Option<String>,
/// Optional numeric ID for direct access
pub id: Option<u32>,
}
/// Get request parameters
///
/// Parameters for retrieving data with ACL verification.
/// Used with the `get` method.
#[derive(Debug, Deserialize, ToSchema)]
pub struct GetParams {
/// Public key of the requesting user
pub caller_pubkey: String,
/// ID of the circle where the data belongs
pub circle_id: String,
/// String identifier for the database category
pub topic: String,
/// Optional string key for the record
pub key: Option<String>,
/// Optional numeric ID for direct access
pub id: Option<u32>,
}
/// Prefix request parameters
///
/// Parameters for searching for keys with a specific prefix.
/// Used with the `prefix` method.
#[derive(Debug, Deserialize, ToSchema)]
pub struct PrefixParams {
/// Public key of the requesting user
pub caller_pubkey: String,
/// ID of the circle where the data belongs
pub circle_id: String,
/// String identifier for the database category
pub topic: String,
/// Prefix to search for
pub prefix: String,
}
/// RPC interface for handling client requests
pub struct RpcInterface {
/// Map of method names to handler functions
handlers: HashMap<String, Box<dyn Fn(serde_json::Value) -> Result<serde_json::Value, Error> + Send + Sync>>,
}
impl RpcInterface {
/// Creates a new RPC interface
pub fn new() -> Self {
RpcInterface {
handlers: HashMap::new(),
}
}
/// Registers a handler for a method
pub fn register<F>(&mut self, method: &str, handler: F)
where
F: Fn(serde_json::Value) -> Result<serde_json::Value, Error> + Send + Sync + 'static,
{
self.handlers.insert(method.to_string(), Box::new(handler));
}
/// Handles an RPC request
pub fn handle(&self, request: RpcRequest) -> RpcResponse {
// Verify the signature
if let Err(err) = self.verify_signature(&request) {
return RpcResponse {
result: None,
error: Some(err.to_string()),
};
}
// Extract the caller's public key from the signature
let _caller_pubkey = self.extract_pubkey(&request.signature).unwrap_or_default();
// Call the appropriate handler
match self.handlers.get(&request.method) {
Some(handler) => {
match handler(request.params) {
Ok(result) => RpcResponse {
result: Some(result),
error: None,
},
Err(err) => RpcResponse {
result: None,
error: Some(err.to_string()),
},
}
}
None => RpcResponse {
result: None,
error: Some(format!("Method not found: {}", request.method)),
},
}
}
/// Verifies the signature of an RPC request
fn verify_signature(&self, request: &RpcRequest) -> Result<(), Error> {
// In a real implementation, this would verify the cryptographic signature
// For now, we'll just check that the signature is not empty
if request.signature.is_empty() {
return Err(Error::SignatureError("Empty signature".to_string()));
}
Ok(())
}
/// Extracts the public key from a signature
fn extract_pubkey(&self, _signature: &str) -> Result<String, Error> {
// In a real implementation, this would extract the public key from the signature
// For now, we'll just return a placeholder
Ok("extracted_pubkey".to_string())
}
/// Parses a string to an ACLRight enum
pub fn parse_acl_right(right_str: &str) -> Result<ACLRight, Error> {
match right_str.to_lowercase().as_str() {
"read" => Ok(ACLRight::Read),
"write" => Ok(ACLRight::Write),
"delete" => Ok(ACLRight::Delete),
"execute" => Ok(ACLRight::Execute),
"admin" => Ok(ACLRight::Admin),
_ => Err(Error::InvalidRequest(format!("Invalid ACL right: {}", right_str))),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_acl_right() {
let rpc = RpcInterface::new();
assert_eq!(RpcInterface::parse_acl_right("read").unwrap(), ACLRight::Read);
assert_eq!(RpcInterface::parse_acl_right("write").unwrap(), ACLRight::Write);
assert_eq!(RpcInterface::parse_acl_right("delete").unwrap(), ACLRight::Delete);
assert_eq!(RpcInterface::parse_acl_right("execute").unwrap(), ACLRight::Execute);
assert_eq!(RpcInterface::parse_acl_right("admin").unwrap(), ACLRight::Admin);
assert!(RpcInterface::parse_acl_right("invalid").is_err());
}
}

View File

@@ -0,0 +1,520 @@
use actix_web::{web, App, HttpResponse, HttpServer, Responder};
use actix_web::middleware::Logger;
use actix_cors::Cors;
use std::collections::HashMap;
use std::sync::Arc;
use std::future;
use tokio::sync::mpsc;
use tokio::sync::RwLock;
use serde_json::json;
use log::{error, info};
use utoipa::OpenApi;
use utoipa_redoc::{Redoc, Servable};
use crate::ACLDB;
use crate::rpc::{RpcInterface, RpcRequest, RpcResponse, AclUpdateParams, AclRemoveParams, AclDelParams, SetParams, DelParams, GetParams, PrefixParams};
use crate::error::Error;
use crate::utils::base64_decode;
use std::collections::VecDeque;
use tokio::task;
use tokio::time::{sleep, Duration};
/// Server configuration
#[derive(Clone)]
pub struct ServerConfig {
/// Host address
pub host: String,
/// Port number
pub port: u16,
}
impl Default for ServerConfig {
fn default() -> Self {
ServerConfig {
host: "127.0.0.1".to_string(),
port: 8080,
}
}
}
/// Request queue for a circle
struct CircleQueue {
/// Queue of pending requests
queue: VecDeque<(RpcRequest, mpsc::Sender<RpcResponse>)>,
/// Flag to indicate if a worker is currently processing this queue
is_processing: bool,
}
impl CircleQueue {
/// Creates a new circle queue
fn new() -> Self {
CircleQueue {
queue: VecDeque::new(),
is_processing: false,
}
}
/// Adds a request to the queue and starts processing if needed
async fn add_request(
&mut self,
request: RpcRequest,
response_sender: mpsc::Sender<RpcResponse>,
rpc_interface: Arc<RpcInterface>,
acldb_factory: Arc<ACLDBFactory>,
) {
// Add the request to the queue
self.queue.push_back((request.clone(), response_sender));
// If no worker is processing this queue, start one
if !self.is_processing {
self.is_processing = true;
// Clone what we need for the worker
let rpc = Arc::clone(&rpc_interface);
let factory = Arc::clone(&acldb_factory);
let mut queue = self.queue.clone();
// Spawn a worker task
task::spawn(async move {
// Process all requests in the queue
while let Some((req, sender)) = queue.pop_front() {
// Process the request
let response = process_request(&req, &rpc, &factory).await;
// Send the response
if let Err(err) = sender.send(response).await {
error!("Failed to send response: {}", err);
}
// Small delay to prevent CPU hogging
sleep(Duration::from_millis(1)).await;
}
});
}
}
}
/// Factory for creating ACLDB instances
pub struct ACLDBFactory {
/// Map of circle IDs to ACLDB instances
dbs: RwLock<HashMap<String, Arc<RwLock<ACLDB>>>>,
}
impl ACLDBFactory {
/// Creates a new ACLDBFactory
pub fn new() -> Self {
ACLDBFactory {
dbs: RwLock::new(HashMap::new()),
}
}
/// Gets or creates an ACLDB instance for a circle
pub async fn get_or_create(&self, circle_id: &str) -> Result<Arc<RwLock<ACLDB>>, Error> {
// Try to get an existing instance
let dbs = self.dbs.read().await;
if let Some(db) = dbs.get(circle_id) {
return Ok(Arc::clone(db));
}
drop(dbs); // Release the read lock
// Create a new instance
let db = Arc::new(RwLock::new(ACLDB::new(circle_id)?));
// Store it in the map
let mut dbs = self.dbs.write().await;
dbs.insert(circle_id.to_string(), Arc::clone(&db));
Ok(db)
}
}
/// Server for handling RPC requests
#[derive(Clone)]
pub struct Server {
/// Server configuration
config: ServerConfig,
/// RPC interface
rpc: Arc<RpcInterface>,
/// Map of circle IDs to request queues
queues: Arc<RwLock<HashMap<String, CircleQueue>>>,
/// Factory for creating ACLDB instances
acldb_factory: Arc<ACLDBFactory>,
}
impl Server {
/// Creates a new server
pub fn new(config: ServerConfig) -> Self {
let rpc = Arc::new(RpcInterface::new());
let queues = Arc::new(RwLock::new(HashMap::new()));
let acldb_factory = Arc::new(ACLDBFactory::new());
Server {
config,
rpc,
queues,
acldb_factory,
}
}
/// Starts the server
pub async fn start(&self) -> std::io::Result<()> {
let server = self.clone();
// Register RPC handlers
self.register_handlers();
info!("Starting ACLDB server on {}:{}", self.config.host, self.config.port);
info!("API documentation available at: http://{}:{}/redoc", self.config.host, self.config.port);
// Start the HTTP server
HttpServer::new(move || {
App::new()
.app_data(web::Data::new(server.clone()))
.wrap(Logger::default())
.wrap(
Cors::default()
.allow_any_origin()
.allow_any_method()
.allow_any_header()
)
.route("/rpc", web::post().to(handle_rpc))
.route("/health", web::get().to(health_check))
.service(
Redoc::with_url("/redoc", serde_json::to_value(ApiDoc::openapi()).unwrap())
)
})
.bind(format!("{0}:{1}", self.config.host, self.config.port))?
.run()
.await
}
/// Registers RPC handlers
fn register_handlers(&self) {
// Nothing to do here - handlers are now processed dynamically
}
/// Adds a request to the queue for a circle
async fn add_to_queue(&self, circle_id: &str, request: RpcRequest) -> mpsc::Receiver<RpcResponse> {
let (response_sender, response_receiver) = mpsc::channel(1);
// Get or create the queue for this circle
let mut queues = self.queues.write().await;
if !queues.contains_key(circle_id) {
queues.insert(circle_id.to_string(), CircleQueue::new());
}
// Get a mutable reference to the queue
if let Some(queue) = queues.get_mut(circle_id) {
// Add the request to the queue
queue.add_request(
request,
response_sender,
Arc::clone(&self.rpc),
Arc::clone(&self.acldb_factory)
).await;
}
response_receiver
}
}
/// Extracts the circle ID from an RPC request
fn extract_circle_id(request: &web::Json<RpcRequest>) -> Result<String, Error> {
// Extract from different parameter types based on the method
match request.method.as_str() {
"aclupdate" => {
let params: AclUpdateParams = serde_json::from_value(request.params.clone())?;
Ok(params.circle_id)
}
"aclremove" => {
let params: AclRemoveParams = serde_json::from_value(request.params.clone())?;
Ok(params.circle_id)
}
"acldel" => {
let params: AclDelParams = serde_json::from_value(request.params.clone())?;
Ok(params.circle_id)
}
"set" => {
let params: SetParams = serde_json::from_value(request.params.clone())?;
Ok(params.circle_id)
}
"del" => {
let params: DelParams = serde_json::from_value(request.params.clone())?;
Ok(params.circle_id)
}
"get" => {
let params: GetParams = serde_json::from_value(request.params.clone())?;
Ok(params.circle_id)
}
"prefix" => {
let params: PrefixParams = serde_json::from_value(request.params.clone())?;
Ok(params.circle_id)
}
_ => Err(Error::InvalidRequest(format!("Unknown method: {}", request.method))),
}
}
/// API documentation schema
#[derive(OpenApi)]
#[openapi(
paths(
handle_rpc,
health_check
),
components(
schemas(RpcRequest, RpcResponse, AclUpdateParams, AclRemoveParams, AclDelParams, SetParams, DelParams, GetParams, PrefixParams)
),
tags(
(name = "acldb", description = "ACLDB API - Access Control Database"),
(name = "acl", description = "Access Control List Management"),
(name = "data", description = "Data Operations")
),
info(
title = "ACLDB API",
version = "1.0.0",
description = "API for managing access control lists and data operations in HeroDB. This API provides functionality for ACL management and data operations with access control.",
contact(
name = "HeroDB Team",
url = "https://ourworld.tf"
)
)
)]
struct ApiDoc;
/// Handler for RPC requests
///
/// This endpoint handles all RPC requests to the ACLDB API.
/// Supported methods include:
/// - `acl_update`: Update or create an ACL with specified permissions
/// - `acl_remove`: Remove specific public keys from an existing ACL
/// - `acl_del`: Delete an entire ACL
/// - `set`: Store data with optional ACL protection
/// - `get`: Retrieve data with ACL verification
/// - `del`: Delete data with ACL verification
/// - `prefix`: Search for keys with a specific prefix
#[utoipa::path(
post,
path = "/rpc",
request_body = RpcRequest,
responses(
(status = 200, description = "RPC request processed successfully", body = RpcResponse),
(status = 400, description = "Invalid request parameters", body = RpcResponse),
(status = 401, description = "Permission denied", body = RpcResponse),
(status = 404, description = "Resource not found", body = RpcResponse),
(status = 500, description = "Server error", body = RpcResponse)
),
tag = "acldb"
)]
async fn handle_rpc(
server: web::Data<Server>,
request: web::Json<RpcRequest>,
) -> impl Responder {
// Extract the circle ID from the request
let circle_id = match extract_circle_id(&request) {
Ok(id) => id,
Err(err) => {
return HttpResponse::BadRequest().json(RpcResponse {
result: None,
error: Some(err.to_string()),
});
}
};
// Add the request to the queue for this circle
let mut response_receiver = server.add_to_queue(&circle_id, request.0.clone()).await;
// Wait for the response
match response_receiver.recv().await {
Some(response) => HttpResponse::Ok().json(response),
None => HttpResponse::InternalServerError().json(RpcResponse {
result: None,
error: Some("Failed to get response".to_string()),
}),
}
}
/// Process an RPC request
async fn process_request(
request: &RpcRequest,
_rpc_interface: &Arc<RpcInterface>,
acldb_factory: &Arc<ACLDBFactory>
) -> RpcResponse {
match request.method.as_str() {
"aclupdate" => {
match serde_json::from_value::<AclUpdateParams>(request.params.clone()) {
Ok(params) => {
match RpcInterface::parse_acl_right(&params.right) {
Ok(right) => {
match acldb_factory.get_or_create(&params.circle_id).await {
Ok(db) => {
let mut db = db.write().await;
match db.acl_update(&params.caller_pubkey, &params.name, &params.pubkeys, right).await {
Ok(_) => RpcResponse {
result: Some(json!({"success": true})),
error: None,
},
Err(err) => RpcResponse {
result: None,
error: Some(err.to_string()),
},
}
},
Err(err) => RpcResponse {
result: None,
error: Some(err.to_string()),
},
}
},
Err(err) => RpcResponse {
result: None,
error: Some(err.to_string()),
},
}
},
Err(err) => RpcResponse {
result: None,
error: Some(format!("Invalid parameters: {}", err)),
},
}
},
"aclremove" => {
match serde_json::from_value::<AclRemoveParams>(request.params.clone()) {
Ok(params) => {
match acldb_factory.get_or_create(&params.circle_id).await {
Ok(db) => {
let mut db = db.write().await;
match db.acl_remove(&params.caller_pubkey, &params.name, &params.pubkeys).await {
Ok(_) => RpcResponse {
result: Some(json!({"success": true})),
error: None,
},
Err(err) => RpcResponse {
result: None,
error: Some(err.to_string()),
},
}
},
Err(err) => RpcResponse {
result: None,
error: Some(err.to_string()),
},
}
},
Err(err) => RpcResponse {
result: None,
error: Some(format!("Invalid parameters: {}", err)),
},
}
},
"acldel" => {
match serde_json::from_value::<AclDelParams>(request.params.clone()) {
Ok(params) => {
match acldb_factory.get_or_create(&params.circle_id).await {
Ok(db) => {
let mut db = db.write().await;
match db.acl_del(&params.caller_pubkey, &params.name).await {
Ok(_) => RpcResponse {
result: Some(json!({"success": true})),
error: None,
},
Err(err) => RpcResponse {
result: None,
error: Some(err.to_string()),
},
}
},
Err(err) => RpcResponse {
result: None,
error: Some(err.to_string()),
},
}
},
Err(err) => RpcResponse {
result: None,
error: Some(format!("Invalid parameters: {}", err)),
},
}
},
"set" => {
match serde_json::from_value::<SetParams>(request.params.clone()) {
Ok(params) => {
match acldb_factory.get_or_create(&params.circle_id).await {
Ok(db) => {
let mut db = db.write().await;
let topic = db.topic(&params.topic);
match base64_decode(&params.value) {
Ok(value) => {
let acl_id = params.acl_id.unwrap_or(0);
let result = if let Some(key) = params.key {
let topic = topic.write().await;
topic.set_with_acl(&key, &value, acl_id).await
} else if let Some(id) = params.id {
let topic = topic.write().await;
topic.set_with_acl(&id.to_string(), &value, acl_id).await
} else {
// Return a future that resolves to an error for consistency
future::ready(Err(Error::InvalidRequest("Either key or id must be provided".to_string()))).await
};
match result {
Ok(id) => RpcResponse {
result: Some(json!({"id": id})),
error: None,
},
Err(err) => RpcResponse {
result: None,
error: Some(err.to_string()),
},
}
},
Err(err) => RpcResponse {
result: None,
error: Some(err.to_string()),
},
}
},
Err(err) => RpcResponse {
result: None,
error: Some(err.to_string()),
},
}
},
Err(err) => RpcResponse {
result: None,
error: Some(format!("Invalid parameters: {}", err)),
},
}
},
_ => RpcResponse {
result: None,
error: Some(format!("Unknown method: {}", request.method)),
},
}
}
/// Handler for health check
///
/// This endpoint provides a simple health check to verify the server is running.
#[utoipa::path(
get,
path = "/health",
responses(
(status = 200, description = "Server is healthy", body = String)
),
tag = "acldb"
)]
async fn health_check() -> impl Responder {
HttpResponse::Ok().json(json!({"status": "ok"}))
}
#[cfg(test)]
mod tests {
// Tests will be added here
}

344
_archive/acldb/src/topic.rs Normal file
View File

@@ -0,0 +1,344 @@
use std::sync::Arc;
use tokio::sync::RwLock;
use ourdb::{OurDB, OurDBSetArgs};
use tst::TST;
use crate::error::Error;
use crate::acl::{ACL, ACLRight};
/// ACLDBTopic represents a database instance for a specific topic within a circle
pub struct ACLDBTopic {
/// Circle ID
circle_id: String,
/// Topic name
topic: String,
/// OurDB instance
db: Arc<RwLock<OurDB>>,
/// TST instance for key-to-id mapping
tst: Arc<RwLock<TST>>,
}
impl ACLDBTopic {
/// Creates a new ACLDBTopic instance
pub fn new(circle_id: String, topic: String, db: Arc<RwLock<OurDB>>, tst: Arc<RwLock<TST>>) -> Self {
ACLDBTopic {
circle_id,
topic,
db,
tst,
}
}
/// Sets a value in the database with optional ACL protection
pub async fn set(&self, key: &str, value: &[u8]) -> Result<u32, Error> {
self.set_with_acl(key, value, 0).await // 0 means no ACL protection
}
/// Sets a value in the database with ACL protection
pub async fn set_with_acl(&self, key: &str, value: &[u8], acl_id: u32) -> Result<u32, Error> {
// Create the TST key
let tst_key = format!("{}{}", self.topic, key);
// Check if the key already exists in TST
let id = {
// First try to get the ID from TST
let mut id_opt = None;
{
let mut tst = self.tst.write().await;
if let Ok(id_bytes) = tst.list(&tst_key) {
if !id_bytes.is_empty() {
let id_str = String::from_utf8_lossy(&id_bytes[0].as_bytes());
if let Ok(parsed_id) = id_str.parse::<u32>() {
id_opt = Some(parsed_id);
}
}
}
}
// If not found, get a new ID
match id_opt {
Some(id) => id,
None => {
let mut db = self.db.write().await;
db.get_next_id()?
}
}
};
// Prepare the data with ACL ID prefix if needed
let data = if acl_id > 0 {
// Add ACL ID as the first 4 bytes
let mut acl_data = acl_id.to_be_bytes().to_vec();
acl_data.extend_from_slice(value);
acl_data
} else {
value.to_vec()
};
// Store the data in OurDB
{
let mut db = self.db.write().await;
db.set(OurDBSetArgs {
id: Some(id),
data: &data,
})?;
}
// Store the ID in TST
{
let mut tst = self.tst.write().await;
tst.set(&tst_key, id.to_string().into_bytes())?;
}
Ok(id)
}
/// Gets a value from the database
pub async fn get(&self, key: &str) -> Result<Vec<u8>, Error> {
// Create the TST key
let tst_key = format!("{}{}", self.topic, key);
// Get the ID from TST
let id = {
let mut tst = self.tst.write().await;
let keys = tst.list(&tst_key)?;
if keys.is_empty() {
return Err(Error::NotFound);
}
let id_str = &keys[0];
id_str.parse::<u32>().map_err(|_| Error::InvalidOperation("Invalid ID format in TST".to_string()))?
};
// Get the data from OurDB
let data = {
let mut db = self.db.write().await;
db.get(id)?
};
// Check if the data has an ACL ID prefix
if data.len() >= 4 {
let (acl_id_bytes, actual_data) = data.split_at(4);
let acl_id = u32::from_be_bytes([acl_id_bytes[0], acl_id_bytes[1], acl_id_bytes[2], acl_id_bytes[3]]);
if acl_id > 0 {
// This record is ACL-protected, but we're not checking permissions here
// The permission check should be done at a higher level
return Ok(actual_data.to_vec());
}
}
Ok(data)
}
/// Gets a value from the database with permission check
pub async fn get_with_permission(&self, key: &str, caller_pubkey: &str, parent_acls: &[ACL]) -> Result<Vec<u8>, Error> {
// Create the TST key
let tst_key = format!("{}{}", self.topic, key);
// Get the ID from TST
let id = {
let mut tst = self.tst.write().await;
let keys = tst.list(&tst_key)?;
if keys.is_empty() {
return Err(Error::NotFound);
}
let id_str = &keys[0];
id_str.parse::<u32>().map_err(|_| Error::InvalidOperation("Invalid ID format in TST".to_string()))?
};
// Get the data from OurDB
let data = {
let mut db = self.db.write().await;
db.get(id)?
};
// Check if the data has an ACL ID prefix
if data.len() >= 4 {
let (acl_id_bytes, actual_data) = data.split_at(4);
let acl_id = u32::from_be_bytes([acl_id_bytes[0], acl_id_bytes[1], acl_id_bytes[2], acl_id_bytes[3]]);
if acl_id > 0 {
// This record is ACL-protected, check permissions
let acl_name = format!("acl_{}", acl_id);
// Find the ACL in the parent ACLs
let has_permission = parent_acls.iter()
.find(|acl| acl.name == acl_name)
.map(|acl| acl.has_permission(caller_pubkey, ACLRight::Read))
.unwrap_or(false);
if !has_permission {
return Err(Error::PermissionDenied);
}
return Ok(actual_data.to_vec());
}
}
Ok(data)
}
/// Gets a value by ID from the database
pub async fn get_by_id(&self, id: u32) -> Result<Vec<u8>, Error> {
// Get the data from OurDB
let data = {
let mut db = self.db.write().await;
db.get(id)?
};
// Check if the data has an ACL ID prefix
if data.len() >= 4 {
let (_, actual_data) = data.split_at(4);
return Ok(actual_data.to_vec());
}
Ok(data)
}
/// Gets a value by ID from the database with permission check
pub async fn get_by_id_with_permission(&self, id: u32, caller_pubkey: &str, parent_acls: &[ACL]) -> Result<Vec<u8>, Error> {
// Get the data from OurDB
let data = {
let mut db = self.db.write().await;
db.get(id)?
};
// Check if the data has an ACL ID prefix
if data.len() >= 4 {
let (acl_id_bytes, actual_data) = data.split_at(4);
let acl_id = u32::from_be_bytes([acl_id_bytes[0], acl_id_bytes[1], acl_id_bytes[2], acl_id_bytes[3]]);
if acl_id > 0 {
// This record is ACL-protected, check permissions
let acl_name = format!("acl_{}", acl_id);
// Find the ACL in the parent ACLs
let has_permission = parent_acls.iter()
.find(|acl| acl.name == acl_name)
.map(|acl| acl.has_permission(caller_pubkey, ACLRight::Read))
.unwrap_or(false);
if !has_permission {
return Err(Error::PermissionDenied);
}
return Ok(actual_data.to_vec());
}
}
Ok(data)
}
/// Deletes a value from the database
pub async fn delete(&self, key: &str) -> Result<(), Error> {
// Create the TST key
let tst_key = format!("{}{}", self.topic, key);
// Get the ID from TST
let id = {
let mut tst = self.tst.write().await;
let keys = tst.list(&tst_key)?;
if keys.is_empty() {
return Err(Error::NotFound);
}
let id_str = &keys[0];
id_str.parse::<u32>().map_err(|_| Error::InvalidOperation("Invalid ID format in TST".to_string()))?
};
// Delete from OurDB
{
let mut db = self.db.write().await;
db.delete(id)?;
}
// Delete from TST
{
let mut tst = self.tst.write().await;
tst.delete(&tst_key)?;
}
Ok(())
}
/// Deletes a value from the database with permission check
pub async fn delete_with_permission(&self, key: &str, caller_pubkey: &str, parent_acls: &[ACL]) -> Result<(), Error> {
// Create the TST key
let tst_key = format!("{}{}", self.topic, key);
// Get the ID from TST
let id = {
let mut tst = self.tst.write().await;
let keys = tst.list(&tst_key)?;
if keys.is_empty() {
return Err(Error::NotFound);
}
let id_str = &keys[0];
id_str.parse::<u32>().map_err(|_| Error::InvalidOperation("Invalid ID format in TST".to_string()))?
};
// Get the data to check ACL
let data = {
let mut db = self.db.write().await;
db.get(id)?
};
// Check if the data has an ACL ID prefix
if data.len() >= 4 {
let (acl_id_bytes, _) = data.split_at(4);
let acl_id = u32::from_be_bytes([acl_id_bytes[0], acl_id_bytes[1], acl_id_bytes[2], acl_id_bytes[3]]);
if acl_id > 0 {
// This record is ACL-protected, check permissions
let acl_name = format!("acl_{}", acl_id);
// Find the ACL in the parent ACLs
let has_permission = parent_acls.iter()
.find(|acl| acl.name == acl_name)
.map(|acl| acl.has_permission(caller_pubkey, ACLRight::Delete))
.unwrap_or(false);
if !has_permission {
return Err(Error::PermissionDenied);
}
}
}
// Delete from OurDB
{
let mut db = self.db.write().await;
db.delete(id)?
};
// Delete from TST
{
let mut tst = self.tst.write().await;
tst.delete(&tst_key)?
};
Ok(())
}
/// Gets all keys with a given prefix
pub async fn prefix(&self, prefix: &str) -> Result<Vec<String>, Error> {
// Create the TST prefix
let tst_prefix = format!("{}{}", self.topic, prefix);
// Get all keys with the prefix
let keys = {
let mut tst = self.tst.write().await;
tst.list(&tst_prefix)?
};
// Remove the topic prefix from the keys
let topic_prefix = format!("{}", self.topic);
let keys = keys.into_iter()
.map(|key| key.strip_prefix(&topic_prefix).unwrap_or(&key).to_string())
.collect();
Ok(keys)
}
}
#[cfg(test)]
mod tests {
// Tests will be added here
}

View File

@@ -0,0 +1,52 @@
use crate::error::Error;
/// Decodes a base64 string to bytes
pub fn base64_decode(input: &str) -> Result<Vec<u8>, Error> {
base64::decode(input).map_err(|e| Error::InvalidRequest(format!("Invalid base64: {}", e)))
}
/// Encodes bytes to a base64 string
pub fn base64_encode(input: &[u8]) -> String {
base64::encode(input)
}
/// Generates a SHA-256 hash of the input
pub fn sha256_hash(input: &[u8]) -> Vec<u8> {
use sha2::{Sha256, Digest};
let mut hasher = Sha256::new();
hasher.update(input);
hasher.finalize().to_vec()
}
/// Converts a hash to a hex string
pub fn to_hex(bytes: &[u8]) -> String {
hex::encode(bytes)
}
/// Validates a signature against a message and public key
pub fn validate_signature(_message: &[u8], _signature: &str, _pubkey: &str) -> Result<bool, Error> {
// In a real implementation, this would validate the cryptographic signature
// For now, we'll just return true
Ok(true)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_base64() {
let original = b"Hello, world!";
let encoded = base64_encode(original);
let decoded = base64_decode(&encoded).unwrap();
assert_eq!(decoded, original);
}
#[test]
fn test_sha256() {
let input = b"test";
let hash = sha256_hash(input);
let hex = to_hex(&hash);
assert_eq!(hex, "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08");
}
}

View File

@@ -0,0 +1,304 @@
{
"openapi": "3.0.0",
"info": {
"title": "ACLDB API",
"description": "API for the ACLDB module which implements an Access Control List layer on top of the existing ourdb and tst databases.",
"version": "1.0.0"
},
"servers": [
{
"url": "http://localhost:8080",
"description": "Local development server"
}
],
"paths": {
"/rpc": {
"post": {
"summary": "RPC endpoint",
"description": "Handles all RPC requests to the ACLDB system",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/RpcRequest"
}
}
}
},
"responses": {
"200": {
"description": "Successful response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/RpcResponse"
}
}
}
},
"400": {
"description": "Bad request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ErrorResponse"
}
}
}
}
}
}
},
"/health": {
"get": {
"summary": "Health check",
"description": "Returns the health status of the server",
"responses": {
"200": {
"description": "Server is healthy",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"status": {
"type": "string",
"example": "ok"
}
}
}
}
}
}
}
}
}
},
"components": {
"schemas": {
"RpcRequest": {
"type": "object",
"required": ["method", "params", "signature"],
"properties": {
"method": {
"type": "string",
"description": "The name of the method to call",
"example": "set"
},
"params": {
"type": "object",
"description": "The parameters for the method"
},
"signature": {
"type": "string",
"description": "Cryptographic signature of the request"
}
}
},
"RpcResponse": {
"type": "object",
"properties": {
"result": {
"type": "object",
"description": "The result of the method call if successful"
},
"error": {
"type": "string",
"description": "Error message if the method call failed"
}
}
},
"ErrorResponse": {
"type": "object",
"properties": {
"error": {
"type": "string",
"description": "Error message"
}
}
},
"AclUpdateParams": {
"type": "object",
"required": ["caller_pubkey", "circle_id", "name", "pubkeys", "right"],
"properties": {
"caller_pubkey": {
"type": "string",
"description": "Public key of the requesting user"
},
"circle_id": {
"type": "string",
"description": "ID of the circle where the ACL exists"
},
"name": {
"type": "string",
"description": "Unique name for the ACL within the circle"
},
"pubkeys": {
"type": "array",
"items": {
"type": "string"
},
"description": "Array of public keys to grant permissions to"
},
"right": {
"type": "string",
"description": "Permission level (read/write/delete/execute/admin)",
"enum": ["read", "write", "delete", "execute", "admin"]
}
}
},
"AclRemoveParams": {
"type": "object",
"required": ["caller_pubkey", "circle_id", "name", "pubkeys"],
"properties": {
"caller_pubkey": {
"type": "string",
"description": "Public key of the requesting user"
},
"circle_id": {
"type": "string",
"description": "ID of the circle where the ACL exists"
},
"name": {
"type": "string",
"description": "Name of the ACL to modify"
},
"pubkeys": {
"type": "array",
"items": {
"type": "string"
},
"description": "Array of public keys to remove from the ACL"
}
}
},
"AclDelParams": {
"type": "object",
"required": ["caller_pubkey", "circle_id", "name"],
"properties": {
"caller_pubkey": {
"type": "string",
"description": "Public key of the requesting user"
},
"circle_id": {
"type": "string",
"description": "ID of the circle where the ACL exists"
},
"name": {
"type": "string",
"description": "Name of the ACL to delete"
}
}
},
"SetParams": {
"type": "object",
"required": ["caller_pubkey", "circle_id", "topic", "value"],
"properties": {
"caller_pubkey": {
"type": "string",
"description": "Public key of the requesting user"
},
"circle_id": {
"type": "string",
"description": "ID of the circle where the data belongs"
},
"topic": {
"type": "string",
"description": "String identifier for the database category"
},
"key": {
"type": "string",
"description": "Optional string key for the record"
},
"id": {
"type": "integer",
"description": "Optional numeric ID for direct access"
},
"value": {
"type": "string",
"description": "Base64-encoded data to store"
},
"acl_id": {
"type": "integer",
"description": "ID of the ACL to protect this record (0 for public access)"
}
}
},
"DelParams": {
"type": "object",
"required": ["caller_pubkey", "circle_id", "topic"],
"properties": {
"caller_pubkey": {
"type": "string",
"description": "Public key of the requesting user"
},
"circle_id": {
"type": "string",
"description": "ID of the circle where the data belongs"
},
"topic": {
"type": "string",
"description": "String identifier for the database category"
},
"key": {
"type": "string",
"description": "Optional string key for the record"
},
"id": {
"type": "integer",
"description": "Optional numeric ID for direct access"
}
}
},
"GetParams": {
"type": "object",
"required": ["caller_pubkey", "circle_id", "topic"],
"properties": {
"caller_pubkey": {
"type": "string",
"description": "Public key of the requesting user"
},
"circle_id": {
"type": "string",
"description": "ID of the circle where the data belongs"
},
"topic": {
"type": "string",
"description": "String identifier for the database category"
},
"key": {
"type": "string",
"description": "Optional string key for the record"
},
"id": {
"type": "integer",
"description": "Optional numeric ID for direct access"
}
}
},
"PrefixParams": {
"type": "object",
"required": ["caller_pubkey", "circle_id", "topic", "prefix"],
"properties": {
"caller_pubkey": {
"type": "string",
"description": "Public key of the requesting user"
},
"circle_id": {
"type": "string",
"description": "ID of the circle where the data belongs"
},
"topic": {
"type": "string",
"description": "String identifier for the database category"
},
"prefix": {
"type": "string",
"description": "Prefix to search for"
}
}
}
}
}
}

View File

@@ -0,0 +1,54 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>ACLDB API Documentation</title>
<link rel="stylesheet" type="text/css" href="https://unpkg.com/swagger-ui-dist@4.5.0/swagger-ui.css" />
<link rel="icon" type="image/png" href="https://unpkg.com/swagger-ui-dist@4.5.0/favicon-32x32.png" sizes="32x32" />
<link rel="icon" type="image/png" href="https://unpkg.com/swagger-ui-dist@4.5.0/favicon-16x16.png" sizes="16x16" />
<style>
html {
box-sizing: border-box;
overflow: -moz-scrollbars-vertical;
overflow-y: scroll;
}
*,
*:before,
*:after {
box-sizing: inherit;
}
body {
margin: 0;
background: #fafafa;
}
</style>
</head>
<body>
<div id="swagger-ui"></div>
<script src="https://unpkg.com/swagger-ui-dist@4.5.0/swagger-ui-bundle.js"></script>
<script src="https://unpkg.com/swagger-ui-dist@4.5.0/swagger-ui-standalone-preset.js"></script>
<script>
window.onload = function() {
const ui = SwaggerUIBundle({
url: "/openapi.json",
dom_id: '#swagger-ui',
deepLinking: true,
presets: [
SwaggerUIBundle.presets.apis,
SwaggerUIStandalonePreset
],
plugins: [
SwaggerUIBundle.plugins.DownloadUrl
],
layout: "StandaloneLayout"
});
window.ui = ui;
};
</script>
</body>
</html>

5
_archive/herodb_old/.gitignore vendored Normal file
View File

@@ -0,0 +1,5 @@
target/
temp/
tmp/
*.log
*.tmp

2054
_archive/herodb_old/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,57 @@
[package]
name = "herodb"
version = "0.1.0"
edition = "2021"
description = "A database library built on top of ourdb with model support"
license = "MIT"
authors = ["HeroCode Team"]
[dependencies]
ourdb = { path = "../ourdb" }
tst = { path = "../tst" }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
thiserror = "1.0"
uuid = { version = "1.3", features = ["v4", "serde"] }
chrono = { version = "0.4", features = ["serde"] }
bincode = "1.3"
brotli = "3.4"
tempfile = "3.8"
poem = "1.3.55"
poem-openapi = { version = "2.0.11", features = ["swagger-ui"] }
tokio = { version = "1", features = ["full"] }
rhai = "1.21.0"
paste = "1.0"
lazy_static = "1.4.0"
[[example]]
name = "rhai_demo"
path = "examples/rhai_demo.rs"
[[example]]
name = "business_models_demo"
path = "examples/business_models_demo.rs"
[[example]]
name = "ourdb_example"
path = "examples/ourdb_example.rs"
[[example]]
name = "tst_index_example"
path = "examples/tst_index_example.rs"
[[bin]]
name = "dbexample_prod"
path = "src/cmd/dbexample_prod/main.rs"
[[bin]]
name = "dbexample_mcc"
path = "src/cmd/dbexample_mcc/main.rs"
[[bin]]
name = "dbexample_gov"
path = "src/cmd/dbexample_gov/main.rs"
[[bin]]
name = "dbexample_biz"
path = "src/cmd/dbexample_biz/main.rs"

View File

@@ -0,0 +1,94 @@
# HeroDB
A database library built on top of sled with model support.
## example
```bash
#test for mcc module
cargo run --bin dbexample_mcc
#test for governance module
cargo run --bin dbexample_gov
#test for products
cargo run --bin dbexample_prod
```
## Features
- Type-safe database operations
- Builder pattern for model creation
- Transaction support
- Model-specific convenience methods
- Compression for efficient storage
## Usage
### Basic Usage
```rust
use herodb::db::{DB, DBBuilder};
use herodb::models::biz::{Product, ProductBuilder, ProductType, ProductStatus, Currency, CurrencyBuilder};
// Create a database instance
let db = DBBuilder::new("db")
.register_model::<Product>()
.register_model::<Currency>()
.build()
.expect("Failed to create database");
// Create a product using the builder pattern
let price = CurrencyBuilder::new()
.amount(29.99)
.currency_code("USD")
.build()
.expect("Failed to build currency");
let product = ProductBuilder::new()
.id(1)
.name("Premium Service")
.description("Our premium service offering")
.price(price)
.type_(ProductType::Service)
.category("Services")
.status(ProductStatus::Available)
.max_amount(100)
.validity_days(30)
.build()
.expect("Failed to build product");
// Insert the product using the generic method
db.set(&product).expect("Failed to insert product");
// Retrieve the product
let retrieved_product = db.get::<Product>(&"1".to_string()).expect("Failed to retrieve product");
```
### Using Model-Specific Convenience Methods
The library provides model-specific convenience methods for common database operations:
```rust
// Insert a product using the model-specific method
db.insert_product(&product).expect("Failed to insert product");
// Retrieve a product by ID
let retrieved_product = db.get_product(1).expect("Failed to retrieve product");
// List all products
let all_products = db.list_products().expect("Failed to list products");
// Delete a product
db.delete_product(1).expect("Failed to delete product");
```
These methods are available for all registered models:
- `insert_product`, `get_product`, `delete_product`, `list_products` for Product
- `insert_currency`, `get_currency`, `delete_currency`, `list_currencies` for Currency
- `insert_sale`, `get_sale`, `delete_sale`, `list_sales` for Sale
## License
MIT

View File

@@ -0,0 +1,156 @@
please refactor each of the objects in the the chosen folder to use builder paradigm, see below for an example
we always start from root object, each file e.g. product.rs corresponds to a root object, the rootobject is what is stored in the DB, the rest are sub objects which are children of the root object
---
### ✅ Step 1: Define your struct
```rust
#[derive(Debug)]
pub enum ProductType {
Service,
// Other variants...
}
#[derive(Debug)]
pub enum ProductStatus {
Available,
Unavailable,
// Other variants...
}
#[derive(Debug)]
pub struct Product {
id: u32,
name: String,
description: String,
price: f64,
product_type: ProductType,
category: String,
status: ProductStatus,
max_amount: u32,
validity_days: u32,
}
```
---
### ✅ Step 2: Create a builder
```rust
pub struct ProductBuilder {
id: Option<u32>,
name: Option<String>,
description: Option<String>,
price: Option<f64>,
product_type: Option<ProductType>,
category: Option<String>,
status: Option<ProductStatus>,
max_amount: Option<u32>,
validity_days: Option<u32>,
}
impl ProductBuilder {
pub fn new() -> Self {
Self {
id: None,
name: None,
description: None,
price: None,
product_type: None,
category: None,
status: None,
max_amount: None,
validity_days: None,
}
}
pub fn id(mut self, id: u32) -> Self {
self.id = Some(id);
self
}
pub fn name<S: Into<String>>(mut self, name: S) -> Self {
self.name = Some(name.into());
self
}
pub fn description<S: Into<String>>(mut self, description: S) -> Self {
self.description = Some(description.into());
self
}
pub fn price(mut self, price: f64) -> Self {
self.price = Some(price);
self
}
pub fn product_type(mut self, product_type: ProductType) -> Self {
self.product_type = Some(product_type);
self
}
pub fn category<S: Into<String>>(mut self, category: S) -> Self {
self.category = Some(category.into());
self
}
pub fn status(mut self, status: ProductStatus) -> Self {
self.status = Some(status);
self
}
pub fn max_amount(mut self, max_amount: u32) -> Self {
self.max_amount = Some(max_amount);
self
}
pub fn validity_days(mut self, validity_days: u32) -> Self {
self.validity_days = Some(validity_days);
self
}
pub fn build(self) -> Result<Product, &'static str> {
Ok(Product {
id: self.id.ok_or("id is required")?,
name: self.name.ok_or("name is required")?,
description: self.description.ok_or("description is required")?,
price: self.price.ok_or("price is required")?,
product_type: self.product_type.ok_or("type is required")?,
category: self.category.ok_or("category is required")?,
status: self.status.ok_or("status is required")?,
max_amount: self.max_amount.ok_or("max_amount is required")?,
validity_days: self.validity_days.ok_or("validity_days is required")?,
})
}
}
```
---
### ✅ Step 3: Use it like this
```rust
let product = ProductBuilder::new()
.id(1)
.name("Premium Service")
.description("Our premium service offering")
.price(99.99)
.product_type(ProductType::Service)
.category("Services")
.status(ProductStatus::Available)
.max_amount(100)
.validity_days(30)
.build()
.expect("Failed to build product");
```
---
This way:
- You dont need to remember the order of parameters.
- You get readable, self-documenting code.
- Its easier to provide defaults or optional values if you want later.
Want help generating this automatically via a macro or just want it shorter? I can show you a derive macro to do that too.

View File

@@ -0,0 +1,9 @@
make a readme for the chosen folder (module)
make a dense representation of the objects and how to use them
explain what the rootobjects are
we always start from root object, each file e.g. product.rs corresponds to a root object, the rootobject is what is stored in the DB, the rest are sub objects which are children of the root object
don't explain the low level implementation details like sled, ...

View File

@@ -0,0 +1,994 @@
# Best Practices for Wrapping Rust Functions with Rhai
This document provides comprehensive guidance on how to effectively wrap Rust functions with different standard arguments, pass structs, and handle various return types including errors when using the Rhai scripting language.
## Table of Contents
1. [Introduction](#introduction)
2. [Basic Function Registration](#basic-function-registration)
3. [Working with Different Argument Types](#working-with-different-argument-types)
4. [Passing and Working with Structs](#passing-and-working-with-structs)
5. [Error Handling](#error-handling)
6. [Returning Different Types](#returning-different-types)
7. [Native Function Handling](#native-function-handling)
8. [Advanced Patterns](#advanced-patterns)
9. [Complete Examples](#complete-examples)
## Introduction
Rhai is an embedded scripting language for Rust that allows you to expose Rust functions to scripts and vice versa. This document focuses on the best practices for wrapping Rust functions so they can be called from Rhai scripts, with special attention to handling different argument types, structs, and error conditions.
## Basic Function Registration
### Simple Function Registration
The most basic way to register a Rust function with Rhai is using the `register_fn` method:
```rust
fn add(x: i64, y: i64) -> i64 {
x + y
}
fn main() -> Result<(), Box<EvalAltResult>> {
let mut engine = Engine::new();
// Register the function with Rhai
engine.register_fn("add", add);
// Now the function can be called from Rhai scripts
let result = engine.eval::<i64>("add(40, 2)")?;
println!("Result: {}", result); // prints 42
Ok(())
}
```
### Function Naming Conventions
When registering functions, follow these naming conventions:
1. Use snake_case for function names to maintain consistency with Rhai's style
2. Choose descriptive names that clearly indicate the function's purpose
3. For functions that operate on specific types, consider prefixing with the type name (e.g., `string_length`)
## Working with Different Argument Types
### Primitive Types
Rhai supports the following primitive types that can be directly used as function arguments:
- `i64` (integer)
- `f64` (float)
- `bool` (boolean)
- `String` or `&str` (string)
- `char` (character)
- `()` (unit type)
Example:
```rust
fn calculate(num: i64, factor: f64, enabled: bool) -> f64 {
if enabled {
num as f64 * factor
} else {
0.0
}
}
engine.register_fn("calculate", calculate);
```
### Arrays and Collections
For array arguments:
```rust
fn sum_array(arr: Array) -> i64 {
arr.iter()
.filter_map(|v| v.as_int().ok())
.sum()
}
engine.register_fn("sum_array", sum_array);
```
### Optional Arguments and Function Overloading
Rhai supports function overloading, which allows you to register multiple functions with the same name but different parameter types or counts:
```rust
fn greet(name: &str) -> String {
format!("Hello, {}!", name)
}
fn greet_with_title(title: &str, name: &str) -> String {
format!("Hello, {} {}!", title, name)
}
engine.register_fn("greet", greet);
engine.register_fn("greet", greet_with_title);
// In Rhai:
// greet("World") -> "Hello, World!"
// greet("Mr.", "Smith") -> "Hello, Mr. Smith!"
```
## Passing and Working with Structs
### Registering Custom Types
To use Rust structs in Rhai, you need to register them:
#### Method 1: Using the CustomType Trait (Recommended)
```rust
#[derive(Debug, Clone, CustomType)]
#[rhai_type(extra = Self::build_extra)]
struct TestStruct {
x: i64,
}
impl TestStruct {
pub fn new() -> Self {
Self { x: 1 }
}
pub fn update(&mut self) {
self.x += 1000;
}
pub fn calculate(&mut self, data: i64) -> i64 {
self.x * data
}
fn build_extra(builder: &mut TypeBuilder<Self>) {
builder
.with_name("TestStruct")
.with_fn("new_ts", Self::new)
.with_fn("update", Self::update)
.with_fn("calc", Self::calculate);
}
}
// In your main function:
let mut engine = Engine::new();
engine.build_type::<TestStruct>();
```
#### Method 2: Manual Registration
```rust
#[derive(Debug, Clone)]
struct TestStruct {
x: i64,
}
impl TestStruct {
pub fn new() -> Self {
Self { x: 1 }
}
pub fn update(&mut self) {
self.x += 1000;
}
}
let mut engine = Engine::new();
engine
.register_type_with_name::<TestStruct>("TestStruct")
.register_fn("new_ts", TestStruct::new)
.register_fn("update", TestStruct::update);
```
### Accessing Struct Fields
By default, Rhai can access public fields of registered structs:
```rust
// In Rhai script:
let x = new_ts();
x.x = 42; // Direct field access
```
### Passing Structs as Arguments
When passing structs as arguments to functions, ensure they implement the `Clone` trait:
```rust
fn process_struct(test: TestStruct) -> i64 {
test.x * 2
}
engine.register_fn("process_struct", process_struct);
```
### Returning Structs from Functions
You can return custom structs from functions:
```rust
fn create_struct(value: i64) -> TestStruct {
TestStruct { x: value }
}
engine.register_fn("create_struct", create_struct);
```
## Error Handling
Error handling is a critical aspect of integrating Rust functions with Rhai. Proper error handling ensures that script execution fails gracefully with meaningful error messages.
### Basic Error Handling
The most basic way to handle errors is to return a `Result` type:
```rust
fn divide(a: i64, b: i64) -> Result<i64, Box<EvalAltResult>> {
if b == 0 {
// Return an error if division by zero
Err("Division by zero".into())
} else {
Ok(a / b)
}
}
engine.register_fn("divide", divide);
```
### EvalAltResult Types
Rhai provides several error types through the `EvalAltResult` enum:
```rust
use rhai::EvalAltResult;
use rhai::Position;
fn my_function() -> Result<i64, Box<EvalAltResult>> {
// Different error types
// Runtime error - general purpose error
return Err(Box::new(EvalAltResult::ErrorRuntime(
"Something went wrong".into(),
Position::NONE
)));
// Type error - when a type mismatch occurs
return Err(Box::new(EvalAltResult::ErrorMismatchOutputType(
"expected i64, got string".into(),
Position::NONE,
"i64".into()
)));
// Function not found error
return Err(Box::new(EvalAltResult::ErrorFunctionNotFound(
"function_name".into(),
Position::NONE
)));
}
```
### Custom Error Types
For more structured error handling, you can create custom error types:
```rust
use thiserror::Error;
use rhai::{EvalAltResult, Position};
#[derive(Error, Debug)]
enum MyError {
#[error("Invalid input: {0}")]
InvalidInput(String),
#[error("Calculation error: {0}")]
CalculationError(String),
#[error("Database error: {0}")]
DatabaseError(String),
}
// Convert your custom error to EvalAltResult
fn process_data(input: i64) -> Result<i64, Box<EvalAltResult>> {
// Your logic here that might return a custom error
let result = validate_input(input)
.map_err(|e| Box::new(EvalAltResult::ErrorRuntime(
format!("Validation failed: {}", e),
Position::NONE
)))?;
let processed = calculate(result)
.map_err(|e| Box::new(EvalAltResult::ErrorRuntime(
format!("Calculation failed: {}", e),
Position::NONE
)))?;
if processed < 0 {
return Err(Box::new(EvalAltResult::ErrorRuntime(
"Negative result not allowed".into(),
Position::NONE
)));
}
Ok(processed)
}
// Helper functions that return our custom error type
fn validate_input(input: i64) -> Result<i64, MyError> {
if input <= 0 {
return Err(MyError::InvalidInput("Input must be positive".into()));
}
Ok(input)
}
fn calculate(value: i64) -> Result<i64, MyError> {
if value > 1000 {
return Err(MyError::CalculationError("Value too large".into()));
}
Ok(value * 2)
}
```
### Error Propagation
When calling Rhai functions from Rust, errors are propagated through the `?` operator:
```rust
let result = engine.eval::<i64>("divide(10, 0)")?; // This will propagate the error
```
### Error Context and Position Information
For better debugging, include position information in your errors:
```rust
fn parse_config(config: &str) -> Result<Map, Box<EvalAltResult>> {
// Get the call position from the context
let pos = Position::NONE; // In a real function, you'd get this from NativeCallContext
match serde_json::from_str::<serde_json::Value>(config) {
Ok(json) => {
// Convert JSON to Rhai Map
let mut map = Map::new();
// ... conversion logic ...
Ok(map)
},
Err(e) => {
Err(Box::new(EvalAltResult::ErrorRuntime(
format!("Failed to parse config: {}", e),
pos
)))
}
}
}
```
### Best Practices for Error Handling
1. **Be Specific**: Provide clear, specific error messages that help script writers understand what went wrong
2. **Include Context**: When possible, include relevant context in error messages (e.g., variable values, expected types)
3. **Consistent Error Types**: Use consistent error types for similar issues
4. **Validate Early**: Validate inputs at the beginning of functions to fail fast
5. **Document Error Conditions**: Document possible error conditions for functions exposed to Rhai
## Returning Different Types
Properly handling return types is crucial for creating a seamless integration between Rust and Rhai. This section covers various approaches to returning different types of data from Rust functions to Rhai scripts.
### Simple Return Types
For simple return types, specify the type when registering the function:
```rust
fn get_number() -> i64 { 42 }
fn get_string() -> String { "hello".to_string() }
fn get_boolean() -> bool { true }
fn get_float() -> f64 { 3.14159 }
fn get_char() -> char { 'A' }
fn get_unit() -> () { () }
engine.register_fn("get_number", get_number);
engine.register_fn("get_string", get_string);
engine.register_fn("get_boolean", get_boolean);
engine.register_fn("get_float", get_float);
engine.register_fn("get_char", get_char);
engine.register_fn("get_unit", get_unit);
```
### Dynamic Return Types
WE SHOULD TRY NOT TO DO THIS
For functions that may return different types based on conditions, use the `Dynamic` type:
```rust
fn get_value(which: i64) -> Dynamic {
match which {
0 => Dynamic::from(42),
1 => Dynamic::from("hello"),
2 => Dynamic::from(true),
3 => Dynamic::from(3.14159),
4 => {
let mut array = Array::new();
array.push(Dynamic::from(1));
array.push(Dynamic::from(2));
Dynamic::from_array(array)
},
5 => {
let mut map = Map::new();
map.insert("key".into(), "value".into());
Dynamic::from_map(map)
},
_ => Dynamic::UNIT,
}
}
engine.register_fn("get_value", get_value);
```
### Returning Collections
Rhai supports various collection types:
```rust
// Returning an array
fn get_array() -> Array {
let mut array = Array::new();
array.push(Dynamic::from(1));
array.push(Dynamic::from("hello"));
array.push(Dynamic::from(true));
array
}
// Returning a map
fn get_map() -> Map {
let mut map = Map::new();
map.insert("number".into(), 42.into());
map.insert("string".into(), "hello".into());
map.insert("boolean".into(), true.into());
map
}
// Returning a typed Vec (will be converted to Rhai Array)
fn get_numbers() -> Vec<i64> {
vec![1, 2, 3, 4, 5]
}
// Returning a HashMap (will be converted to Rhai Map)
fn get_config() -> HashMap<String, String> {
let mut map = HashMap::new();
map.insert("host".to_string(), "localhost".to_string());
map.insert("port".to_string(), "8080".to_string());
map
}
engine.register_fn("get_array", get_array);
engine.register_fn("get_map", get_map);
engine.register_fn("get_numbers", get_numbers);
engine.register_fn("get_config", get_config);
```
### Returning Custom Structs
For returning custom structs, ensure they implement the `Clone` trait:
```rust
#[derive(Debug, Clone)]
struct TestStruct {
x: i64,
name: String,
active: bool,
}
fn create_struct(value: i64, name: &str, active: bool) -> TestStruct {
TestStruct {
x: value,
name: name.to_string(),
active
}
}
fn get_struct_array() -> Vec<TestStruct> {
vec![
TestStruct { x: 1, name: "one".to_string(), active: true },
TestStruct { x: 2, name: "two".to_string(), active: false },
]
}
engine.register_type_with_name::<TestStruct>("TestStruct")
.register_fn("create_struct", create_struct)
.register_fn("get_struct_array", get_struct_array);
```
### Returning Results and Options
For functions that might fail or return optional values:
```rust
// Returning a Result
fn divide(a: i64, b: i64) -> Result<i64, Box<EvalAltResult>> {
if b == 0 {
Err("Division by zero".into())
} else {
Ok(a / b)
}
}
// Returning an Option (converted to Dynamic)
fn find_item(id: i64) -> Dynamic {
let item = lookup_item(id);
match item {
Some(value) => value.into(),
None => Dynamic::UNIT, // Rhai has no null, so use () for None
}
}
// Helper function returning Option
fn lookup_item(id: i64) -> Option<TestStruct> {
match id {
1 => Some(TestStruct { x: 1, name: "one".to_string(), active: true }),
2 => Some(TestStruct { x: 2, name: "two".to_string(), active: false }),
_ => None,
}
}
engine.register_fn("divide", divide);
engine.register_fn("find_item", find_item);
```
### Serialization and Deserialization
When working with JSON or other serialized formats:
```rust
use serde_json::{Value as JsonValue, json};
// Return JSON data as a Rhai Map
fn get_json_data() -> Result<Map, Box<EvalAltResult>> {
// Simulate fetching JSON data
let json_data = json!({
"name": "John Doe",
"age": 30,
"address": {
"street": "123 Main St",
"city": "Anytown"
},
"phones": ["+1-555-1234", "+1-555-5678"]
});
// Convert JSON to Rhai Map
json_to_rhai_value(json_data)
.and_then(|v| v.try_cast::<Map>().map_err(|_| "Expected a map".into()))
}
// Helper function to convert JSON Value to Rhai Dynamic
fn json_to_rhai_value(json: JsonValue) -> Result<Dynamic, Box<EvalAltResult>> {
match json {
JsonValue::Null => Ok(Dynamic::UNIT),
JsonValue::Bool(b) => Ok(b.into()),
JsonValue::Number(n) => {
if n.is_i64() {
Ok(n.as_i64().unwrap().into())
} else {
Ok(n.as_f64().unwrap().into())
}
},
JsonValue::String(s) => Ok(s.into()),
JsonValue::Array(arr) => {
let mut rhai_array = Array::new();
for item in arr {
rhai_array.push(json_to_rhai_value(item)?);
}
Ok(Dynamic::from_array(rhai_array))
},
JsonValue::Object(obj) => {
let mut rhai_map = Map::new();
for (k, v) in obj {
rhai_map.insert(k.into(), json_to_rhai_value(v)?);
}
Ok(Dynamic::from_map(rhai_map))
}
}
}
engine.register_fn("get_json_data", get_json_data);
```
### Working with Dynamic Type System
Understanding how to work with Rhai's Dynamic type system is essential:
```rust
// Function that examines a Dynamic value and returns information about it
fn inspect_value(value: Dynamic) -> Map {
let mut info = Map::new();
// Store the type name
info.insert("type".into(), value.type_name().into());
// Store specific type information
if value.is_int() {
info.insert("category".into(), "number".into());
info.insert("value".into(), value.clone());
} else if value.is_float() {
info.insert("category".into(), "number".into());
info.insert("value".into(), value.clone());
} else if value.is_string() {
info.insert("category".into(), "string".into());
info.insert("length".into(), value.clone_cast::<String>().len().into());
info.insert("value".into(), value.clone());
} else if value.is_array() {
info.insert("category".into(), "array".into());
info.insert("length".into(), value.clone_cast::<Array>().len().into());
} else if value.is_map() {
info.insert("category".into(), "map".into());
info.insert("keys".into(), value.clone_cast::<Map>().keys().len().into());
} else if value.is_bool() {
info.insert("category".into(), "boolean".into());
info.insert("value".into(), value.clone());
} else {
info.insert("category".into(), "other".into());
}
info
}
engine.register_fn("inspect", inspect_value);
```
## Native Function Handling
When working with native Rust functions in Rhai, there are several important considerations for handling different argument types, especially when dealing with complex data structures and error cases.
### Native Function Signature
Native Rust functions registered with Rhai can have one of two signatures:
1. **Standard Function Signature**: Functions with typed parameters
```rust
fn my_function(param1: Type1, param2: Type2, ...) -> ReturnType { ... }
```
2. **Dynamic Function Signature**: Functions that handle raw Dynamic values
```rust
fn my_dynamic_function(context: NativeCallContext, args: &mut [&mut Dynamic]) -> Result<Dynamic, Box<EvalAltResult>> { ... }
```
### Working with Raw Dynamic Arguments
The dynamic function signature gives you more control but requires manual type checking and conversion:
```rust
fn process_dynamic_args(context: NativeCallContext, args: &mut [&mut Dynamic]) -> Result<Dynamic, Box<EvalAltResult>> {
// Check number of arguments
if args.len() != 2 {
return Err("Expected exactly 2 arguments".into());
}
// Extract and convert the first argument to an integer
let arg1 = args[0].as_int().map_err(|_| "First argument must be an integer".into())?;
// Extract and convert the second argument to a string
let arg2 = args[1].as_str().map_err(|_| "Second argument must be a string".into())?;
// Process the arguments
let result = format!("{}: {}", arg2, arg1);
// Return the result as a Dynamic value
Ok(result.into())
}
// Register the function
engine.register_fn("process", process_dynamic_args);
```
### Handling Complex Struct Arguments
When working with complex struct arguments, you have several options:
#### Option 1: Use typed parameters (recommended for simple cases)
```rust
#[derive(Clone)]
struct ComplexData {
id: i64,
values: Vec<f64>,
}
fn process_complex(data: &mut ComplexData, factor: f64) -> f64 {
let sum: f64 = data.values.iter().sum();
data.values.push(sum * factor);
sum * factor
}
engine.register_fn("process_complex", process_complex);
```
#### Option 2: Use Dynamic parameters for more flexibility
```rust
fn process_complex_dynamic(context: NativeCallContext, args: &mut [&mut Dynamic]) -> Result<Dynamic, Box<EvalAltResult>> {
// Check arguments
if args.len() != 2 {
return Err("Expected exactly 2 arguments".into());
}
// Get mutable reference to the complex data
let data = args[0].write_lock::<ComplexData>()
.ok_or_else(|| "First argument must be ComplexData".into())?;
// Get the factor
let factor = args[1].as_float().map_err(|_| "Second argument must be a number".into())?;
// Process the data
let sum: f64 = data.values.iter().sum();
data.values.push(sum * factor);
Ok((sum * factor).into())
}
```
### Handling Variable Arguments
For functions that accept a variable number of arguments:
```rust
fn sum_all(context: NativeCallContext, args: &mut [&mut Dynamic]) -> Result<Dynamic, Box<EvalAltResult>> {
let mut total: i64 = 0;
for arg in args.iter() {
total += arg.as_int().map_err(|_| "All arguments must be integers".into())?;
}
Ok(total.into())
}
engine.register_fn("sum_all", sum_all);
// In Rhai:
// sum_all(1, 2, 3, 4, 5) -> 15
// sum_all(10, 20) -> 30
```
### Handling Optional Arguments
For functions with optional arguments, use function overloading:
```rust
fn create_person(name: &str) -> Person {
Person { name: name.to_string(), age: 30 } // Default age
}
fn create_person_with_age(name: &str, age: i64) -> Person {
Person { name: name.to_string(), age }
}
engine.register_fn("create_person", create_person);
engine.register_fn("create_person", create_person_with_age);
// In Rhai:
// create_person("John") -> Person with name "John" and age 30
// create_person("John", 25) -> Person with name "John" and age 25
```
### Handling Default Arguments
Rhai doesn't directly support default arguments, but you can simulate them:
```rust
fn configure(options: &mut Map) -> Result<(), Box<EvalAltResult>> {
// Check if certain options exist, if not, set defaults
if !options.contains_key("timeout") {
options.insert("timeout".into(), 30_i64.into());
}
if !options.contains_key("retry") {
options.insert("retry".into(), true.into());
}
Ok(())
}
engine.register_fn("configure", configure);
// In Rhai:
// let options = #{};
// configure(options);
// print(options.timeout); // Prints 30
```
### Handling Mutable and Immutable References
Rhai supports both mutable and immutable references:
```rust
// Function taking an immutable reference
fn get_name(person: &Person) -> String {
person.name.clone()
}
// Function taking a mutable reference
fn increment_age(person: &mut Person) {
person.age += 1;
}
engine.register_fn("get_name", get_name);
engine.register_fn("increment_age", increment_age);
```
### Converting Between Rust and Rhai Types
When you need to convert between Rust and Rhai types:
```rust
// Convert a Rust HashMap to a Rhai Map
fn create_config() -> Map {
let mut rust_map = HashMap::new();
rust_map.insert("server".to_string(), "localhost".to_string());
rust_map.insert("port".to_string(), "8080".to_string());
// Convert to Rhai Map
let mut rhai_map = Map::new();
for (k, v) in rust_map {
rhai_map.insert(k.into(), v.into());
}
rhai_map
}
// Convert a Rhai Array to a Rust Vec
fn process_array(arr: Array) -> Result<i64, Box<EvalAltResult>> {
// Convert to Rust Vec<i64>
let rust_vec: Result<Vec<i64>, _> = arr.iter()
.map(|v| v.as_int().map_err(|_| "Array must contain only integers".into()))
.collect();
let numbers = rust_vec?;
Ok(numbers.iter().sum())
}
```
## Complete Examples
### Example 1: Basic Function Registration and Struct Handling
```rust
use rhai::{Engine, EvalAltResult, RegisterFn};
#[derive(Debug, Clone)]
struct Person {
name: String,
age: i64,
}
impl Person {
fn new(name: &str, age: i64) -> Self {
Self {
name: name.to_string(),
age,
}
}
fn greet(&self) -> String {
format!("Hello, my name is {} and I am {} years old.", self.name, self.age)
}
fn have_birthday(&mut self) {
self.age += 1;
}
}
fn is_adult(person: &Person) -> bool {
person.age >= 18
}
fn main() -> Result<(), Box<EvalAltResult>> {
let mut engine = Engine::new();
// Register the Person type
engine
.register_type_with_name::<Person>("Person")
.register_fn("new_person", Person::new)
.register_fn("greet", Person::greet)
.register_fn("have_birthday", Person::have_birthday)
.register_fn("is_adult", is_adult);
// Run a script that uses the Person type
let result = engine.eval::<String>(r#"
let p = new_person("John", 17);
let greeting = p.greet();
if !is_adult(p) {
p.have_birthday();
}
greeting + " Now I am " + p.age.to_string() + " years old."
"#)?;
println!("{}", result);
Ok(())
}
```
### Example 2: Error Handling and Complex Return Types
```rust
use rhai::{Engine, EvalAltResult, Map, Dynamic};
use std::collections::HashMap;
#[derive(Debug, Clone)]
struct Product {
id: i64,
name: String,
price: f64,
}
fn get_product(id: i64) -> Result<Product, Box<EvalAltResult>> {
match id {
1 => Ok(Product { id: 1, name: "Laptop".to_string(), price: 999.99 }),
2 => Ok(Product { id: 2, name: "Phone".to_string(), price: 499.99 }),
_ => Err("Product not found".into())
}
}
fn calculate_total(products: Array) -> Result<f64, Box<EvalAltResult>> {
let mut total = 0.0;
for product_dynamic in products.iter() {
let product = product_dynamic.clone().try_cast::<Product>()
.map_err(|_| "Invalid product in array".into())?;
total += product.price;
}
Ok(total)
}
fn get_product_map() -> Map {
let mut map = Map::new();
map.insert("laptop".into(),
Dynamic::from(Product { id: 1, name: "Laptop".to_string(), price: 999.99 }));
map.insert("phone".into(),
Dynamic::from(Product { id: 2, name: "Phone".to_string(), price: 499.99 }));
map
}
fn main() -> Result<(), Box<EvalAltResult>> {
let mut engine = Engine::new();
engine
.register_type_with_name::<Product>("Product")
.register_fn("get_product", get_product)
.register_fn("calculate_total", calculate_total)
.register_fn("get_product_map", get_product_map);
let result = engine.eval::<f64>(r#"
let products = [];
// Try to get products
try {
products.push(get_product(1));
products.push(get_product(2));
products.push(get_product(3)); // This will throw an error
} catch(err) {
print(`Error: ${err}`);
}
// Get products from map
let product_map = get_product_map();
products.push(product_map.laptop);
calculate_total(products)
"#)?;
println!("Total: ${:.2}", result);
Ok(())
}
```

View File

@@ -0,0 +1,134 @@
### Error Handling in Dynamic Functions
When working with the dynamic function signature, error handling is slightly different:
```rust
fn dynamic_function(ctx: NativeCallContext, args: &mut [&mut Dynamic]) -> Result<Dynamic, Box<EvalAltResult>> {
// Get the position information from the context
let pos = ctx.position();
// Validate arguments
if args.len() < 2 {
return Err(Box::new(EvalAltResult::ErrorRuntime(
format!("Expected at least 2 arguments, got {}", args.len()),
pos
)));
}
// Try to convert arguments with proper error handling
let arg1 = match args[0].as_int() {
Ok(val) => val,
Err(_) => return Err(Box::new(EvalAltResult::ErrorMismatchOutputType(
"Expected first argument to be an integer".into(),
pos,
"i64".into()
)))
};
// Process with error handling
if arg1 <= 0 {
return Err(Box::new(EvalAltResult::ErrorRuntime(
"First argument must be positive".into(),
pos
)));
}
// Return success
Ok(Dynamic::from(arg1 * 2))
}
```
## Advanced Patterns
### Working with Function Pointers
You can create function pointers that bind to Rust functions:
```rust
fn my_awesome_fn(ctx: NativeCallContext, args: &mut[&mut Dynamic]) -> Result<Dynamic, Box<EvalAltResult>> {
// Check number of arguments
if args.len() != 2 {
return Err("one argument is required, plus the object".into());
}
// Get call arguments
let x = args[1].try_cast::<i64>().map_err(|_| "argument must be an integer".into())?;
// Get mutable reference to the object map, which is passed as the first argument
let map = &mut *args[0].as_map_mut().map_err(|_| "object must be a map".into())?;
// Do something awesome here ...
let result = x * 2;
Ok(result.into())
}
// Register a function to create a pre-defined object
engine.register_fn("create_awesome_object", || {
// Use an object map as base
let mut map = Map::new();
// Create a function pointer that binds to 'my_awesome_fn'
let fp = FnPtr::from_fn("awesome", my_awesome_fn)?;
// ^ name of method
// ^ native function
// Store the function pointer in the object map
map.insert("awesome".into(), fp.into());
Ok(Dynamic::from_map(map))
});
```
### Creating Rust Closures from Rhai Functions
You can encapsulate a Rhai script as a Rust closure:
```rust
use rhai::{Engine, Func};
let engine = Engine::new();
let script = "fn calc(x, y) { x + y.len < 42 }";
// Create a Rust closure from a Rhai function
let func = Func::<(i64, &str), bool>::create_from_script(
engine, // the 'Engine' is consumed into the closure
script, // the script
"calc" // the entry-point function name
)?;
// Call the closure
let result = func(123, "hello")?;
// Pass it as a callback to another function
schedule_callback(func);
```
### Calling Rhai Functions from Rust
You can call Rhai functions from Rust:
```rust
// Compile the script to AST
let ast = engine.compile(script)?;
// Create a custom 'Scope'
let mut scope = Scope::new();
// Add variables to the scope
scope.push("my_var", 42_i64);
scope.push("my_string", "hello, world!");
scope.push_constant("MY_CONST", true);
// Call a function defined in the script
let result = engine.call_fn::<i64>(&mut scope, &ast, "hello", ("abc", 123_i64))?;
// For a function with one parameter, use a tuple with a trailing comma
let result = engine.call_fn::<i64>(&mut scope, &ast, "hello", (123_i64,))?;
// For a function with no parameters
let result = engine.call_fn::<i64>(&mut scope, &ast, "hello", ())?;
```

View File

@@ -0,0 +1,187 @@
## Best Practices and Optimization
When wrapping Rust functions for use with Rhai, following these best practices will help you create efficient, maintainable, and robust code.
### Performance Considerations
1. **Minimize Cloning**: Rhai often requires cloning data, but you can minimize this overhead:
```rust
// Prefer immutable references when possible
fn process_data(data: &MyStruct) -> i64 {
// Work with data without cloning
data.value * 2
}
// Use mutable references for in-place modifications
fn update_data(data: &mut MyStruct) {
data.value += 1;
}
```
2. **Avoid Excessive Type Conversions**: Converting between Rhai's Dynamic type and Rust types has overhead:
```rust
// Inefficient - multiple conversions
fn process_inefficient(ctx: NativeCallContext, args: &mut [&mut Dynamic]) -> Result<Dynamic, Box<EvalAltResult>> {
let value = args[0].as_int()?;
let result = value * 2;
Ok(Dynamic::from(result))
}
// More efficient - use typed parameters when possible
fn process_efficient(value: i64) -> i64 {
value * 2
}
```
3. **Batch Operations**: For operations on collections, batch processing is more efficient:
```rust
// Process an entire array at once rather than element by element
fn sum_array(arr: Array) -> Result<i64, Box<EvalAltResult>> {
arr.iter()
.map(|v| v.as_int())
.collect::<Result<Vec<i64>, _>>()
.map(|nums| nums.iter().sum())
.map_err(|_| "Array must contain only integers".into())
}
```
4. **Compile Scripts Once**: Reuse compiled ASTs for scripts that are executed multiple times:
```rust
// Compile once
let ast = engine.compile(script)?;
// Execute multiple times with different parameters
for i in 0..10 {
let result = engine.eval_ast::<i64>(&ast)?;
println!("Result {}: {}", i, result);
}
```
### Thread Safety
1. **Use Sync Mode When Needed**: If you need thread safety, use the `sync` feature:
```rust
// In Cargo.toml
// rhai = { version = "1.x", features = ["sync"] }
// This creates a thread-safe engine
let engine = Engine::new();
// Now you can safely share the engine between threads
std::thread::spawn(move || {
let result = engine.eval::<i64>("40 + 2")?;
println!("Result: {}", result);
});
```
2. **Clone the Engine for Multiple Threads**: When not using `sync`, clone the engine for each thread:
```rust
let engine = Engine::new();
let handles: Vec<_> = (0..5).map(|i| {
let engine_clone = engine.clone();
std::thread::spawn(move || {
let result = engine_clone.eval::<i64>(&format!("{} + 2", i * 10))?;
println!("Thread {}: {}", i, result);
})
}).collect();
for handle in handles {
handle.join().unwrap();
}
```
### Memory Management
1. **Control Scope Size**: Be mindful of the size of your scopes:
```rust
// Create a new scope for each operation to avoid memory buildup
for item in items {
let mut scope = Scope::new();
scope.push("item", item);
engine.eval_with_scope::<()>(&mut scope, "process(item)")?;
}
```
2. **Limit Script Complexity**: Use engine options to limit script complexity:
```rust
let mut engine = Engine::new();
// Set limits to prevent scripts from consuming too many resources
engine.set_max_expr_depths(64, 64) // Max expression/statement depth
.set_max_function_expr_depth(64) // Max function depth
.set_max_array_size(10000) // Max array size
.set_max_map_size(10000) // Max map size
.set_max_string_size(10000) // Max string size
.set_max_call_levels(64); // Max call stack depth
```
3. **Use Shared Values Carefully**: Shared values (via closures) have reference-counting overhead:
```rust
// Avoid unnecessary capturing in closures when possible
engine.register_fn("process", |x: i64| x * 2);
// Instead of capturing large data structures
let large_data = vec![1, 2, 3, /* ... thousands of items ... */];
engine.register_fn("process_data", move |idx: i64| {
if idx >= 0 && (idx as usize) < large_data.len() {
large_data[idx as usize]
} else {
0
}
});
// Consider registering a lookup function instead
let large_data = std::sync::Arc::new(vec![1, 2, 3, /* ... thousands of items ... */]);
let data_ref = large_data.clone();
engine.register_fn("lookup", move |idx: i64| {
if idx >= 0 && (idx as usize) < data_ref.len() {
data_ref[idx as usize]
} else {
0
}
});
```
### API Design
1. **Consistent Naming**: Use consistent naming conventions:
```rust
// Good: Consistent naming pattern
engine.register_fn("create_user", create_user)
.register_fn("update_user", update_user)
.register_fn("delete_user", delete_user);
// Bad: Inconsistent naming
engine.register_fn("create_user", create_user)
.register_fn("user_update", update_user)
.register_fn("remove", delete_user);
```
2. **Logical Function Grouping**: Group related functions together:
```rust
// Register all string-related functions together
engine.register_fn("str_length", |s: &str| s.len() as i64)
.register_fn("str_uppercase", |s: &str| s.to_uppercase())
.register_fn("str_lowercase", |s: &str| s.to_lowercase());
// Register all math-related functions together
engine.register_fn("math_sin", |x: f64| x.sin())
.register_fn("math_cos", |x: f64| x.cos())
.register_fn("math_tan", |x: f64| x.tan());
```
3. **Comprehensive Documentation**: Document your API thoroughly:
```rust
// Add documentation for script writers
let mut engine = Engine::new();
#[cfg(feature = "metadata")]
{
// Add function documentation
engine.register_fn("calculate_tax", calculate_tax)
.register_fn_metadata("calculate_tax", |metadata| {
metadata.set_doc_comment("Calculates tax based on income and rate.\n\nParameters:\n- income: Annual income\n- rate: Tax rate (0.0-1.0)\n\nReturns: Calculated tax amount");
});
}
```

View File

@@ -0,0 +1,428 @@
use chrono::{DateTime, Duration, Utc};
use serde::{Deserialize, Serialize};
/// This example demonstrates business models in action:
/// 1. Defining products (2 types of server nodes)
/// 2. Defining components (parts of the nodes)
/// 3. Setting up pricing
/// 4. Creating a function to check which products can be bought
/// 5. Simulating a user buying a product
/// 6. Generating an invoice
/// 7. Simulating payment
fn main() {
println!("Business Models Example");
println!("=======================\n");
// Create a customer
let customer = create_customer();
println!("Created customer: {}", customer.name);
// Define products (server nodes)
let (standard_node, premium_node) = create_server_products();
println!("Created server products:");
println!(" - Standard Node: ${} {}", standard_node.price.amount, standard_node.price.currency_code);
println!(" - Premium Node: ${} {}", premium_node.price.amount, premium_node.price.currency_code);
// Check which products can be purchased
println!("\nChecking which products can be purchased:");
let purchasable_products = get_purchasable_products(&[&standard_node, &premium_node]);
for product in purchasable_products {
println!(" - {} is available for purchase", product.name);
}
// Simulate a user buying a product
println!("\nSimulating purchase of a Premium Node:");
let sale = create_sale(&customer, &premium_node);
println!(" - Sale created with ID: {}", sale.id);
println!(" - Total amount: ${} {}", sale.total_amount.amount, sale.total_amount.currency_code);
// Generate an invoice
println!("\nGenerating invoice:");
let invoice = create_invoice(&customer, &sale);
println!(" - Invoice created with ID: {}", invoice.id);
println!(" - Total amount: ${} {}", invoice.total_amount.amount, invoice.total_amount.currency_code);
println!(" - Due date: {}", invoice.due_date);
println!(" - Status: {:?}", invoice.status);
// Simulate payment
println!("\nSimulating payment:");
let paid_invoice = process_payment(invoice);
println!(" - Payment processed");
println!(" - New balance due: ${} {}", paid_invoice.balance_due.amount, paid_invoice.balance_due.currency_code);
println!(" - Payment status: {:?}", paid_invoice.payment_status);
println!(" - Invoice status: {:?}", paid_invoice.status);
println!("\nBusiness transaction completed successfully!");
}
// ===== Model Definitions =====
// Currency represents a monetary value with amount and currency code
#[derive(Debug, Clone, Serialize, Deserialize)]
struct Currency {
amount: f64,
currency_code: String,
}
// Customer represents a customer who can purchase products
#[derive(Debug, Clone, Serialize, Deserialize)]
struct Customer {
id: u32,
name: String,
description: String,
pubkey: String,
}
// ProductType represents the type of a product
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
enum ProductType {
Product,
Service,
}
// ProductStatus represents the status of a product
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
enum ProductStatus {
Available,
Unavailable,
}
// ProductComponent represents a component of a product
#[derive(Debug, Clone, Serialize, Deserialize)]
struct ProductComponent {
id: i64,
name: String,
description: String,
quantity: i64,
}
// Product represents a product or service offered
#[derive(Debug, Clone, Serialize, Deserialize)]
struct Product {
id: i64,
name: String,
description: String,
price: Currency,
type_: ProductType,
category: String,
status: ProductStatus,
max_amount: i64,
purchase_till: DateTime<Utc>,
active_till: DateTime<Utc>,
components: Vec<ProductComponent>,
}
// SaleStatus represents the status of a sale
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
enum SaleStatus {
Pending,
Completed,
Cancelled,
}
// SaleItem represents an item in a sale
#[derive(Debug, Clone, Serialize, Deserialize)]
struct SaleItem {
id: u32,
sale_id: u32,
product_id: u32,
name: String,
description: String,
comments: String,
quantity: i32,
unit_price: Currency,
subtotal: Currency,
tax_rate: f64,
tax_amount: Currency,
active_till: DateTime<Utc>,
}
// Sale represents a sale of products or services
#[derive(Debug, Clone, Serialize, Deserialize)]
struct Sale {
id: u32,
company_id: u32,
customer_id: u32,
buyer_name: String,
buyer_email: String,
subtotal_amount: Currency,
tax_amount: Currency,
total_amount: Currency,
status: SaleStatus,
service_id: Option<u32>,
sale_date: DateTime<Utc>,
items: Vec<SaleItem>,
}
// InvoiceStatus represents the status of an invoice
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
enum InvoiceStatus {
Draft,
Sent,
Paid,
Overdue,
Cancelled,
}
// PaymentStatus represents the payment status of an invoice
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
enum PaymentStatus {
Unpaid,
PartiallyPaid,
Paid,
}
// Payment represents a payment made against an invoice
#[derive(Debug, Clone, Serialize, Deserialize)]
struct Payment {
amount: Currency,
date: DateTime<Utc>,
method: String,
comment: String,
}
// InvoiceItem represents an item in an invoice
#[derive(Debug, Clone, Serialize, Deserialize)]
struct InvoiceItem {
id: u32,
invoice_id: u32,
description: String,
amount: Currency,
sale_id: Option<u32>,
}
// Invoice represents an invoice sent to a customer
#[derive(Debug, Clone, Serialize, Deserialize)]
struct Invoice {
id: u32,
customer_id: u32,
total_amount: Currency,
balance_due: Currency,
status: InvoiceStatus,
payment_status: PaymentStatus,
issue_date: DateTime<Utc>,
due_date: DateTime<Utc>,
items: Vec<InvoiceItem>,
payments: Vec<Payment>,
}
// ===== Implementation Functions =====
// Create a customer for our example
fn create_customer() -> Customer {
Customer {
id: 1,
name: "TechCorp Inc.".to_string(),
description: "Enterprise technology company".to_string(),
pubkey: "tech-corp-public-key-123".to_string(),
}
}
// Create two types of server node products with their components
fn create_server_products() -> (Product, Product) {
let now = Utc::now();
// Create currency for pricing
let usd = |amount| {
Currency {
amount,
currency_code: "USD".to_string(),
}
};
// Standard Node Components
let cpu_standard = ProductComponent {
id: 1,
name: "CPU".to_string(),
description: "4-core CPU".to_string(),
quantity: 1,
};
let ram_standard = ProductComponent {
id: 2,
name: "RAM".to_string(),
description: "16GB RAM".to_string(),
quantity: 1,
};
let storage_standard = ProductComponent {
id: 3,
name: "Storage".to_string(),
description: "500GB SSD".to_string(),
quantity: 1,
};
// Premium Node Components
let cpu_premium = ProductComponent {
id: 4,
name: "CPU".to_string(),
description: "8-core CPU".to_string(),
quantity: 1,
};
let ram_premium = ProductComponent {
id: 5,
name: "RAM".to_string(),
description: "32GB RAM".to_string(),
quantity: 1,
};
let storage_premium = ProductComponent {
id: 6,
name: "Storage".to_string(),
description: "1TB SSD".to_string(),
quantity: 1,
};
let gpu_premium = ProductComponent {
id: 7,
name: "GPU".to_string(),
description: "Dedicated GPU".to_string(),
quantity: 1,
};
// Create Standard Node Product
let standard_node = Product {
id: 1,
name: "Standard Server Node".to_string(),
description: "Basic server node for general workloads".to_string(),
price: usd(99.99),
type_: ProductType::Product,
category: "Servers".to_string(),
status: ProductStatus::Available,
max_amount: 100,
purchase_till: now + Duration::days(365),
active_till: now + Duration::days(365),
components: vec![cpu_standard, ram_standard, storage_standard],
};
// Create Premium Node Product
let premium_node = Product {
id: 2,
name: "Premium Server Node".to_string(),
description: "High-performance server node for demanding workloads".to_string(),
price: usd(199.99),
type_: ProductType::Product,
category: "Servers".to_string(),
status: ProductStatus::Available,
max_amount: 50,
purchase_till: now + Duration::days(365),
active_till: now + Duration::days(365),
components: vec![cpu_premium, ram_premium, storage_premium, gpu_premium],
};
(standard_node, premium_node)
}
// Check which products can be purchased
fn get_purchasable_products<'a>(products: &[&'a Product]) -> Vec<&'a Product> {
products.iter()
.filter(|p| p.status == ProductStatus::Available && Utc::now() <= p.purchase_till)
.copied()
.collect()
}
// Create a sale for a customer buying a product
fn create_sale(customer: &Customer, product: &Product) -> Sale {
let now = Utc::now();
let active_till = now + Duration::days(365);
// Create a sale item for the product
let sale_item = SaleItem {
id: 1,
sale_id: 1,
product_id: product.id as u32,
name: product.name.clone(),
description: product.description.clone(),
comments: "Customer requested expedited setup".to_string(),
quantity: 1,
unit_price: product.price.clone(),
subtotal: Currency {
amount: product.price.amount * 1.0,
currency_code: product.price.currency_code.clone(),
},
tax_rate: 10.0, // 10% tax rate
tax_amount: Currency {
amount: product.price.amount * 0.1,
currency_code: product.price.currency_code.clone(),
},
active_till,
};
// Calculate totals
let subtotal = sale_item.subtotal.clone();
let tax_amount = sale_item.tax_amount.clone();
let total_amount = Currency {
amount: subtotal.amount + tax_amount.amount,
currency_code: subtotal.currency_code.clone(),
};
// Create the sale
Sale {
id: 1,
company_id: 101, // Assuming company ID 101
customer_id: customer.id,
buyer_name: customer.name.clone(),
buyer_email: "contact@techcorp.com".to_string(), // Example email
subtotal_amount: subtotal,
tax_amount,
total_amount,
status: SaleStatus::Completed,
service_id: None,
sale_date: now,
items: vec![sale_item],
}
}
// Create an invoice for a sale
fn create_invoice(customer: &Customer, sale: &Sale) -> Invoice {
let now = Utc::now();
let due_date = now + Duration::days(30); // Due in 30 days
// Create an invoice item for the sale
let invoice_item = InvoiceItem {
id: 1,
invoice_id: 1,
description: format!("Purchase of {}", sale.items[0].name),
amount: sale.total_amount.clone(),
sale_id: Some(sale.id),
};
// Create the invoice
Invoice {
id: 1,
customer_id: customer.id,
total_amount: sale.total_amount.clone(),
balance_due: sale.total_amount.clone(),
status: InvoiceStatus::Sent,
payment_status: PaymentStatus::Unpaid,
issue_date: now,
due_date,
items: vec![invoice_item],
payments: Vec::new(),
}
}
// Process a payment for an invoice
fn process_payment(mut invoice: Invoice) -> Invoice {
// Create a payment for the full amount
let payment = Payment {
amount: invoice.total_amount.clone(),
date: Utc::now(),
method: "Credit Card".to_string(),
comment: "Payment received via credit card ending in 1234".to_string(),
};
// Add the payment to the invoice
invoice.payments.push(payment);
// Update the balance due
invoice.balance_due.amount = 0.0;
// Update the payment status
invoice.payment_status = PaymentStatus::Paid;
invoice.status = InvoiceStatus::Paid;
invoice
}

View File

@@ -0,0 +1,108 @@
// This example demonstrates the basic functionality of the circle models
// without using the database functionality
use herodb::models::circle::{Circle, Member, Name, Wallet, Role, Record, RecordType};
fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("Circle Module Basic Demo");
// Create a circle
let circle = Circle::new(
1,
"ThreeFold Community".to_string(),
"A circle for ThreeFold community members".to_string(),
);
println!("Created circle: {:?}", circle);
// Create members
let mut alice = Member::new(
1,
"Alice".to_string(),
"Core contributor".to_string(),
Role::Admin,
);
alice.add_email("alice@example.com".to_string());
let mut bob = Member::new(
2,
"Bob".to_string(),
"Community member".to_string(),
Role::Member,
);
bob.add_email("bob@example.com".to_string());
println!("Created members: {:?}, {:?}", alice, bob);
// Create a domain name
let mut domain = Name::new(
1,
"threefold.io".to_string(),
"ThreeFold main domain".to_string(),
);
let record = Record {
name: "www".to_string(),
text: "ThreeFold Website".to_string(),
category: RecordType::A,
addr: vec!["192.168.1.1".to_string()],
};
domain.add_record(record);
domain.add_admin("alice_pubkey".to_string());
println!("Created domain: {:?}", domain);
// Create wallets
let mut alice_wallet = Wallet::new(
1,
"Alice's TFT Wallet".to_string(),
"Main TFT wallet".to_string(),
"Stellar".to_string(),
"GALICE...".to_string(),
);
alice_wallet.set_asset("TFT".to_string(), 1000.0);
alice_wallet.set_asset("XLM".to_string(), 100.0);
let mut bob_wallet = Wallet::new(
2,
"Bob's TFT Wallet".to_string(),
"Main TFT wallet".to_string(),
"Stellar".to_string(),
"GBOB...".to_string(),
);
bob_wallet.set_asset("TFT".to_string(), 500.0);
println!("Created wallets: {:?}, {:?}", alice_wallet, bob_wallet);
// Link wallets to members
alice.link_wallet(alice_wallet.id);
bob.link_wallet(bob_wallet.id);
println!("Linked wallets to members");
// Demonstrate wallet operations
println!("\nDemonstrating wallet operations:");
println!("Alice's wallet before transfer: {:?}", alice_wallet);
println!("Alice's wallet total value: {}", alice_wallet.total_value());
println!("Bob's wallet before transfer: {:?}", bob_wallet);
println!("Bob's wallet total value: {}", bob_wallet.total_value());
// Simulate a transfer of 100 TFT from Alice to Bob
alice_wallet.set_asset("TFT".to_string(), 900.0); // Decrease Alice's TFT by 100
bob_wallet.set_asset("TFT".to_string(), 600.0); // Increase Bob's TFT by 100
println!("Alice's wallet after transfer: {:?}", alice_wallet);
println!("Alice's wallet total value: {}", alice_wallet.total_value());
println!("Bob's wallet after transfer: {:?}", bob_wallet);
println!("Bob's wallet total value: {}", bob_wallet.total_value());
println!("\nCircle basic demo completed successfully!");
Ok(())
}

View File

@@ -0,0 +1,114 @@
use herodb::models::circle::{Circle, Member, Name, Wallet, Role, Record, RecordType};
fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("Circle Models Demo (In-Memory Version)");
println!("=====================================\n");
// Create a circle
let circle = Circle::new(
1,
"ThreeFold Community".to_string(),
"A circle for ThreeFold community members".to_string(),
);
println!("Created circle: {:?}", circle);
// Create members
let mut alice = Member::new(
1,
"Alice".to_string(),
"Core contributor".to_string(),
Role::Admin,
);
alice.add_email("alice@example.com".to_string());
let mut bob = Member::new(
2,
"Bob".to_string(),
"Community member".to_string(),
Role::Member,
);
bob.add_email("bob@example.com".to_string());
println!("Created members: {:?}, {:?}", alice, bob);
// Create a domain name
let mut domain = Name::new(
1,
"threefold.io".to_string(),
"ThreeFold main domain".to_string(),
);
let record = Record {
name: "www".to_string(),
text: "ThreeFold Website".to_string(),
category: RecordType::A,
addr: vec!["192.168.1.1".to_string()],
};
domain.add_record(record);
domain.add_admin("alice_pubkey".to_string());
println!("Created domain: {:?}", domain);
// Create wallets
let mut alice_wallet = Wallet::new(
1,
"Alice's TFT Wallet".to_string(),
"Main TFT wallet".to_string(),
"Stellar".to_string(),
"GALICE...".to_string(),
);
alice_wallet.set_asset("TFT".to_string(), 1000.0);
alice_wallet.set_asset("XLM".to_string(), 100.0);
let mut bob_wallet = Wallet::new(
2,
"Bob's TFT Wallet".to_string(),
"Main TFT wallet".to_string(),
"Stellar".to_string(),
"GBOB...".to_string(),
);
bob_wallet.set_asset("TFT".to_string(), 500.0);
println!("Created wallets: {:?}, {:?}", alice_wallet, bob_wallet);
// Link wallets to members
alice.link_wallet(alice_wallet.id);
bob.link_wallet(bob_wallet.id);
println!("Linked wallets to members");
// Display all data
println!("\nDisplaying all data:");
println!("Circles: {:#?}", vec![circle]);
println!("Members: {:#?}", vec![alice.clone(), bob.clone()]);
println!("Names: {:#?}", vec![domain]);
println!("Wallets: {:#?}", vec![alice_wallet.clone(), bob_wallet.clone()]);
// Demonstrate wallet operations
println!("\nDemonstrating wallet operations:");
println!("Alice's wallet before transfer: {:?}", alice_wallet);
println!("Alice's wallet total value: {}", alice_wallet.total_value());
println!("Bob's wallet before transfer: {:?}", bob_wallet);
println!("Bob's wallet total value: {}", bob_wallet.total_value());
// Simulate a transfer of 100 TFT from Alice to Bob
alice_wallet.set_asset("TFT".to_string(), 900.0); // Decrease Alice's TFT by 100
bob_wallet.set_asset("TFT".to_string(), 600.0); // Increase Bob's TFT by 100
println!("Alice's wallet after transfer: {:?}", alice_wallet);
println!("Alice's wallet total value: {}", alice_wallet.total_value());
println!("Bob's wallet after transfer: {:?}", bob_wallet);
println!("Bob's wallet total value: {}", bob_wallet.total_value());
println!("\nCircle models demo completed successfully!");
Ok(())
}

View File

@@ -0,0 +1,106 @@
//! This is a standalone example that demonstrates the circle models
//! without using any database functionality.
use herodb::models::circle::{Circle, Member, Name, Wallet, Role, Record, RecordType};
fn main() {
println!("Circle Module Standalone Demo");
// Create a circle
let circle = Circle::new(
1,
"ThreeFold Community".to_string(),
"A circle for ThreeFold community members".to_string(),
);
println!("Created circle: {:?}", circle);
// Create members
let mut alice = Member::new(
1,
"Alice".to_string(),
"Core contributor".to_string(),
Role::Admin,
);
alice.add_email("alice@example.com".to_string());
let mut bob = Member::new(
2,
"Bob".to_string(),
"Community member".to_string(),
Role::Member,
);
bob.add_email("bob@example.com".to_string());
println!("Created members: {:?}, {:?}", alice, bob);
// Create a domain name
let mut domain = Name::new(
1,
"threefold.io".to_string(),
"ThreeFold main domain".to_string(),
);
let record = Record {
name: "www".to_string(),
text: "ThreeFold Website".to_string(),
category: RecordType::A,
addr: vec!["192.168.1.1".to_string()],
};
domain.add_record(record);
domain.add_admin("alice_pubkey".to_string());
println!("Created domain: {:?}", domain);
// Create wallets
let mut alice_wallet = Wallet::new(
1,
"Alice's TFT Wallet".to_string(),
"Main TFT wallet".to_string(),
"Stellar".to_string(),
"GALICE...".to_string(),
);
alice_wallet.set_asset("TFT".to_string(), 1000.0);
alice_wallet.set_asset("XLM".to_string(), 100.0);
let mut bob_wallet = Wallet::new(
2,
"Bob's TFT Wallet".to_string(),
"Main TFT wallet".to_string(),
"Stellar".to_string(),
"GBOB...".to_string(),
);
bob_wallet.set_asset("TFT".to_string(), 500.0);
println!("Created wallets: {:?}, {:?}", alice_wallet, bob_wallet);
// Link wallets to members
alice.link_wallet(alice_wallet.id);
bob.link_wallet(bob_wallet.id);
println!("Linked wallets to members");
// Demonstrate wallet operations
println!("\nDemonstrating wallet operations:");
println!("Alice's wallet before transfer: {:?}", alice_wallet);
println!("Alice's wallet total value: {}", alice_wallet.total_value());
println!("Bob's wallet before transfer: {:?}", bob_wallet);
println!("Bob's wallet total value: {}", bob_wallet.total_value());
// Simulate a transfer of 100 TFT from Alice to Bob
alice_wallet.set_asset("TFT".to_string(), 900.0); // Decrease Alice's TFT by 100
bob_wallet.set_asset("TFT".to_string(), 600.0); // Increase Bob's TFT by 100
println!("Alice's wallet after transfer: {:?}", alice_wallet);
println!("Alice's wallet total value: {}", alice_wallet.total_value());
println!("Bob's wallet after transfer: {:?}", bob_wallet);
println!("Bob's wallet total value: {}", bob_wallet.total_value());
println!("\nCircle standalone demo completed successfully!");
}

View File

@@ -0,0 +1,81 @@
use herodb::db::{DB, DBBuilder, Model};
use herodb::models::biz::{Product, ProductBuilder, ProductType, ProductStatus, Currency, CurrencyBuilder};
use chrono::Utc;
fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("OurDB Backend Example");
println!("====================\n");
// Create a temporary directory for the database
let db_path = std::env::temp_dir().join("herodb_ourdb_example");
std::fs::create_dir_all(&db_path)?;
println!("Creating database at: {}", db_path.display());
// Create a new database with Product model registered
let db = DBBuilder::new(db_path.clone())
.register_model::<Product>()
.build()?;
println!("Database created successfully");
// Create a currency for pricing
let usd = CurrencyBuilder::new()
.id(1) // Add an ID for the currency
.amount(99.99)
.currency_code("USD")
.build()
.expect("Failed to create currency");
// Create a product
let product = ProductBuilder::new()
.id(1) // We're setting an ID manually for this example
.name("Test Product")
.description("A test product for our OurDB example")
.price(usd)
.type_(ProductType::Product)
.category("Test")
.status(ProductStatus::Available)
.max_amount(100)
.validity_days(365)
.build()
.expect("Failed to create product");
println!("\nCreated product: {}", product.name);
println!("Product ID: {}", product.get_id());
// Insert the product into the database
db.set(&product)?;
println!("Product saved to database");
// Retrieve the product from the database
let retrieved_product = db.get::<Product>(product.get_id())?;
println!("\nRetrieved product from database:");
println!(" Name: {}", retrieved_product.name);
println!(" Description: {}", retrieved_product.description);
println!(" Price: ${} {}", retrieved_product.price.amount, retrieved_product.price.currency_code);
// Create a product with auto-incremented ID
// For this to work, we would need to modify the Product model to support auto-incremented IDs
// This is just a conceptual example
println!("\nDemonstrating auto-incremented IDs:");
println!("(Note: This would require additional implementation in the Product model)");
// Delete the product
db.delete::<Product>(product.get_id())?;
println!("\nProduct deleted from database");
// Try to retrieve the deleted product (should fail)
match db.get::<Product>(product.get_id()) {
Ok(_) => println!("Product still exists (unexpected)"),
Err(e) => println!("Verified deletion: {}", e),
}
println!("\nExample completed successfully!");
// Clean up
std::fs::remove_dir_all(&db_path)?;
println!("Cleaned up database directory");
Ok(())
}

View File

@@ -0,0 +1,93 @@
use herodb::db::{DB, DBBuilder, Model, IndexKey};
use herodb::models::biz::Customer;
use std::path::PathBuf;
use std::fs;
fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("TST Index Example");
println!("================");
// Create a temporary directory for the database
let db_path = PathBuf::from("/tmp/tst_index_example");
if db_path.exists() {
fs::remove_dir_all(&db_path)?;
}
fs::create_dir_all(&db_path)?;
println!("Database path: {:?}", db_path);
// Create a database instance with the Customer model registered
let db = DBBuilder::new(&db_path)
.register_model::<Customer>()
.build()?;
// Create some customers
let customer1 = Customer::new(
1,
"John Doe".to_string(),
"A regular customer".to_string(),
"pk123456".to_string(),
);
let customer2 = Customer::new(
2,
"Jane Smith".to_string(),
"A VIP customer".to_string(),
"pk789012".to_string(),
);
let customer3 = Customer::new(
3,
"John Smith".to_string(),
"Another customer".to_string(),
"pk345678".to_string(),
);
// Insert the customers
db.set(&customer1)?;
db.set(&customer2)?;
db.set(&customer3)?;
println!("\nCustomers created:");
println!("1. {} ({})", customer1.name, customer1.pubkey);
println!("2. {} ({})", customer2.name, customer2.pubkey);
println!("3. {} ({})", customer3.name, customer3.pubkey);
// List all customers
println!("\nListing all customers:");
let customers = db.list::<Customer>()?;
for customer in &customers {
println!("- {} (ID: {})", customer.name, customer.id);
}
println!("Total: {} customers", customers.len());
// Find customers by name index
println!("\nFinding customers by name 'John':");
let john_customers = db.find_by_index_prefix::<Customer>("name", "John")?;
for customer in &john_customers {
println!("- {} (ID: {})", customer.name, customer.id);
}
println!("Total: {} customers", john_customers.len());
// Find customers by pubkey index
println!("\nFinding customer by pubkey 'pk789012':");
let pubkey_customers = db.find_by_index::<Customer>("pubkey", "pk789012")?;
for customer in &pubkey_customers {
println!("- {} (ID: {})", customer.name, customer.id);
}
println!("Total: {} customers", pubkey_customers.len());
// Delete a customer
println!("\nDeleting customer with ID 2");
db.delete::<Customer>(2)?;
// List all customers again
println!("\nListing all customers after deletion:");
let customers = db.list::<Customer>()?;
for customer in &customers {
println!("- {} (ID: {})", customer.name, customer.id);
}
println!("Total: {} customers", customers.len());
println!("\nExample completed successfully!");
Ok(())
}

View File

@@ -0,0 +1,48 @@
# Business Models Example
This example demonstrates the business models in HeroDB, showcasing a complete business transaction flow from product definition to payment processing.
## Features Demonstrated
1. **Product Definition**: Creating two types of server node products with different components and pricing
2. **Component Definition**: Defining the parts that make up each server node (CPU, RAM, Storage, GPU)
3. **Pricing Setup**: Setting up prices for products using the Currency model
4. **Product Availability**: Checking which products can be purchased based on their status and availability
5. **Sales Process**: Simulating a customer purchasing a product
6. **Invoice Generation**: Creating an invoice for the sale
7. **Payment Processing**: Processing a payment for the invoice and updating its status
## Business Flow
The example follows this business flow:
```
Define Products → Check Availability → Customer Purchase → Generate Invoice → Process Payment
```
## Models Used
- **Product & ProductComponent**: For defining server nodes and their components
- **Customer**: For representing the buyer
- **Sale & SaleItem**: For recording the purchase transaction
- **Invoice & InvoiceItem**: For billing the customer
- **Payment**: For recording the payment
## Running the Example
To run this example, use:
```bash
cargo run --bin dbexample_biz
```
The output will show each step of the business process with relevant details.
## Key Concepts
- **Builder Pattern**: All models use builders for flexible object creation
- **Status Tracking**: Sales and invoices have status enums to track their state
- **Relationship Modeling**: The example shows how different business entities relate to each other
- **Financial Calculations**: Demonstrates tax and total calculations
This example provides a template for implementing business logic in your own applications using HeroDB.

View File

@@ -0,0 +1,326 @@
use chrono::{Duration, Utc};
use herodb::db::{DBBuilder, DB, Model};
use herodb::models::biz::{
Currency, CurrencyBuilder,
Product, ProductBuilder, ProductComponent, ProductComponentBuilder, ProductType, ProductStatus,
Sale, SaleBuilder, SaleItem, SaleItemBuilder, SaleStatus,
Invoice, InvoiceBuilder, InvoiceItem, InvoiceItemBuilder, InvoiceStatus, Payment, PaymentStatus,
Customer, CustomerBuilder,
};
use std::path::PathBuf;
use std::fs;
/// This example demonstrates the business models in action:
/// 1. Defining products (2 types of server nodes)
/// 2. Defining components (parts of the nodes)
/// 3. Setting up pricing
/// 4. Creating a function to check which products can be bought
/// 5. Simulating a user buying a product
/// 6. Generating an invoice
/// 7. Simulating payment
fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("Business Models Example");
println!("=======================\n");
// Create a temporary directory for the database
let db_path = PathBuf::from("/tmp/dbexample_biz");
if db_path.exists() {
fs::remove_dir_all(&db_path)?;
}
fs::create_dir_all(&db_path)?;
println!("Database path: {:?}", db_path);
// Create a database instance with our business models registered
let db = DBBuilder::new(&db_path)
.register_model::<Customer>()
.register_model::<Product>()
.register_model::<Sale>()
.register_model::<Invoice>()
.build()?;
// Create a customer
let customer = create_customer();
println!("Created customer: {}", customer.name);
db.set(&customer)?;
// Define products (server nodes)
let (standard_node, premium_node) = create_server_products();
println!("Created server products:");
println!(" - Standard Node: ${} {}", standard_node.price.amount, standard_node.price.currency_code);
println!(" - Premium Node: ${} {}", premium_node.price.amount, premium_node.price.currency_code);
// Store products in the database
db.set(&standard_node)?;
db.set(&premium_node)?;
// Check which products can be purchased
println!("\nChecking which products can be purchased:");
let purchasable_products = get_purchasable_products(&[&standard_node, &premium_node]);
for product in purchasable_products {
println!(" - {} is available for purchase", product.name);
}
// Simulate a user buying a product
println!("\nSimulating purchase of a Premium Node:");
let sale = create_sale(&customer, &premium_node);
println!(" - Sale created with ID: {}", sale.get_id());
println!(" - Total amount: ${} {}", sale.total_amount.amount, sale.total_amount.currency_code);
db.set(&sale)?;
// Generate an invoice
println!("\nGenerating invoice:");
let invoice = create_invoice(&customer, &sale);
println!(" - Invoice created with ID: {}", invoice.get_id());
println!(" - Total amount: ${} {}", invoice.total_amount.amount, invoice.total_amount.currency_code);
println!(" - Due date: {}", invoice.due_date);
println!(" - Status: {:?}", invoice.status);
db.set(&invoice)?;
// Simulate payment
println!("\nSimulating payment:");
let mut invoice_copy = invoice.clone();
process_payment(&mut invoice_copy);
println!(" - Payment processed");
println!(" - New balance due: ${} {}", invoice_copy.balance_due.amount, invoice_copy.balance_due.currency_code);
println!(" - Payment status: {:?}", invoice_copy.payment_status);
println!(" - Invoice status: {:?}", invoice_copy.status);
db.set(&invoice_copy)?;
// Retrieve data from the database to verify persistence
println!("\nRetrieving data from database:");
// Retrieve customer
let retrieved_customer = db.get::<Customer>(customer.get_id())?;
println!("Retrieved customer: {} (ID: {})", retrieved_customer.name, retrieved_customer.get_id());
// Retrieve product
let retrieved_product = db.get::<Product>(premium_node.get_id())?;
println!("Retrieved product: {} (ID: {})", retrieved_product.name, retrieved_product.get_id());
// Retrieve sale
let retrieved_sale = db.get::<Sale>(sale.get_id())?;
println!("Retrieved sale: ID {} with {} items", retrieved_sale.get_id(), retrieved_sale.items.len());
// Retrieve invoice
let retrieved_invoice = db.get::<Invoice>(invoice.get_id())?;
println!("Retrieved invoice: ID {} with status {:?}", retrieved_invoice.get_id(), retrieved_invoice.status);
println!("\nBusiness transaction completed successfully!");
Ok(())
}
/// Create a customer for our example
fn create_customer() -> Customer {
CustomerBuilder::new()
.id(1)
.name("TechCorp Inc.")
.description("Enterprise technology company")
.pubkey("tech-corp-public-key-123")
.build()
.expect("Failed to create customer")
}
/// Create two types of server node products with their components
fn create_server_products() -> (Product, Product) {
// Create currencies for pricing
let standard_price = CurrencyBuilder::new()
.id(100)
.amount(99.99)
.currency_code("USD")
.build()
.expect("Failed to create currency");
let premium_price = CurrencyBuilder::new()
.id(101)
.amount(199.99)
.currency_code("USD")
.build()
.expect("Failed to create currency");
// Standard Node Components
let cpu_standard = ProductComponentBuilder::new()
.id(1)
.name("CPU")
.description("4-core CPU")
.quantity(1)
.build()
.expect("Failed to create CPU component");
let ram_standard = ProductComponentBuilder::new()
.id(2)
.name("RAM")
.description("16GB RAM")
.quantity(1)
.build()
.expect("Failed to create RAM component");
let storage_standard = ProductComponentBuilder::new()
.id(3)
.name("Storage")
.description("500GB SSD")
.quantity(1)
.build()
.expect("Failed to create Storage component");
// Premium Node Components
let cpu_premium = ProductComponentBuilder::new()
.id(4)
.name("CPU")
.description("8-core CPU")
.quantity(1)
.build()
.expect("Failed to create CPU component");
let ram_premium = ProductComponentBuilder::new()
.id(5)
.name("RAM")
.description("32GB RAM")
.quantity(1)
.build()
.expect("Failed to create RAM component");
let storage_premium = ProductComponentBuilder::new()
.id(6)
.name("Storage")
.description("1TB SSD")
.quantity(1)
.build()
.expect("Failed to create Storage component");
let gpu_premium = ProductComponentBuilder::new()
.id(7)
.name("GPU")
.description("Dedicated GPU")
.quantity(1)
.build()
.expect("Failed to create GPU component");
// Create Standard Node Product
let standard_node = ProductBuilder::new()
.id(1)
.name("Standard Server Node")
.description("Basic server node for general workloads")
.price(standard_price)
.type_(ProductType::Product)
.category("Servers")
.status(ProductStatus::Available)
.max_amount(100)
.validity_days(365)
.add_component(cpu_standard)
.add_component(ram_standard)
.add_component(storage_standard)
.build()
.expect("Failed to create Standard Node product");
// Create Premium Node Product
let premium_node = ProductBuilder::new()
.id(2)
.name("Premium Server Node")
.description("High-performance server node for demanding workloads")
.price(premium_price)
.type_(ProductType::Product)
.category("Servers")
.status(ProductStatus::Available)
.max_amount(50)
.validity_days(365)
.add_component(cpu_premium)
.add_component(ram_premium)
.add_component(storage_premium)
.add_component(gpu_premium)
.build()
.expect("Failed to create Premium Node product");
(standard_node, premium_node)
}
/// Check which products can be purchased
fn get_purchasable_products<'a>(products: &[&'a Product]) -> Vec<&'a Product> {
products.iter()
.filter(|p| p.is_purchasable())
.copied()
.collect()
}
/// Create a sale for a customer buying a product
fn create_sale(customer: &Customer, product: &Product) -> Sale {
let now = Utc::now();
let active_till = now + Duration::days(365);
// Create a sale item for the product
let sale_item = SaleItemBuilder::new()
.id(1)
.sale_id(1)
.product_id(Some(product.get_id()))
.name(product.name.clone())
.description(product.description.clone())
.comments("Customer requested expedited setup")
.quantity(1)
.unit_price(product.price.clone())
.tax_rate(10.0) // 10% tax rate
.active_till(active_till)
.build()
.expect("Failed to create sale item");
// Create the sale
let sale = SaleBuilder::new()
.id(1)
.company_id(101) // Assuming company ID 101
.customer_id(customer.get_id())
.buyer_name(customer.name.clone())
.buyer_email("contact@techcorp.com") // Example email
.currency_code(product.price.currency_code.clone())
.status(SaleStatus::Completed)
.add_item(sale_item)
.build()
.expect("Failed to create sale");
sale
}
/// Create an invoice for a sale
fn create_invoice(customer: &Customer, sale: &Sale) -> Invoice {
let now = Utc::now();
let due_date = now + Duration::days(30); // Due in 30 days
// Create an invoice item for the sale
let invoice_item = InvoiceItemBuilder::new()
.id(1)
.invoice_id(1)
.description(format!("Purchase of {}", sale.items[0].name))
.amount(sale.total_amount.clone())
.sale_id(sale.get_id())
.build()
.expect("Failed to create invoice item");
// Create the invoice
let invoice = InvoiceBuilder::new()
.id(1)
.customer_id(customer.get_id())
.currency_code(sale.total_amount.currency_code.clone())
.status(InvoiceStatus::Sent)
.issue_date(now)
.due_date(due_date)
.add_item(invoice_item)
.build()
.expect("Failed to create invoice");
invoice
}
/// Process a payment for an invoice
fn process_payment(invoice: &mut Invoice) {
// Create a payment for the full amount
let payment = Payment::new(
invoice.total_amount.clone(),
"Credit Card".to_string(),
"Payment received via credit card ending in 1234".to_string()
);
// Add the payment to the invoice
invoice.add_payment(payment);
// The invoice should now be marked as paid
assert_eq!(invoice.payment_status, PaymentStatus::Paid);
assert_eq!(invoice.status, InvoiceStatus::Paid);
}

View File

@@ -0,0 +1,7 @@
//! Business example for HeroDB
//!
//! This module demonstrates business models in action,
//! including products, sales, invoices, and payments.
// Include the main module directly
pub mod main;

View File

@@ -0,0 +1,368 @@
use chrono::{Utc, Duration};
use herodb::db::{DBBuilder, DB};
use herodb::models::gov::{
Company, CompanyStatus, BusinessType,
Shareholder, ShareholderType,
Meeting, Attendee, MeetingStatus, AttendeeRole, AttendeeStatus,
User,
Vote, VoteOption, Ballot, VoteStatus,
Resolution, ResolutionStatus, Approval,
Committee, CommitteeRole, CommitteeMember
};
use std::path::PathBuf;
use std::fs;
fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("DB Example: Gov Module");
println!("============================");
// Create a temporary directory for the database
let db_path = PathBuf::from("/tmp/dbexample_gov");
if db_path.exists() {
fs::remove_dir_all(&db_path)?;
}
fs::create_dir_all(&db_path)?;
println!("Database path: {:?}", db_path);
// Create a database instance with our governance models registered
let db = DBBuilder::new(&db_path)
.register_model::<Company>()
.register_model::<Shareholder>()
.register_model::<Meeting>()
.register_model::<User>()
.register_model::<Vote>()
.register_model::<Resolution>()
.register_model::<Committee>()
.build()?;
println!("\n1. Creating a Company");
println!("-------------------");
// Create a company
let company = Company::new(
1,
"Acme Corporation".to_string(),
"ACM123456".to_string(),
Utc::now(),
"December 31".to_string(),
"info@acmecorp.com".to_string(),
"+1-555-123-4567".to_string(),
"https://acmecorp.com".to_string(),
"123 Main St, Anytown, USA".to_string(),
BusinessType::new(BusinessType::COOP.to_string())
.unwrap_or_else(|e| {
eprintln!("Warning: {}", e);
BusinessType::new_unchecked(BusinessType::COOP.to_string())
}),
"Technology".to_string(),
"A leading technology company".to_string(),
CompanyStatus::Active,
);
// Insert the company
db.set(&company)?;
println!("Company created: {} (ID: {})", company.name, company.id);
println!("Status: {:?}, Business Type: {}", company.status, company.business_type.as_str());
println!("\n2. Creating Users");
println!("---------------");
// Create users
let user1 = User::new(
1,
"John Doe".to_string(),
"john.doe@acmecorp.com".to_string(),
"password123".to_string(), // In a real app, this would be hashed
"Acme Corporation".to_string(),
"CEO".to_string(),
);
let user2 = User::new(
2,
"Jane Smith".to_string(),
"jane.smith@acmecorp.com".to_string(),
"password456".to_string(), // In a real app, this would be hashed
"Acme Corporation".to_string(),
"CFO".to_string(),
);
let user3 = User::new(
3,
"Bob Johnson".to_string(),
"bob.johnson@acmecorp.com".to_string(),
"password789".to_string(), // In a real app, this would be hashed
"Acme Corporation".to_string(),
"CTO".to_string(),
);
// Insert the users
db.set(&user1)?;
db.set(&user2)?;
db.set(&user3)?;
println!("User created: {} ({})", user1.name, user1.role);
println!("User created: {} ({})", user2.name, user2.role);
println!("User created: {} ({})", user3.name, user3.role);
println!("\n3. Creating Shareholders");
println!("----------------------");
// Create shareholders
let mut shareholder1 = Shareholder::new(
1,
company.id,
user1.id,
user1.name.clone(),
1000.0,
40.0,
ShareholderType::Individual,
);
let mut shareholder2 = Shareholder::new(
2,
company.id,
user2.id,
user2.name.clone(),
750.0,
30.0,
ShareholderType::Individual,
);
let mut shareholder3 = Shareholder::new(
3,
company.id,
user3.id,
user3.name.clone(),
750.0,
30.0,
ShareholderType::Individual,
);
// Insert the shareholders
db.set(&shareholder1)?;
db.set(&shareholder2)?;
db.set(&shareholder3)?;
println!("Shareholder created: {} ({} shares, {}%)",
shareholder1.name, shareholder1.shares, shareholder1.percentage);
println!("Shareholder created: {} ({} shares, {}%)",
shareholder2.name, shareholder2.shares, shareholder2.percentage);
println!("Shareholder created: {} ({} shares, {}%)",
shareholder3.name, shareholder3.shares, shareholder3.percentage);
// Update shareholder shares
shareholder1.update_shares(1100.0, 44.0);
db.set(&shareholder1)?;
println!("Updated shareholder: {} ({} shares, {}%)",
shareholder1.name, shareholder1.shares, shareholder1.percentage);
println!("\n4. Creating a Meeting");
println!("------------------");
// Create a meeting
let mut meeting = Meeting::new(
1,
company.id,
"Q2 Board Meeting".to_string(),
Utc::now() + Duration::days(7), // Meeting in 7 days
"Conference Room A".to_string(),
"Quarterly board meeting to discuss financial results".to_string(),
);
// Create attendees
let attendee1 = Attendee::new(
1,
meeting.id,
user1.id,
user1.name.clone(),
AttendeeRole::Coordinator,
);
let attendee2 = Attendee::new(
2,
meeting.id,
user2.id,
user2.name.clone(),
AttendeeRole::Member,
);
let attendee3 = Attendee::new(
3,
meeting.id,
user3.id,
user3.name.clone(),
AttendeeRole::Member,
);
// Add attendees to the meeting
meeting.add_attendee(attendee1);
meeting.add_attendee(attendee2);
meeting.add_attendee(attendee3);
// Insert the meeting
db.set(&meeting)?;
println!("Meeting created: {} ({})", meeting.title, meeting.date.format("%Y-%m-%d %H:%M"));
println!("Status: {:?}, Attendees: {}", meeting.status, meeting.attendees.len());
// Update attendee status
if let Some(attendee) = meeting.find_attendee_by_user_id_mut(user2.id) {
attendee.update_status(AttendeeStatus::Confirmed);
}
if let Some(attendee) = meeting.find_attendee_by_user_id_mut(user3.id) {
attendee.update_status(AttendeeStatus::Confirmed);
}
db.set(&meeting)?;
// Get confirmed attendees
let confirmed = meeting.confirmed_attendees();
println!("Confirmed attendees: {}", confirmed.len());
for attendee in confirmed {
println!(" - {} ({})", attendee.name, match attendee.role {
AttendeeRole::Coordinator => "Coordinator",
AttendeeRole::Member => "Member",
AttendeeRole::Secretary => "Secretary",
AttendeeRole::Participant => "Participant",
AttendeeRole::Advisor => "Advisor",
AttendeeRole::Admin => "Admin",
});
}
println!("\n5. Creating a Resolution");
println!("----------------------");
// Create a resolution
let mut resolution = Resolution::new(
1,
company.id,
"Approval of Q1 Financial Statements".to_string(),
"Resolution to approve the Q1 financial statements".to_string(),
"The Board of Directors hereby approves the financial statements for Q1 2025.".to_string(),
user1.id, // Proposed by the CEO
);
// Link the resolution to the meeting
resolution.link_to_meeting(meeting.id);
// Insert the resolution
db.set(&resolution)?;
println!("Resolution created: {} (Status: {:?})", resolution.title, resolution.status);
// Propose the resolution
resolution.propose();
db.set(&resolution)?;
println!("Resolution proposed on {}", resolution.proposed_at.format("%Y-%m-%d"));
// Add approvals
resolution.add_approval(user1.id, user1.name.clone(), true, "Approved as proposed".to_string());
resolution.add_approval(user2.id, user2.name.clone(), true, "Financials look good".to_string());
resolution.add_approval(user3.id, user3.name.clone(), true, "No concerns".to_string());
db.set(&resolution)?;
// Check approval status
println!("Approvals: {}, Rejections: {}",
resolution.approval_count(),
resolution.rejection_count());
// Approve the resolution
resolution.approve();
db.set(&resolution)?;
println!("Resolution approved on {}",
resolution.approved_at.unwrap().format("%Y-%m-%d"));
println!("\n6. Creating a Vote");
println!("----------------");
// Create a vote
let mut vote = Vote::new(
1,
company.id,
"Vote on New Product Line".to_string(),
"Vote to approve investment in new product line".to_string(),
Utc::now(),
Utc::now() + Duration::days(3), // Voting period of 3 days
VoteStatus::Open,
);
// Add voting options
vote.add_option("Approve".to_string(), 0);
vote.add_option("Reject".to_string(), 0);
vote.add_option("Abstain".to_string(), 0);
// Insert the vote
db.set(&vote)?;
println!("Vote created: {} (Status: {:?})", vote.title, vote.status);
println!("Voting period: {} to {}",
vote.start_date.format("%Y-%m-%d"),
vote.end_date.format("%Y-%m-%d"));
// Cast ballots
vote.add_ballot(user1.id, 1, 1000); // User 1 votes "Approve" with 1000 shares
vote.add_ballot(user2.id, 1, 750); // User 2 votes "Approve" with 750 shares
vote.add_ballot(user3.id, 3, 750); // User 3 votes "Abstain" with 750 shares
db.set(&vote)?;
// Check voting results
println!("Voting results:");
for option in &vote.options {
println!(" - {}: {} votes", option.text, option.count);
}
// Create a resolution for this vote
let mut vote_resolution = Resolution::new(
2,
company.id,
"Investment in New Product Line".to_string(),
"Resolution to approve investment in new product line".to_string(),
"The Board of Directors hereby approves an investment of $1,000,000 in the new product line.".to_string(),
user1.id, // Proposed by the CEO
);
// Link the resolution to the vote
vote_resolution.link_to_vote(vote.id);
vote_resolution.propose();
db.set(&vote_resolution)?;
println!("Created resolution linked to vote: {}", vote_resolution.title);
println!("\n7. Retrieving Related Objects");
println!("---------------------------");
// Retrieve company and related objects
let retrieved_company = db.get::<Company>(company.id)?;
println!("Company: {} (ID: {})", retrieved_company.name, retrieved_company.id);
// Get resolutions for this company
let company_resolutions = retrieved_company.get_resolutions(&db)?;
println!("Company has {} resolutions:", company_resolutions.len());
for res in company_resolutions {
println!(" - {} (Status: {:?})", res.title, res.status);
}
// Get meeting and its resolutions
let retrieved_meeting = db.get::<Meeting>(meeting.id)?;
println!("Meeting: {} ({})", retrieved_meeting.title, retrieved_meeting.date.format("%Y-%m-%d"));
let meeting_resolutions = retrieved_meeting.get_resolutions(&db)?;
println!("Meeting has {} resolutions:", meeting_resolutions.len());
for res in meeting_resolutions {
println!(" - {} (Status: {:?})", res.title, res.status);
}
// Get vote and its resolution
let retrieved_vote = db.get::<Vote>(vote.id)?;
println!("Vote: {} (Status: {:?})", retrieved_vote.title, retrieved_vote.status);
if let Ok(Some(vote_res)) = retrieved_vote.get_resolution(&db) {
println!("Vote is linked to resolution: {}", vote_res.title);
}
// Get resolution and its related objects
let retrieved_resolution = db.get::<Resolution>(resolution.id)?;
println!("Resolution: {} (Status: {:?})", retrieved_resolution.title, retrieved_resolution.status);
if let Ok(Some(res_meeting)) = retrieved_resolution.get_meeting(&db) {
println!("Resolution is discussed in meeting: {}", res_meeting.title);
}
println!("\nExample completed successfully!");
Ok(())
}

View File

@@ -0,0 +1,399 @@
use chrono::{Utc, Duration};
use herodb::db::{DBBuilder, GetId};
use herodb::models::mcc::{
Calendar, Event,
Email, Attachment, Envelope,
Contact, Message
};
use herodb::models::circle::Circle;
use std::path::PathBuf;
use std::fs;
fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("DB Example MCC: Mail, Calendar, Contacts with Group Support");
println!("=======================================================");
// Create a temporary directory for the database
let db_path = PathBuf::from("/tmp/dbexample_mcc");
if db_path.exists() {
fs::remove_dir_all(&db_path)?;
}
fs::create_dir_all(&db_path)?;
println!("Database path: {:?}", db_path);
// Create a database instance with our models registered
let db = DBBuilder::new(&db_path)
.register_type::<Calendar>("calendar")
.register_type::<Event>("event")
.register_type::<Email>("email")
.register_type::<Contact>("contact")
.register_type::<Message>("message")
.register_model::<Circle>() // Circle still uses the Model trait
.build()?;
println!("\n1. Creating Circles (Groups)");
println!("---------------------------");
// Create circles (groups)
let work_circle = Circle::new(
1,
"Work".to_string(),
"Work-related communications".to_string()
);
let family_circle = Circle::new(
2,
"Family".to_string(),
"Family communications".to_string()
);
let friends_circle = Circle::new(
3,
"Friends".to_string(),
"Friends communications".to_string()
);
// Insert circles
db.set::<Circle>(&work_circle)?;
db.set::<Circle>(&family_circle)?;
db.set::<Circle>(&friends_circle)?;
println!("Created circles:");
println!(" - Circle #{}: {}", work_circle.id, work_circle.name);
println!(" - Circle #{}: {}", family_circle.id, family_circle.name);
println!(" - Circle #{}: {}", friends_circle.id, friends_circle.name);
println!("\n2. Creating Contacts with Group Support");
println!("------------------------------------");
// Create contacts
let mut john = Contact::new(
1,
"John".to_string(),
"Doe".to_string(),
"john.doe@example.com".to_string(),
"work".to_string()
);
john.add_group(work_circle.id);
let mut alice = Contact::new(
2,
"Alice".to_string(),
"Smith".to_string(),
"alice.smith@example.com".to_string(),
"family".to_string()
);
alice.add_group(family_circle.id);
let mut bob = Contact::new(
3,
"Bob".to_string(),
"Johnson".to_string(),
"bob.johnson@example.com".to_string(),
"friends".to_string()
);
bob.add_group(friends_circle.id);
bob.add_group(work_circle.id); // Bob is both a friend and a work contact
// Insert contacts
db.set_any::<Contact>(&john, "contact")?;
db.set_any::<Contact>(&alice, "contact")?;
db.set_any::<Contact>(&bob, "contact")?;
println!("Created contacts:");
println!(" - {}: {} (Groups: {:?})", john.full_name(), john.email, john.groups);
println!(" - {}: {} (Groups: {:?})", alice.full_name(), alice.email, alice.groups);
println!(" - {}: {} (Groups: {:?})", bob.full_name(), bob.email, bob.groups);
println!("\n3. Creating Calendars with Group Support");
println!("-------------------------------------");
// Create calendars
let mut work_calendar = Calendar::new(
1,
"Work Calendar".to_string(),
"Work-related events".to_string()
);
work_calendar.add_group(work_circle.id);
let mut personal_calendar = Calendar::new(
2,
"Personal Calendar".to_string(),
"Personal events".to_string()
);
personal_calendar.add_group(family_circle.id);
personal_calendar.add_group(friends_circle.id);
// Insert calendars
db.set_any::<Calendar>(&work_calendar, "calendar")?;
db.set_any::<Calendar>(&personal_calendar, "calendar")?;
println!("Created calendars:");
println!(" - {}: {} (Groups: {:?})", work_calendar.id, work_calendar.title, work_calendar.groups);
println!(" - {}: {} (Groups: {:?})", personal_calendar.id, personal_calendar.title, personal_calendar.groups);
println!("\n4. Creating Events with Group Support");
println!("----------------------------------");
// Create events
let now = Utc::now();
let tomorrow = now + Duration::days(1);
let next_week = now + Duration::days(7);
let mut work_meeting = Event::new(
1,
work_calendar.id,
"Team Meeting".to_string(),
"Weekly team sync".to_string(),
"Conference Room A".to_string(),
tomorrow,
tomorrow + Duration::hours(1),
"organizer@example.com".to_string()
);
work_meeting.add_group(work_circle.id);
work_meeting.add_attendee(john.email.clone());
work_meeting.add_attendee(bob.email.clone());
let mut family_dinner = Event::new(
2,
personal_calendar.id,
"Family Dinner".to_string(),
"Weekly family dinner".to_string(),
"Home".to_string(),
next_week,
next_week + Duration::hours(2),
"me@example.com".to_string()
);
family_dinner.add_group(family_circle.id);
family_dinner.add_attendee(alice.email.clone());
// Insert events
db.set_any::<Event>(&work_meeting, "event")?;
db.set_any::<Event>(&family_dinner, "event")?;
println!("Created events:");
println!(" - {}: {} on {} (Groups: {:?})",
work_meeting.id,
work_meeting.title,
work_meeting.start_time.format("%Y-%m-%d %H:%M"),
work_meeting.groups
);
println!(" - {}: {} on {} (Groups: {:?})",
family_dinner.id,
family_dinner.title,
family_dinner.start_time.format("%Y-%m-%d %H:%M"),
family_dinner.groups
);
println!("\n5. Creating Emails with Group Support");
println!("----------------------------------");
// Create emails
let mut work_email = Email::new(
1,
101,
1,
"INBOX".to_string(),
"Here are the meeting notes from yesterday's discussion.".to_string()
);
work_email.add_group(work_circle.id);
let work_attachment = Attachment {
filename: "meeting_notes.pdf".to_string(),
content_type: "application/pdf".to_string(),
hash: "abc123def456".to_string(),
size: 1024,
};
work_email.add_attachment(work_attachment);
let work_envelope = Envelope {
date: now.timestamp(),
subject: "Meeting Notes".to_string(),
from: vec!["john.doe@example.com".to_string()],
sender: vec!["john.doe@example.com".to_string()],
reply_to: vec!["john.doe@example.com".to_string()],
to: vec!["me@example.com".to_string()],
cc: vec!["bob.johnson@example.com".to_string()],
bcc: vec![],
in_reply_to: "".to_string(),
message_id: "msg123@example.com".to_string(),
};
work_email.set_envelope(work_envelope);
let mut family_email = Email::new(
2,
102,
2,
"INBOX".to_string(),
"Looking forward to seeing you at dinner next week!".to_string()
);
family_email.add_group(family_circle.id);
let family_envelope = Envelope {
date: now.timestamp(),
subject: "Family Dinner".to_string(),
from: vec!["alice.smith@example.com".to_string()],
sender: vec!["alice.smith@example.com".to_string()],
reply_to: vec!["alice.smith@example.com".to_string()],
to: vec!["me@example.com".to_string()],
cc: vec![],
bcc: vec![],
in_reply_to: "".to_string(),
message_id: "msg456@example.com".to_string(),
};
family_email.set_envelope(family_envelope);
// Insert emails
db.set_any::<Email>(&work_email, "email")?;
db.set_any::<Email>(&family_email, "email")?;
println!("Created emails:");
println!(" - From: {}, Subject: {} (Groups: {:?})",
work_email.envelope.as_ref().unwrap().from[0],
work_email.envelope.as_ref().unwrap().subject,
work_email.groups
);
println!(" - From: {}, Subject: {} (Groups: {:?})",
family_email.envelope.as_ref().unwrap().from[0],
family_email.envelope.as_ref().unwrap().subject,
family_email.groups
);
println!("\n6. Creating Messages (Chat) with Group Support");
println!("-----------------------------------------");
// Create messages
let mut work_chat = Message::new(
1,
"thread_work_123".to_string(),
"john.doe@example.com".to_string(),
"Can we move the meeting to 3pm?".to_string()
);
work_chat.add_group(work_circle.id);
work_chat.add_recipient("me@example.com".to_string());
work_chat.add_recipient("bob.johnson@example.com".to_string());
let mut friends_chat = Message::new(
2,
"thread_friends_456".to_string(),
"bob.johnson@example.com".to_string(),
"Are we still on for the game this weekend?".to_string()
);
friends_chat.add_group(friends_circle.id);
friends_chat.add_recipient("me@example.com".to_string());
friends_chat.add_reaction("👍".to_string());
// Insert messages
db.set_any::<Message>(&work_chat, "message")?;
db.set_any::<Message>(&friends_chat, "message")?;
println!("Created messages:");
println!(" - From: {}, Content: {} (Groups: {:?})",
work_chat.sender_id,
work_chat.content,
work_chat.groups
);
println!(" - From: {}, Content: {} (Groups: {:?}, Reactions: {:?})",
friends_chat.sender_id,
friends_chat.content,
friends_chat.groups,
friends_chat.meta.reactions
);
println!("\n7. Demonstrating Utility Methods");
println!("------------------------------");
// Filter contacts by group
println!("\nFiltering contacts by work group (ID: {}):", work_circle.id);
let all_contacts = db.list_any::<Contact>()?;
for contact in all_contacts {
if contact.filter_by_groups(&[work_circle.id]) {
println!(" - {} ({})", contact.full_name(), contact.email);
}
}
// Search emails by subject
println!("\nSearching emails with subject containing 'Meeting':");
let all_emails = db.list_any::<Email>()?;
for email in all_emails {
if email.search_by_subject("Meeting") {
println!(" - Subject: {}, From: {}",
email.envelope.as_ref().unwrap().subject,
email.envelope.as_ref().unwrap().from[0]
);
}
}
// Get events for a calendar
println!("\nGetting events for Work Calendar (ID: {}):", work_calendar.id);
let all_events = db.list_any::<Event>()?;
let work_events: Vec<Event> = all_events
.into_iter()
.filter(|event| event.calendar_id == work_calendar.id)
.collect();
for event in work_events {
println!(" - {}: {} on {}",
event.id,
event.title,
event.start_time.format("%Y-%m-%d %H:%M")
);
}
// Get attendee contacts for an event
println!("\nGetting attendee contacts for Team Meeting (ID: {}):", work_meeting.id);
let all_contacts = db.list_any::<Contact>()?;
let attendee_contacts: Vec<Contact> = all_contacts
.into_iter()
.filter(|contact| work_meeting.attendees.contains(&contact.email))
.collect();
for contact in attendee_contacts {
println!(" - {} ({})", contact.full_name(), contact.email);
}
// Convert email to message
println!("\nConverting work email to message:");
let email_to_message = work_email.to_message(3, "thread_converted_789".to_string());
println!(" - Original Email Subject: {}", work_email.envelope.as_ref().unwrap().subject);
println!(" - Converted Message Content: {}", email_to_message.content.split('\n').next().unwrap_or(""));
println!(" - Converted Message Groups: {:?}", email_to_message.groups);
// Insert the converted message
db.set_any::<Message>(&email_to_message, "message")?;
println!("\n8. Relationship Management");
println!("------------------------");
// Get the calendar for an event
println!("\nGetting calendar for Family Dinner event (ID: {}):", family_dinner.id);
let event_calendar = db.get_any::<Calendar>(family_dinner.calendar_id)?;
println!(" - Calendar: {} ({})", event_calendar.title, event_calendar.description);
// Get events for a contact
println!("\nGetting events where John Doe is an attendee:");
let all_events = db.list_any::<Event>()?;
let john_events: Vec<Event> = all_events
.into_iter()
.filter(|event| event.attendees.contains(&john.email))
.collect();
for event in john_events {
println!(" - {}: {} on {}",
event.id,
event.title,
event.start_time.format("%Y-%m-%d %H:%M")
);
}
// Get messages in the same thread
println!("\nGetting all messages in the work chat thread:");
let all_messages = db.list_any::<Message>()?;
let thread_messages: Vec<Message> = all_messages
.into_iter()
.filter(|message| message.thread_id == work_chat.thread_id)
.collect();
for message in thread_messages {
println!(" - From: {}, Content: {}", message.sender_id, message.content);
}
println!("\nExample completed successfully!");
Ok(())
}

View File

@@ -0,0 +1,218 @@
use chrono::{DateTime, Duration, Utc};
use herodb::db::{DB, DBBuilder};
use herodb::models::biz::{
Currency, CurrencyBuilder, Product, ProductBuilder, ProductComponent, ProductComponentBuilder,
ProductStatus, ProductType, Sale, SaleBuilder, SaleItem, SaleItemBuilder, SaleStatus,
};
use rhai::{Engine, packages::Package};
use std::fs;
use std::path::PathBuf;
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Note: This example has been modified to work with the current API
println!("DB Example 2: Using Builder Pattern and Model-Specific Methods");
println!("============================================================");
// Create a temporary directory for the database
let db_path = PathBuf::from("/tmp/dbexample_prod");
if db_path.exists() {
fs::remove_dir_all(&db_path)?;
}
fs::create_dir_all(&db_path)?;
println!("Database path: {:?}", db_path);
// Skip the Rhai engine part as it has compatibility issues
println!("\nSkipping Rhai engine part due to compatibility issues");
// Create a database instance with our models registered
let mut db = DBBuilder::new(&db_path)
.register_model::<Product>()
.register_model::<Currency>()
.register_model::<Sale>()
.build()?;
println!("\n1. Creating Products with Builder Pattern");
println!("----------------------------------------");
// Create a currency using the builder
let usd = CurrencyBuilder::new()
.id(1) // Add the required ID
.amount(0.0) // Initial amount
.currency_code("USD")
.build()?;
// Insert the currency
db.insert_currency(usd.clone())?;
println!("Currency created: ${:.2} {}", usd.amount, usd.currency_code);
// Create product components using the builder
let component1 = ProductComponentBuilder::new()
.id(101)
.name("Basic Support")
.description("24/7 email support")
.quantity(1)
.build()?;
let component2 = ProductComponentBuilder::new()
.id(102)
.name("Premium Support")
.description("24/7 phone and email support")
.quantity(1)
.build()?;
// Create products using the builder
let product1 = ProductBuilder::new()
.id(1)
.name("Standard Plan")
.description("Our standard service offering")
.price(
CurrencyBuilder::new()
.id(2) // Add ID
.amount(29.99)
.currency_code("USD")
.build()?,
)
.type_(ProductType::Service)
.category("Subscription")
.status(ProductStatus::Available)
.max_amount(1000)
.validity_days(30)
.add_component(component1)
.build()?;
let product2 = ProductBuilder::new()
.id(2)
.name("Premium Plan")
.description("Our premium service offering with priority support")
.price(
CurrencyBuilder::new()
.id(3) // Add ID
.amount(99.99)
.currency_code("USD")
.build()?,
)
.type_(ProductType::Service)
.category("Subscription")
.status(ProductStatus::Available)
.max_amount(500)
.validity_days(30)
.add_component(component2)
.build()?;
// Insert products using model-specific methods
db.insert_product(product1.clone())?;
db.insert_product(product2.clone())?;
println!(
"Product created: {} (${:.2})",
product1.name, product1.price.amount
);
println!(
"Product created: {} (${:.2})",
product2.name, product2.price.amount
);
println!("\n2. Retrieving Products");
println!("--------------------");
// Retrieve products using model-specific methods
let retrieved_product1 = db.get_product(1)?;
println!(
"Retrieved: {} (${:.2})",
retrieved_product1.name, retrieved_product1.price.amount
);
println!("Components:");
for component in &retrieved_product1.components {
println!(" - {} ({})", component.name, component.description);
}
println!("\n3. Listing All Products");
println!("----------------------");
// List all products using model-specific methods
let all_products = db.list_products()?;
println!("Found {} products:", all_products.len());
for product in all_products {
println!(
" - {} (${:.2}, {})",
product.name,
product.price.amount,
if product.is_purchasable() {
"Available"
} else {
"Unavailable"
}
);
}
println!("\n4. Creating a Sale");
println!("-----------------");
// Create a sale using the builder
let now = Utc::now();
let item1 = SaleItemBuilder::new()
.id(201)
.sale_id(1)
.product_id(Some(1))
.name("Standard Plan")
.quantity(1)
.unit_price(
CurrencyBuilder::new()
.id(4) // Add ID
.amount(29.99)
.currency_code("USD")
.build()?,
)
.active_till(now + Duration::days(30))
.build()?;
let sale = SaleBuilder::new()
.id(1)
.company_id(101)
.customer_id(123)
.currency_code("USD")
.status(SaleStatus::Pending)
.add_item(item1)
.build()?;
// Insert the sale using model-specific methods
db.insert_sale(sale.clone())?;
println!(
"Sale created: #{} for customer #{} (${:.2})",
sale.id, sale.customer_id, sale.total_amount.amount
);
println!("\n5. Updating a Sale");
println!("-----------------");
// Retrieve the sale, update it, and save it back
let mut retrieved_sale = db.get_sale(1)?;
println!(
"Retrieved sale: #{} with status {:?}",
retrieved_sale.id, retrieved_sale.status
);
// Update the status
retrieved_sale.update_status(SaleStatus::Completed);
db.insert_sale(retrieved_sale.clone())?;
println!("Updated sale status to {:?}", retrieved_sale.status);
println!("\n6. Deleting Objects");
println!("------------------");
// Delete a product
db.delete_product(2)?;
println!("Deleted product #2");
// List remaining products
let remaining_products = db.list_products()?;
println!("Remaining products: {}", remaining_products.len());
for product in remaining_products {
println!(" - {}", product.name);
}
println!("\nExample completed successfully!");
Ok(())
}

View File

@@ -0,0 +1,7 @@
//! Command examples for HeroDB
//!
//! This module contains various example commands and applications
//! that demonstrate how to use HeroDB in different scenarios.
// Export the example modules
// pub mod dbexample_biz; // Commented out to avoid circular imports

View File

@@ -0,0 +1,646 @@
use crate::db::error::{DbError, DbResult};
use crate::db::model::{Model, IndexKey};
use crate::db::store::{DbOperations, OurDbStore};
use crate::db::generic_store::{GenericStore, GetId};
use crate::db::tst_index::TSTIndexManager;
use std::any::TypeId;
use std::collections::HashMap;
use std::fmt::Debug;
use std::path::{Path, PathBuf};
use std::sync::{Arc, RwLock};
use rhai::{CustomType, EvalAltResult, TypeBuilder};
use serde::{Serialize, de::DeserializeOwned};
/// Represents a single database operation in a transaction
#[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
},
}
/// Transaction state for DB operations
#[derive(Clone)]
pub struct TransactionState {
operations: Vec<DbOperation>,
active: bool,
}
impl TransactionState {
/// Create a new transaction state
pub fn new() -> Self {
Self {
operations: Vec::new(),
active: true,
}
}
}
/// 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>>>,
}
/// Builder for DB that allows registering models
#[derive(Clone, CustomType)]
pub struct DBBuilder {
base_path: PathBuf,
model_registrations: Vec<Arc<dyn ModelRegistration>>,
}
/// Trait for model registration
pub trait ModelRegistration: Send + Sync {
fn register(&self, path: &Path) -> DbResult<(TypeId, Arc<RwLock<dyn DbOperations>>)>;
}
/// Implementation of ModelRegistration for any Model type
pub struct ModelRegistrar<T: Model> {
phantom: std::marker::PhantomData<T>,
}
impl<T: Model> ModelRegistrar<T> {
pub fn new() -> Self {
Self {
phantom: std::marker::PhantomData,
}
}
}
/// Implementation of ModelRegistration for any serializable type that implements GetId
pub struct TypeRegistrar<T: Serialize + DeserializeOwned + GetId + Send + Sync + 'static> {
prefix: &'static str,
phantom: std::marker::PhantomData<T>,
}
impl<T: Serialize + DeserializeOwned + GetId + Send + Sync + 'static> TypeRegistrar<T> {
pub fn new(prefix: &'static str) -> Self {
Self {
prefix,
phantom: std::marker::PhantomData,
}
}
}
impl<T: Model> ModelRegistration for ModelRegistrar<T> {
fn register(&self, path: &Path) -> DbResult<(TypeId, Arc<RwLock<dyn DbOperations>>)> {
let store = OurDbStore::<T>::open(path.join(T::db_prefix()))?;
Ok((TypeId::of::<T>(), Arc::new(RwLock::new(store)) as Arc<RwLock<dyn DbOperations>>))
}
}
impl<T: Serialize + DeserializeOwned + GetId + Send + Sync + 'static> ModelRegistration for TypeRegistrar<T> {
fn register(&self, path: &Path) -> DbResult<(TypeId, Arc<RwLock<dyn DbOperations>>)> {
let store = GenericStore::<T>::open(path, self.prefix)?;
Ok((TypeId::of::<T>(), Arc::new(RwLock::new(store)) as Arc<RwLock<dyn DbOperations>>))
}
}
impl DBBuilder {
/// Create a new DB builder
pub fn new<P: Into<PathBuf>>(base_path: P) -> Self {
Self {
base_path: base_path.into(),
model_registrations: Vec::new(),
}
}
pub fn with_path<P: Into<PathBuf>>(base_path: P) -> Self {
Self {
base_path: base_path.into(),
model_registrations: Vec::new(),
}
}
/// Register a model type with the DB
pub fn register_model<T: Model>(mut self) -> Self {
self.model_registrations
.push(Arc::new(ModelRegistrar::<T>::new()));
self
}
/// Register any serializable type with the DB
pub fn register_type<T: Serialize + DeserializeOwned + GetId + Send + Sync + 'static>(
mut self,
prefix: &'static str
) -> Self {
self.model_registrations
.push(Arc::new(TypeRegistrar::<T>::new(prefix)));
self
}
/// Build the DB with the registered models
pub fn build(self) -> Result<DB, Box<EvalAltResult>> {
let base_path = self.base_path;
// Ensure base directory exists
if !base_path.exists() {
std::fs::create_dir_all(&base_path).map_err(|e| {
EvalAltResult::ErrorSystem("Could not create base dir".to_string(), Box::new(e))
})?;
}
// Register all models
let mut type_map: HashMap<TypeId, Arc<RwLock<dyn DbOperations>>> = HashMap::new();
for registration in self.model_registrations {
let (type_id, store) = registration.register(&base_path).map_err(|e| {
EvalAltResult::ErrorSystem("Could not register type".to_string(), Box::new(e))
})?;
type_map.insert(type_id, store);
}
// Create the TST index manager
let tst_index = TSTIndexManager::new(&base_path).map_err(|e| {
EvalAltResult::ErrorSystem("Could not create TST index manager".to_string(), Box::new(e))
})?;
let transaction = Arc::new(RwLock::new(None));
Ok(DB {
db_path: base_path,
type_map,
tst_index: Arc::new(RwLock::new(tst_index)),
transaction,
})
}
}
impl DB {
/// Create a new empty DB instance without any models
pub fn new<P: Into<PathBuf>>(base_path: P) -> DbResult<Self> {
let base_path = base_path.into();
// Ensure base directory exists
if !base_path.exists() {
std::fs::create_dir_all(&base_path)?;
}
// Create the TST index manager
let tst_index = TSTIndexManager::new(&base_path)?;
let transaction = Arc::new(RwLock::new(None));
Ok(Self {
db_path: base_path,
type_map: HashMap::new(),
tst_index: Arc::new(RwLock::new(tst_index)),
transaction,
})
}
// Transaction-related methods
/// Begin a new transaction
pub fn begin_transaction(&self) -> DbResult<()> {
let mut tx = self.transaction.write().unwrap();
if tx.is_some() {
return Err(DbError::TransactionError(
"Transaction already in progress".into(),
));
}
*tx = Some(TransactionState::new());
Ok(())
}
/// Check if a transaction is active
pub fn has_active_transaction(&self) -> bool {
let tx = self.transaction.read().unwrap();
tx.is_some() && tx.as_ref().unwrap().active
}
/// Apply a set operation with the serialized data - bypass transaction check
fn apply_set_operation(&self, model_type: TypeId, serialized: &[u8]) -> DbResult<()> {
// Get the database operations for this model type
if let Some(db_ops) = self.type_map.get(&model_type) {
// Just pass the raw serialized data to a special raw insert method
let mut db_ops_guard = db_ops.write().unwrap();
return db_ops_guard.insert_raw(serialized);
}
Err(DbError::GeneralError(format!(
"No DB registered for type ID {:?}",
model_type
)))
}
/// Commit the current transaction, applying all operations
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 (primary key only)
// We can't easily get the index keys in the transaction commit
// because we don't have the model type information at runtime
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,
} => {
// For delete operations, we can't get the index keys from the model
// because it's already deleted. We'll just delete the primary key.
// 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 (primary key only)
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()))
}
}
/// Rollback the current transaction, discarding all operations
pub fn rollback_transaction(&self) -> DbResult<()> {
let mut tx = self.transaction.write().unwrap();
if tx.is_none() {
return Err(DbError::TransactionError("No active transaction".into()));
}
*tx = None;
Ok(())
}
/// Get the path to the database
pub fn path(&self) -> &PathBuf {
&self.db_path
}
// Generic methods that work with any supported model type
/// Insert a model instance into its appropriate database based on type
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()?;
// Get the index keys for this model
let index_keys = model.db_keys();
// Record a Set operation in the transaction with prefix and ID
tx_state.operations.push(DbOperation::Set {
model_type: TypeId::of::<T>(),
serialized,
model_prefix: T::db_prefix().to_string(),
model_id: model.get_id(),
});
return Ok(());
}
}
// If we got here, either there's no transaction or it's not active
// Drop the write lock before doing a direct database operation
drop(tx_guard);
// Execute directly
match self.type_map.get(&TypeId::of::<T>()) {
Some(db_ops) => {
let mut db_ops_guard = db_ops.write().unwrap();
db_ops_guard.insert(model)?;
// Also update the TST index with all index keys
let mut tst_index = self.tst_index.write().unwrap();
let prefix = T::db_prefix();
let id = model.get_id();
let data = model.to_bytes()?;
let index_keys = model.db_keys();
tst_index.set_with_indexes(prefix, id, data, &index_keys)?;
Ok(())
},
None => Err(DbError::TypeError),
}
}
/// Insert any serializable struct that implements GetId
pub fn set_any<T: Serialize + DeserializeOwned + GetId + Send + Sync + 'static>(
&self,
item: &T,
prefix: &str
) -> 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 item for later use
let serialized = bincode::serialize(item).map_err(DbError::SerializationError)?;
// Record a Set operation in the transaction with prefix and ID
tx_state.operations.push(DbOperation::Set {
model_type: TypeId::of::<T>(),
serialized,
model_prefix: prefix.to_string(),
model_id: item.get_id(),
});
return Ok(());
}
}
// If we got here, either there's no transaction or it's not active
// Drop the write lock before doing a direct database operation
drop(tx_guard);
// Execute directly
match self.type_map.get(&TypeId::of::<T>()) {
Some(db_ops) => {
// Serialize the item
let data = bincode::serialize(item).map_err(DbError::SerializationError)?;
// Insert the raw data
let mut db_ops_guard = db_ops.write().unwrap();
db_ops_guard.insert_raw(&data)?;
// Also update the TST index (primary key only)
let mut tst_index = self.tst_index.write().unwrap();
tst_index.set(prefix, item.get_id(), data)?;
Ok(())
},
None => Err(DbError::TypeError),
}
}
/// Check the transaction state for the given type and id
fn check_transaction<T: Model>(&self, id: u32) -> Option<Result<Option<T>, DbError>> {
// Try to acquire a read lock on the transaction
let tx_guard = self.transaction.read().unwrap();
if let Some(tx_state) = tx_guard.as_ref() {
if !tx_state.active {
return None;
}
let type_id = TypeId::of::<T>();
// Process operations in reverse order (last operation wins)
for op in tx_state.operations.iter().rev() {
match op {
// First check if this ID has been deleted in the transaction
DbOperation::Delete {
model_type,
id: op_id,
model_prefix: _,
} => {
if *model_type == type_id && *op_id == id {
// Return NotFound error for deleted records
return Some(Err(DbError::NotFound(id)));
}
}
// Then check if it has been set in the transaction
DbOperation::Set {
model_type,
serialized,
model_prefix: _,
model_id,
} => {
if *model_type == type_id && *model_id == id {
// Try to deserialize
match T::from_bytes(serialized) {
Ok(model) => {
return Some(Ok(Some(model)));
}
Err(_) => continue, // Skip if deserialization fails
}
}
}
}
}
}
// Not found in transaction (continue to database)
None
}
/// Get a model instance by its ID and type
pub fn get<T: Model>(&self, id: u32) -> DbResult<T> {
// First check if there's a pending value in the current transaction
if let Some(tx_result) = self.check_transaction::<T>(id) {
match tx_result {
Ok(Some(model)) => return Ok(model),
Ok(None) => return Err(DbError::NotFound(id)),
Err(e) => return Err(e),
}
}
// If not found in transaction, get from database
match self.type_map.get(&TypeId::of::<T>()) {
Some(db_ops) => {
let mut db_ops_guard = db_ops.write().unwrap();
let any_result = db_ops_guard.get(id)?;
// Try to downcast to T
match any_result.downcast::<T>() {
Ok(boxed_t) => Ok(*boxed_t),
Err(_) => Err(DbError::TypeError),
}
}
None => Err(DbError::TypeError),
}
}
/// Get any serializable struct by its ID and type
pub fn get_any<T: Serialize + DeserializeOwned + GetId + Send + Sync + 'static>(
&self,
id: u32
) -> DbResult<T> {
// If not found in transaction, get from database
match self.type_map.get(&TypeId::of::<T>()) {
Some(db_ops) => {
let mut db_ops_guard = db_ops.write().unwrap();
let any_result = db_ops_guard.get(id)?;
// Try to downcast to T
match any_result.downcast::<T>() {
Ok(boxed_t) => Ok(*boxed_t),
Err(_) => Err(DbError::TypeError),
}
}
None => Err(DbError::TypeError),
}
}
/// Delete a model instance by its ID and type
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
tx_state.operations.push(DbOperation::Delete {
model_type: TypeId::of::<T>(),
id,
model_prefix: T::db_prefix().to_string(),
});
return Ok(());
}
}
// If we got here, either there's no transaction or it's not active
// Drop the write lock before doing a direct database operation
drop(tx_guard);
// Execute directly
match self.type_map.get(&TypeId::of::<T>()) {
Some(db_ops) => {
let mut db_ops_guard = db_ops.write().unwrap();
db_ops_guard.delete(id)?;
// Also delete from the TST index
let mut tst_index = self.tst_index.write().unwrap();
tst_index.delete(T::db_prefix(), id)?;
Ok(())
}
None => Err(DbError::TypeError),
}
}
/// Delete any serializable struct by its ID and type
pub fn delete_any<T: Serialize + DeserializeOwned + GetId + Send + Sync + 'static>(
&self,
id: u32,
prefix: &str
) -> DbResult<()> {
// Execute directly
match self.type_map.get(&TypeId::of::<T>()) {
Some(db_ops) => {
let mut db_ops_guard = db_ops.write().unwrap();
db_ops_guard.delete(id)?;
// Also delete from the TST index
let mut tst_index = self.tst_index.write().unwrap();
tst_index.delete(prefix, id)?;
Ok(())
}
None => Err(DbError::TypeError),
}
}
/// List all model instances of a given type
pub fn list<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 any_result = db_ops_guard.list()?;
// Try to downcast to Vec<T>
match any_result.downcast::<Vec<T>>() {
Ok(boxed_vec) => Ok(*boxed_vec),
Err(_) => Err(DbError::TypeError),
}
}
None => Err(DbError::TypeError),
}
}
/// List all instances of any serializable type
pub fn list_any<T: Serialize + DeserializeOwned + GetId + Send + Sync + 'static>(
&self
) -> DbResult<Vec<T>> {
match self.type_map.get(&TypeId::of::<T>()) {
Some(db_ops) => {
let db_ops_guard = db_ops.read().unwrap();
let any_result = db_ops_guard.list()?;
// Try to downcast to Vec<T>
match any_result.downcast::<Vec<T>>() {
Ok(boxed_vec) => Ok(*boxed_vec),
Err(_) => Err(DbError::TypeError),
}
}
None => Err(DbError::TypeError),
}
}
/// Get the history of a model instance
pub fn get_history<T: Model>(&self, id: u32, depth: u8) -> DbResult<Vec<T>> {
match self.type_map.get(&TypeId::of::<T>()) {
Some(db_ops) => {
let mut db_ops_guard = db_ops.write().unwrap();
let any_results = db_ops_guard.get_history(id, depth)?;
let mut results = Vec::with_capacity(any_results.len());
for any_result in any_results {
match any_result.downcast::<T>() {
Ok(boxed_t) => results.push(*boxed_t),
Err(_) => return Err(DbError::TypeError),
}
}
Ok(results)
}
None => Err(DbError::TypeError),
}
}
}

View File

@@ -0,0 +1,30 @@
use thiserror::Error;
use std::fmt::Debug;
/// Errors that can occur during database operations
#[derive(Error, Debug)]
pub enum DbError {
#[error("I/O error: {0}")]
IoError(#[from] std::io::Error),
#[error("Serialization/Deserialization error: {0}")]
SerializationError(#[from] bincode::Error),
#[error("Record not found for ID: {0}")]
NotFound(u32),
#[error("Type mismatch during deserialization")]
TypeError,
#[error("Transaction error: {0}")]
TransactionError(String),
#[error("OurDB error: {0}")]
OurDbError(#[from] ourdb::Error),
#[error("General database error: {0}")]
GeneralError(String),
}
/// Result type for DB operations
pub type DbResult<T> = Result<T, DbError>;

View File

@@ -0,0 +1,140 @@
use crate::db::error::{DbError, DbResult};
use crate::db::store::DbOperations;
use ourdb::{OurDB, OurDBConfig, OurDBSetArgs};
use serde::{Serialize, de::DeserializeOwned};
use std::marker::PhantomData;
use std::path::{Path, PathBuf};
use std::any::Any;
// Trait for getting ID from any serializable type
pub trait GetId {
fn get_id(&self) -> u32;
}
/// A store implementation for any serializable type using OurDB as the backend
pub struct GenericStore<T: Serialize + DeserializeOwned + GetId + Send + Sync + 'static> {
db: OurDB,
path: PathBuf,
prefix: String,
_phantom: PhantomData<T>,
}
impl<T: Serialize + DeserializeOwned + GetId + Send + Sync + 'static> GenericStore<T> {
/// Opens or creates an OurDB database at the specified path
pub fn open<P: AsRef<Path>>(path: P, prefix: &str) -> DbResult<Self> {
let path_buf = path.as_ref().to_path_buf();
let db_path = path_buf.join(prefix);
// Create directory if it doesn't exist
std::fs::create_dir_all(&db_path).map_err(DbError::IoError)?;
let config = OurDBConfig {
path: db_path.clone(),
incremental_mode: true, // Always use incremental mode for auto IDs
file_size: None, // Use default (500MB)
keysize: None, // Use default (4 bytes)
reset: None, // Don't reset existing database
};
let db = OurDB::new(config).map_err(DbError::OurDbError)?;
Ok(Self {
db,
path: db_path,
prefix: prefix.to_string(),
_phantom: PhantomData,
})
}
/// Serializes an item to bytes
fn serialize(item: &T) -> DbResult<Vec<u8>> {
bincode::serialize(item).map_err(DbError::SerializationError)
}
/// Deserializes bytes to an item
fn deserialize(data: &[u8]) -> DbResult<T> {
bincode::deserialize(data).map_err(DbError::SerializationError)
}
/// Gets the raw bytes for an item by ID
pub fn get_raw(&self, id: u32) -> DbResult<Vec<u8>> {
self.db.get(id).map_err(DbError::OurDbError)
}
/// Lists all raw items as bytes
pub fn list_raw(&self) -> DbResult<Vec<Vec<u8>>> {
let items = self.db.list().map_err(DbError::OurDbError)?;
Ok(items)
}
/// Get the prefix for this store
pub fn prefix(&self) -> &str {
&self.prefix
}
}
impl<T: Serialize + DeserializeOwned + GetId + Send + Sync + 'static> DbOperations for GenericStore<T> {
fn delete(&mut self, id: u32) -> DbResult<()> {
self.db.delete(id).map_err(DbError::OurDbError)
}
fn get(&mut self, id: u32) -> DbResult<Box<dyn Any>> {
let data = self.db.get(id).map_err(DbError::OurDbError)?;
let item = Self::deserialize(&data)?;
Ok(Box::new(item))
}
fn list(&self) -> DbResult<Box<dyn Any>> {
let items = self.db.list().map_err(DbError::OurDbError)?;
let mut result = Vec::with_capacity(items.len());
for data in items {
let item = Self::deserialize(&data)?;
result.push(item);
}
Ok(Box::new(result))
}
fn insert(&mut self, model: &dyn Any) -> DbResult<()> {
// Try to downcast to T
if let Some(item) = model.downcast_ref::<T>() {
let data = Self::serialize(item)?;
let id = item.get_id();
let args = OurDBSetArgs {
id: Some(id),
data,
};
self.db.set(args).map_err(DbError::OurDbError)
} else {
Err(DbError::TypeError)
}
}
fn insert_raw(&mut self, serialized: &[u8]) -> DbResult<()> {
// Deserialize to get the ID
let item = Self::deserialize(serialized)?;
let id = item.get_id();
let args = OurDBSetArgs {
id: Some(id),
data: serialized.to_vec(),
};
self.db.set(args).map_err(DbError::OurDbError)
}
fn get_history(&mut self, id: u32, depth: u8) -> DbResult<Vec<Box<dyn Any>>> {
let history = self.db.get_history(id, depth).map_err(DbError::OurDbError)?;
let mut result = Vec::with_capacity(history.len());
for data in history {
let item = Self::deserialize(&data)?;
result.push(Box::new(item));
}
Ok(result)
}
}

View File

@@ -0,0 +1,38 @@
//! Macros for implementing model methods
/// Macro to implement typed access methods on the DB struct for a given model
#[macro_export]
macro_rules! impl_model_methods {
($model:ty, $singular:ident, $plural:ident) => {
impl DB {
paste::paste! {
/// Insert a model instance into the database
pub fn [<insert_ $singular>](&mut self, item: $model) -> Result<(), Box<rhai::EvalAltResult>> {
Ok(self.set(&item).map_err(|e| {
rhai::EvalAltResult::ErrorSystem("could not insert $singular".to_string(), Box::new(e))
})?)
}
/// Get a model instance by its ID
pub fn [<get_ $singular>](&mut self, id: u32) -> DbResult<$model> {
self.get::<$model>(id)
}
/// Delete a model instance by its ID
pub fn [<delete_ $singular>](&mut self, id: u32) -> DbResult<()> {
self.delete::<$model>(id)
}
/// List all model instances
pub fn [<list_ $plural>](&mut self) -> DbResult<Vec<$model>> {
self.list::<$model>()
}
/// Get history of a model instance
pub fn [<get_ $singular _history>](&mut self, id: u32, depth: u8) -> DbResult<Vec<$model>> {
self.get_history::<$model>(id, depth)
}
}
}
};
}

View File

@@ -0,0 +1,33 @@
// Export the error module
pub mod error;
pub use error::{DbError, DbResult};
// Export the model module
pub mod model;
pub use model::{Model, Storable, IndexKey, GetId};
// Export the store module
pub mod store;
pub use store::{DbOperations, OurDbStore};
// Export the generic store module
pub mod generic_store;
pub use generic_store::GenericStore;
// Export the db module
pub mod db;
pub use db::{DB, DBBuilder, ModelRegistration, ModelRegistrar};
// Export the TST index module
pub mod tst_index;
pub use tst_index::TSTIndexManager;
// Export macros for model methods
pub mod macros;
// Export model-specific methods
pub mod model_methods;
// Tests
#[cfg(test)]
mod tests;

View File

@@ -0,0 +1,96 @@
use crate::db::error::{DbError, DbResult};
use serde::{Deserialize, Serialize, de::DeserializeOwned};
use std::fmt::Debug;
/// Trait for models that can be serialized and deserialized
pub trait Storable: Serialize + for<'de> Deserialize<'de> + Sized {
/// Serializes the instance using bincode
fn to_bytes(&self) -> DbResult<Vec<u8>> {
bincode::serialize(self).map_err(DbError::SerializationError)
}
/// Deserializes data from bytes into an instance
fn from_bytes(data: &[u8]) -> DbResult<Self> {
bincode::deserialize(data).map_err(DbError::SerializationError)
}
}
/// Represents an index key for a model
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct IndexKey {
/// The name of the index key
pub name: &'static str,
/// The value of the index key for a specific model instance
pub value: String,
}
/// Trait identifying a model suitable for the database
/// The 'static lifetime bound is required for type identification via Any
pub trait Model: Storable + Debug + Clone + Send + Sync + 'static {
/// Returns the unique ID for this model instance
fn get_id(&self) -> u32;
/// Returns a prefix used for this model type in the database
/// Helps to logically separate different model types
fn db_prefix() -> &'static str;
/// Returns a list of index keys for this model instance
/// These keys will be used to create additional indexes in the TST
/// The default implementation returns an empty vector
/// Override this method to provide custom indexes
fn db_keys(&self) -> Vec<IndexKey> {
Vec::new()
}
}
/// Trait for adapting any serializable struct to work with the database
/// This is a lighter-weight alternative to the Model trait
pub trait ModelAdapter {
/// Returns the unique ID for this model instance
fn get_id(&self) -> u32;
/// Returns a prefix used for this model type in the database
fn db_prefix() -> &'static str;
/// Returns a list of index keys for this model instance
fn db_keys(&self) -> Vec<IndexKey> {
Vec::new()
}
}
/// Trait for getting ID from any serializable type
pub trait GetId {
/// Returns the unique ID for this instance
fn get_id(&self) -> u32;
}
/// Macro to automatically implement GetId for any struct with an id field of type u32
#[macro_export]
macro_rules! impl_get_id {
($type:ty) => {
impl GetId for $type {
fn get_id(&self) -> u32 {
self.id
}
}
};
}
/// Helper functions for serializing and deserializing any type
pub mod serialization {
use super::*;
/// Serialize any serializable type to bytes
pub fn to_bytes<T: Serialize>(value: &T) -> DbResult<Vec<u8>> {
bincode::serialize(value).map_err(DbError::SerializationError)
}
/// Deserialize bytes to any deserializable type
pub fn from_bytes<T: DeserializeOwned>(data: &[u8]) -> DbResult<T> {
bincode::deserialize(data).map_err(DbError::SerializationError)
}
}
// Note: We don't provide a blanket implementation of Storable
// Each model type must implement Storable explicitly

View File

@@ -0,0 +1,78 @@
use crate::db::db::DB;
use crate::db::model::Model;
use crate::impl_model_methods;
use crate::DbResult; // Add DbResult import
use crate::models::biz::{Product, Sale, Currency, ExchangeRate, Service, Customer, Contract, Invoice};
use crate::models::gov::{
Company, Shareholder, Meeting, User, Vote, Resolution,
Committee
// ComplianceRequirement, ComplianceDocument, ComplianceAudit - These don't exist
};
use crate::models::circle::{Circle, Member, Name, Wallet}; // Remove Asset
// Implement model-specific methods for Product
impl_model_methods!(Product, product, products);
// Implement model-specific methods for Sale
impl_model_methods!(Sale, sale, sales);
// Implement model-specific methods for Currency
impl_model_methods!(Currency, currency, currencies);
// Implement model-specific methods for ExchangeRate
impl_model_methods!(ExchangeRate, exchange_rate, exchange_rates);
// Implement model-specific methods for Service
impl_model_methods!(Service, service, services);
// Implement model-specific methods for Customer
impl_model_methods!(Customer, customer, customers);
// Implement model-specific methods for Contract
impl_model_methods!(Contract, contract, contracts);
// Implement model-specific methods for Invoice
impl_model_methods!(Invoice, invoice, invoices);
// Implement model-specific methods for Company
impl_model_methods!(Company, company, companies);
// Implement model-specific methods for Shareholder
impl_model_methods!(Shareholder, shareholder, shareholders);
// Implement model-specific methods for Meeting
impl_model_methods!(Meeting, meeting, meetings);
// Implement model-specific methods for User
impl_model_methods!(User, user, users);
// Implement model-specific methods for Vote
impl_model_methods!(Vote, vote, votes);
// Implement model-specific methods for Resolution
impl_model_methods!(Resolution, resolution, resolutions);
// Implement model-specific methods for Committee
impl_model_methods!(Committee, committee, committees);
// These models don't exist, so comment them out
// // Implement model-specific methods for ComplianceRequirement
// impl_model_methods!(ComplianceRequirement, compliance_requirement, compliance_requirements);
// // Implement model-specific methods for ComplianceDocument
// impl_model_methods!(ComplianceDocument, compliance_document, compliance_documents);
// // Implement model-specific methods for ComplianceAudit
// impl_model_methods!(ComplianceAudit, compliance_audit, compliance_audits);
// Implement model-specific methods for Circle
impl_model_methods!(Circle, circle, circles);
// Implement model-specific methods for Member
impl_model_methods!(Member, member, members);
// Implement model-specific methods for Name
impl_model_methods!(Name, name, names);
// Implement model-specific methods for Wallet
impl_model_methods!(Wallet, wallet, wallets);

View File

@@ -0,0 +1,156 @@
use crate::db::error::{DbError, DbResult};
use crate::db::model::Model;
use ourdb::{OurDB, OurDBConfig, OurDBSetArgs};
use std::marker::PhantomData;
use std::path::{Path, PathBuf};
use std::any::Any;
/// Trait for type-erased database operations
pub trait DbOperations: Send + Sync {
fn delete(&mut self, id: u32) -> DbResult<()>;
fn get(&mut self, id: u32) -> DbResult<Box<dyn Any>>;
fn list(&self) -> DbResult<Box<dyn Any>>;
fn insert(&mut self, model: &dyn Any) -> DbResult<()>;
fn insert_raw(&mut self, serialized: &[u8]) -> DbResult<()>;
fn get_history(&mut self, id: u32, depth: u8) -> DbResult<Vec<Box<dyn Any>>>;
}
/// A store implementation using OurDB as the backend
pub struct OurDbStore<T: Model> {
db: OurDB,
path: PathBuf,
_phantom: PhantomData<T>,
}
impl<T: Model> OurDbStore<T> {
/// Opens or creates an OurDB database at the specified path
pub fn open<P: AsRef<Path>>(path: P) -> DbResult<Self> {
let path_buf = path.as_ref().to_path_buf();
let db_path = path_buf.join(T::db_prefix());
// Create directory if it doesn't exist
std::fs::create_dir_all(&db_path).map_err(DbError::IoError)?;
let config = OurDBConfig {
path: db_path.clone(),
incremental_mode: true, // Always use incremental mode for auto IDs
file_size: None, // Use default (500MB)
keysize: None, // Use default (4 bytes)
reset: None, // Don't reset existing database
};
let db = OurDB::new(config).map_err(DbError::OurDbError)?;
Ok(Self {
db,
path: db_path,
_phantom: PhantomData,
})
}
/// Inserts or updates a model instance in the database
pub fn insert(&mut self, model: &T) -> DbResult<()> {
// Use the new method name
let data = model.to_bytes()?;
// Don't pass the ID when using incremental mode
// OurDB will automatically assign an ID
self.db.set(OurDBSetArgs {
id: None,
data: &data,
}).map_err(DbError::OurDbError)?;
Ok(())
}
/// Retrieves a model instance by its ID
pub fn get(&mut self, id: u32) -> DbResult<T> {
let data = self.db.get(id).map_err(|e| {
match e {
ourdb::Error::NotFound(_) => DbError::NotFound(id),
_ => DbError::OurDbError(e),
}
})?;
// Use the new method name
T::from_bytes(&data)
}
/// Deletes a model instance by its ID
pub fn delete(&mut self, id: u32) -> DbResult<()> {
self.db.delete(id).map_err(|e| {
match e {
ourdb::Error::NotFound(_) => DbError::NotFound(id),
_ => DbError::OurDbError(e),
}
})
}
/// Lists all models of this type
pub fn list(&self) -> DbResult<Vec<T>> {
// OurDB doesn't have a built-in list function, so we need to implement it
// This is a placeholder - in a real implementation, we would need to
// maintain a list of all IDs for each model type
Err(DbError::GeneralError("List operation not implemented yet".to_string()))
}
/// Gets the history of a model by its ID
pub fn get_history(&mut self, id: u32, depth: u8) -> DbResult<Vec<T>> {
let history_data = self.db.get_history(id, depth).map_err(|e| {
match e {
ourdb::Error::NotFound(_) => DbError::NotFound(id),
_ => DbError::OurDbError(e),
}
})?;
let mut result = Vec::with_capacity(history_data.len());
for data in history_data {
result.push(T::from_bytes(&data)?);
}
Ok(result)
}
}
impl<T: Model> DbOperations for OurDbStore<T> {
fn delete(&mut self, id: u32) -> DbResult<()> {
self.delete(id)
}
fn get(&mut self, id: u32) -> DbResult<Box<dyn Any>> {
let result = self.get(id)?;
Ok(Box::new(result))
}
fn list(&self) -> DbResult<Box<dyn Any>> {
// This doesn't require &mut self
let result = self.list()?;
Ok(Box::new(result))
}
fn insert(&mut self, model: &dyn Any) -> DbResult<()> {
// Downcast the Any to T
if let Some(model_t) = model.downcast_ref::<T>() {
self.insert(model_t)
} else {
Err(DbError::TypeError)
}
}
fn insert_raw(&mut self, serialized: &[u8]) -> DbResult<()> {
// Deserialize the raw bytes to a model
let model = T::from_bytes(serialized)?;
self.insert(&model)
}
fn get_history(&mut self, id: u32, depth: u8) -> DbResult<Vec<Box<dyn Any>>> {
let history = self.get_history(id, depth)?;
let mut result = Vec::with_capacity(history.len());
for item in history {
result.push(Box::new(item) as Box<dyn Any>);
}
Ok(result)
}
}

View File

@@ -0,0 +1,98 @@
use super::*;
use crate::db::model::Storable;
use serde::{Deserialize, Serialize};
use tempfile::tempdir;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
struct TestModel {
id: u32,
name: String,
}
impl Storable for TestModel {}
impl Model for TestModel {
fn get_id(&self) -> u32 {
self.id
}
fn db_prefix() -> &'static str {
"test"
}
}
#[test]
fn test_tst_integration() {
// Create a temporary directory for the test
let temp_dir = tempdir().unwrap();
let path = temp_dir.path();
// Create a DB instance
let mut db = DB::new(path).unwrap();
db.register::<TestModel>().unwrap();
// Create some test models
let model1 = TestModel { id: 1, name: "Test 1".to_string() };
let model2 = TestModel { id: 2, name: "Test 2".to_string() };
let model3 = TestModel { id: 3, name: "Test 3".to_string() };
// Insert the models
db.set(&model1).unwrap();
db.set(&model2).unwrap();
db.set(&model3).unwrap();
// List all models
let models = db.list::<TestModel>().unwrap();
assert_eq!(models.len(), 3);
// Verify that all models are in the list
assert!(models.contains(&model1));
assert!(models.contains(&model2));
assert!(models.contains(&model3));
// Delete a model
db.delete::<TestModel>(2).unwrap();
// List again
let models = db.list::<TestModel>().unwrap();
assert_eq!(models.len(), 2);
assert!(models.contains(&model1));
assert!(models.contains(&model3));
assert!(!models.contains(&model2));
// Test transaction with commit
db.begin_transaction().unwrap();
db.set(&model2).unwrap(); // Add back model2
db.delete::<TestModel>(1).unwrap(); // Delete model1
db.commit_transaction().unwrap();
// List again after transaction
let models = db.list::<TestModel>().unwrap();
assert_eq!(models.len(), 2);
assert!(!models.contains(&model1));
assert!(models.contains(&model2));
assert!(models.contains(&model3));
// Test transaction with rollback
db.begin_transaction().unwrap();
db.delete::<TestModel>(3).unwrap(); // Delete model3
db.rollback_transaction().unwrap();
// List again after rollback
let models = db.list::<TestModel>().unwrap();
assert_eq!(models.len(), 2);
assert!(!models.contains(&model1));
assert!(models.contains(&model2));
assert!(models.contains(&model3));
// Test the synchronize_tst_index method
// Since we can't directly access private fields, we'll just verify that
// the method runs without errors
db.synchronize_tst_index::<TestModel>().unwrap();
// Verify that our models are still accessible
let models = db.list::<TestModel>().unwrap();
assert_eq!(models.len(), 2);
assert!(models.contains(&model2));
assert!(models.contains(&model3));
}

View File

@@ -0,0 +1,261 @@
use crate::db::error::{DbError, DbResult};
use crate::db::model::IndexKey;
use std::path::{Path, PathBuf};
use std::collections::HashMap;
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: 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: 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 with primary key
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 primary key in the format prefix_id
let key = format!("{}_{}", prefix, id);
// Set the key-value pair in the TST
tst.set(&key, data.clone())
.map_err(|e| DbError::GeneralError(format!("TST error: {:?}", e)))?;
Ok(())
}
/// Adds or updates an object in the TST index with additional index keys
pub fn set_with_indexes(&mut self, prefix: &str, id: u32, data: Vec<u8>, index_keys: &[IndexKey]) -> DbResult<()> {
// First set the primary key
self.set(prefix, id, data.clone())?;
// Get the TST for this prefix
let tst = self.get_tst(prefix)?;
// Add additional index keys
for index_key in index_keys {
// Create the index key in the format prefix_indexname_value
let key = format!("{}_{}_{}", prefix, index_key.name, index_key.value);
// Set the key-value pair in the TST
// For index keys, we store the ID as the value
let id_bytes = id.to_be_bytes().to_vec();
tst.set(&key, id_bytes)
.map_err(|e| DbError::GeneralError(format!("TST error: {:?}", e)))?;
}
Ok(())
}
/// Removes an object from the TST index (primary key only)
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(())
}
/// Removes an object from the TST index including all index keys
pub fn delete_with_indexes(&mut self, prefix: &str, id: u32, index_keys: &[IndexKey]) -> DbResult<()> {
// First delete the primary key
self.delete(prefix, id)?;
// Get the TST for this prefix
let tst = self.get_tst(prefix)?;
// Delete additional index keys
for index_key in index_keys {
// Create the index key in the format prefix_indexname_value
let key = format!("{}_{}_{}", prefix, index_key.name, index_key.value);
// 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 (primary keys only)
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 followed by an underscore
let search_prefix = format!("{}_", prefix);
let keys = tst.list(&search_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 {
// Check if this is a primary key (prefix_id) and not an index key (prefix_indexname_value)
let parts: Vec<&str> = key.split('_').collect();
if parts.len() != 2 {
continue; // Skip index keys
}
// Extract the ID from the key (format: prefix_id)
let id_str = parts[1];
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)
}
/// Finds objects by a specific index key
pub fn find_by_index(&mut self, prefix: &str, index_name: &str, index_value: &str) -> DbResult<Vec<u32>> {
// Get the TST for this prefix
let tst = self.get_tst(prefix)?;
// Create the index key in the format prefix_indexname_value
let key = format!("{}_{}_{}", prefix, index_name, index_value);
// Try to get the value from the TST
match tst.get(&key) {
Ok(id_bytes) => {
// Convert the bytes to a u32 ID
if id_bytes.len() == 4 {
let mut bytes = [0u8; 4];
bytes.copy_from_slice(&id_bytes[0..4]);
let id = u32::from_be_bytes(bytes);
Ok(vec![id])
} else {
Err(DbError::GeneralError(format!("Invalid ID bytes for key: {}", key)))
}
},
Err(_) => Ok(Vec::new()), // No matches found
}
}
/// Finds objects by a prefix of an index key
pub fn find_by_index_prefix(&mut self, prefix: &str, index_name: &str, index_value_prefix: &str) -> DbResult<Vec<u32>> {
// Get the TST for this prefix
let tst = self.get_tst(prefix)?;
// Create the index key prefix in the format prefix_indexname_valueprefix
let key_prefix = format!("{}_{}_{}", prefix, index_name, index_value_prefix);
// Get all keys with this prefix
let keys = tst.list(&key_prefix)
.map_err(|e| DbError::GeneralError(format!("TST error: {:?}", e)))?;
// Extract the IDs from the values
let mut result = Vec::with_capacity(keys.len());
for key in keys {
// Get the value from the TST
let id_bytes = tst.get(&key)
.map_err(|e| DbError::GeneralError(format!("TST error: {:?}", e)))?;
// Convert the bytes to a u32 ID
if id_bytes.len() == 4 {
let mut bytes = [0u8; 4];
bytes.copy_from_slice(&id_bytes[0..4]);
let id = u32::from_be_bytes(bytes);
result.push(id);
}
}
Ok(result)
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
#[test]
fn test_tst_index_manager() {
// Create a temporary directory for the test
let temp_dir = tempdir().unwrap();
let path = temp_dir.path();
// Create a TST index manager
let mut manager = TSTIndexManager::new(path).unwrap();
// Test setting values
let data1 = vec![1, 2, 3];
let data2 = vec![4, 5, 6];
manager.set("test", 1, data1.clone()).unwrap();
manager.set("test", 2, data2.clone()).unwrap();
// Test listing values
let items = manager.list("test").unwrap();
assert_eq!(items.len(), 2);
// Check that the values are correct
let mut found_data1 = false;
let mut found_data2 = false;
for (id, data) in items {
if id == 1 && data == data1 {
found_data1 = true;
} else if id == 2 && data == data2 {
found_data2 = true;
}
}
assert!(found_data1);
assert!(found_data2);
// Test deleting a value
manager.delete("test", 1).unwrap();
// Test listing again
let items = manager.list("test").unwrap();
assert_eq!(items.len(), 1);
assert_eq!(items[0].0, 2);
assert_eq!(items[0].1, data2);
}
}

View File

@@ -0,0 +1,23 @@
use thiserror::Error;
/// Error type for HeroDB operations
#[derive(Error, Debug)]
pub enum Error {
#[error("Database error: {0}")]
DbError(#[from] crate::db::error::DbError),
#[error("I/O error: {0}")]
IoError(#[from] std::io::Error),
#[error("Serialization error: {0}")]
SerializationError(#[from] bincode::Error),
#[error("OurDB error: {0}")]
OurDbError(#[from] ourdb::Error),
#[error("General error: {0}")]
GeneralError(String),
}
/// Result type for HeroDB operations
pub type Result<T> = std::result::Result<T, Error>;

View File

@@ -0,0 +1,143 @@
# HeroDB: ACL Layer Implementation
## Project Overview
Create a new module that implements an Access Control List (ACL) layer on top of the existing `ourdb` and `tst` databases. This module will manage permissions and access control for data stored in the database system.
call this module: acldb
implement in acldb
remark: there is no dependency on herodb
## Architecture
- The module will sit as a layer between client applications and the underlying `ourdb` & `tst` databases
- ACLs are defined at the circle level and stored in a special topic called "acl"
- Data in `ourdb` is stored at path: `~/hero/var/ourdb/$circleid/$topicid`
- `tst` is used to create mappings between keys and IDs in `ourdb`
## ACL Structure
Each ACL contains:
- A unique name (per circle)
- A list of public keys with associated permissions
- Rights are hierarchical: read → write → delete → execute → admin (each right includes all rights to its left)
## Core Methods
### ACL Management
#### aclupdate
Updates or creates an ACL with specified permissions.
**Parameters:**
- `callerpubkey`: Public key of the requesting user
- `circleid`: ID of the circle where the ACL exists
- `name`: Unique name for the ACL within the circle
- `pubkeys`: Array of public keys to grant permissions to
- `right`: Permission level (enum: read/write/delete/execute/admin)
#### aclremove
Removes specific public keys from an existing ACL.
**Parameters:**
- `callerpubkey`: Public key of the requesting user
- `circleid`: ID of the circle where the ACL exists
- `name`: Name of the ACL to modify
- `pubkeys`: Array of public keys to remove from the ACL
#### acldel
Deletes an entire ACL.
**Parameters:**
- `callerpubkey`: Public key of the requesting user
- `circleid`: ID of the circle where the ACL exists
- `name`: Name of the ACL to delete
### Data Operations
#### set
Stores or updates data in the database with optional ACL protection.
**Parameters:**
- `callerpubkey`: Public key of the requesting user
- `circleid`: ID of the circle where the data belongs
- `topic`: String identifier for the database category (e.g., "customer", "product")
- `key`: Optional string key for the record
- `id`: Optional numeric ID for direct access
- `value`: Binary blob of data to store
- `aclid`: ID of the ACL to protect this record (0 for public access)
**Behavior:**
- If only `key` is provided, use `tst` to map the key to a new or existing ID
- If `id` is specified or derived from an existing key, update the corresponding record
- Returns the ID of the created/updated record
#### del
Marks a record as deleted.
**Parameters:**
- `callerpubkey`: Public key of the requesting user
- `circleid`: ID of the circle where the data belongs
- `topic`: String identifier for the database category
- `id` or `key`: Identifier for the record to delete
**Behavior:**
- Deletes the mapping in `tst` if a key was used
- Marks the record as deleted in `ourdb` (not physically removed)
#### get
Retrieves data from the database.
**Parameters:**
- `callerpubkey`: Public key of the requesting user
- `circleid`: ID of the circle where the data belongs
- `topic`: String identifier for the database category
- `id` or `key`: Identifier for the record to retrieve
**Returns:**
- The binary data stored in the record if the caller has access
## Implementation Details
### ACL Storage Format
- ACLs are stored in a special topic named "acl" within each circle
- Each ACL has a unique numeric ID within the circle
### Record ACL Protection
- When a record uses ACL protection, the first 4 bytes of the stored data contain the ACL ID
- A new constructor in `ourdb` should be created to handle ACL-protected records
- Records with ACL ID of 0 are accessible to everyone
## RPC Interface
The module should expose its functionality through an RPC interface:
1. Client sends:
- Method name (e.g., "del", "set", "get")
- JSON-encoded arguments
- Cryptographic signature of the JSON data
2. Server:
- Verifies the signature is valid
- Extracts the caller's public key from the signature
- Checks permissions against applicable ACLs
- Executes the requested operation if authorized
- Returns appropriate response
## Security Considerations
- All operations must validate the caller has appropriate permissions
- ACL changes should be logged for audit purposes
- Consider implementing rate limiting to prevent abuse
## THE SERVER
- create actix webserver
- make a router that handles the rpc interface
- use openapi spec
- embed swagger interface
- implement a queuing mechanism, so internal we don't have to implement locks, but we do 1 request after the other per circle, so we know we never have conflicting changes in 1 circle
- create a logger which gives us good overview of what happened when

View File

@@ -0,0 +1,19 @@
//! HeroDB: A database library built on top of ourdb with model support
//!
//! This library provides a simple interface for working with an ourdb-based database
//! and includes support for defining and working with data models.
// Core modules
pub mod db;
pub mod error;
pub mod models;
// Temporarily commented out due to compilation errors
// pub mod rhaiengine;
pub mod cmd;
// Re-exports
pub use error::Error;
pub use db::{DB, DBBuilder, Model, Storable, DbError, DbResult, GetId};
/// Re-export ourdb for advanced usage
pub use ourdb;

View File

@@ -0,0 +1,6 @@
// Export core module
pub mod core;
// Export zaz module
pub mod zaz;

View File

@@ -0,0 +1,530 @@
# Business Models
This directory contains the core business models used throughout the application for representing essential business objects like products, sales, and currency.
```
┌─────────────┐
│ Customer │
└──────┬──────┘
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Currency │◄────┤ Product │◄────┤ │ │ │
└─────────────┘ └─────────────┘ │ │ │ │
▲ │ SaleItem │◄────┤ Sale │
│ │ │ │ │
┌─────┴──────────┐ │ │ │ │
│ProductComponent│ └─────────────┘ └──────┬──────┘
└────────────────┘ ▲ │
/ │
┌─────────────┐ ┌─────────────┐ / │
│ Currency │◄────┤ Service │◄────────/ │
└─────────────┘ └─────────────┘ │
┌─────────────┐ ┌─────────────┐
│ InvoiceItem │◄────┤ Invoice │
└─────────────┘ └─────────────┘
```
## Business Logic Relationships
- **Customer**: The entity purchasing products or services
- **Product/Service**: Defines what is being sold, including its base price
- Can be marked as a template (`is_template=true`) to create copies for actual sales
- **Sale**: Represents the transaction of selling products/services to customers, including tax calculations
- Contains SaleItems that can be linked to either Products or Services
- **SaleItem**: Represents an item within a sale
- Can be linked to either a Product or a Service (via product_id or service_id)
- **Service**: Represents an ongoing service provided to a customer
- Created from a Product template when the product type is Service
- **Invoice**: Represents the billing document for a sale, with payment tracking
- Created from a Sale object to handle billing and payment tracking
## Root Objects
- Root objects are the ones stored directly in the DB
- Root Objects are:
- Customer
- Currency
- Product
- Sale
- Service
- Invoice
## Models
### Currency (Root Object)
Represents a monetary value with an amount and currency code.
**Properties:**
- `amount`: f64 - The monetary amount
- `currency_code`: String - The currency code (e.g., "USD", "EUR")
**Builder:**
- `CurrencyBuilder` - Provides a fluent interface for creating Currency instances
### Customer (Root Object)
Represents a customer who can purchase products or services.
**Properties:**
- `id`: u32 - Unique identifier
- `name`: String - Customer name
- `description`: String - Customer description
- `pubkey`: String - Customer's public key
- `contact_ids`: Vec<u32> - List of contact IDs
- `created_at`: DateTime<Utc> - Creation timestamp
- `updated_at`: DateTime<Utc> - Last update timestamp
**Methods:**
- `add_contact()` - Adds a contact ID to the customer
- `remove_contact()` - Removes a contact ID from the customer
### Product
#### ProductType Enum
Categorizes products:
- `Product` - Physical product
- `Service` - Non-physical service
#### ProductStatus Enum
Tracks product availability:
- `Available` - Product can be purchased
- `Unavailable` - Product cannot be purchased
#### ProductComponent
Represents a component part of a product.
**Properties:**
- `id`: u32 - Unique identifier
- `name`: String - Component name
- `description`: String - Component description
- `quantity`: i32 - Number of this component
- `created_at`: DateTime<Utc> - Creation timestamp
- `updated_at`: DateTime<Utc> - Last update timestamp
**Builder:**
- `ProductComponentBuilder` - Provides a fluent interface for creating ProductComponent instances
#### Product (Root Object)
Represents a product or service offered.
**Properties:**
- `id`: i64 - Unique identifier
- `name`: String - Product name
- `description`: String - Product description
- `price`: Currency - Product price
- `type_`: ProductType - Product or Service
- `category`: String - Product category
- `status`: ProductStatus - Available or Unavailable
- `created_at`: DateTime<Utc> - Creation timestamp
- `updated_at`: DateTime<Utc> - Last update timestamp
- `max_amount`: i64 - Maximum quantity available
- `purchase_till`: DateTime<Utc> - Deadline for purchasing
- `active_till`: DateTime<Utc> - When product/service expires
- `components`: Vec<ProductComponent> - List of product components
- `is_template`: bool - Whether this is a template product (to be added)
**Methods:**
- `add_component()` - Adds a component to this product
- `set_purchase_period()` - Updates purchase availability timeframe
- `set_active_period()` - Updates active timeframe
- `is_purchasable()` - Checks if product is available for purchase
- `is_active()` - Checks if product is still active
**Builder:**
- `ProductBuilder` - Provides a fluent interface for creating Product instances
**Database Implementation:**
- Implements `Storable` trait for serialization
- Implements `SledModel` trait with:
- `get_id()` - Returns the ID as a string
- `db_prefix()` - Returns "product" as the database prefix
### Service (Root Object)
#### BillingFrequency Enum
Defines how often a service is billed:
- `Hourly` - Billed by the hour
- `Daily` - Billed daily
- `Weekly` - Billed weekly
- `Monthly` - Billed monthly
- `Yearly` - Billed yearly
#### ServiceStatus Enum
Tracks the status of a service:
- `Active` - Service is currently active
- `Paused` - Service is temporarily paused
- `Cancelled` - Service has been cancelled
- `Completed` - Service has been completed
#### ServiceItem
Represents an item within a service.
**Properties:**
- `id`: u32 - Unique identifier
- `service_id`: u32 - Parent service ID
- `product_id`: u32 - ID of the product this service is based on
- `name`: String - Service name
- `description`: String - Detailed description of the service item
- `comments`: String - Additional notes or comments about the service item
- `quantity`: i32 - Number of units
- `unit_price`: Currency - Price per unit
- `subtotal`: Currency - Total price before tax
- `tax_rate`: f64 - Tax rate as a percentage
- `tax_amount`: Currency - Calculated tax amount
- `is_taxable`: bool - Whether this item is taxable
- `active_till`: DateTime<Utc> - When service expires
#### Service
Represents an ongoing service provided to a customer.
**Properties:**
- `id`: u32 - Unique identifier
- `customer_id`: u32 - ID of the customer receiving the service
- `total_amount`: Currency - Total service amount including tax
- `status`: ServiceStatus - Current service status
- `billing_frequency`: BillingFrequency - How often the service is billed
- `service_date`: DateTime<Utc> - When service started
- `created_at`: DateTime<Utc> - Creation timestamp
- `updated_at`: DateTime<Utc> - Last update timestamp
- `items`: Vec<ServiceItem> - List of items in the service
- `is_template`: bool - Whether this is a template service (to be added)
**Methods:**
- `add_item()` - Adds an item to the service and updates total
- `calculate_total()` - Recalculates the total amount
- `update_status()` - Updates the status of the service
### Sale
#### SaleStatus Enum
Tracks the status of a sale:
- `Pending` - Sale is in progress
- `Completed` - Sale has been finalized
- `Cancelled` - Sale has been cancelled
#### SaleItem
Represents an item within a sale.
**Properties:**
- `id`: u32 - Unique identifier
- `sale_id`: u32 - Parent sale ID
- `product_id`: Option<u32> - ID of the product sold (if this is a product sale)
- `service_id`: Option<u32> - ID of the service sold (if this is a service sale)
- `name`: String - Product/service name at time of sale
- `description`: String - Detailed description of the item
- `comments`: String - Additional notes or comments about the item
- `quantity`: i32 - Number of items purchased
- `unit_price`: Currency - Price per unit
- `subtotal`: Currency - Total price for this item before tax (calculated)
- `tax_rate`: f64 - Tax rate as a percentage (e.g., 20.0 for 20%)
- `tax_amount`: Currency - Calculated tax amount for this item
- `active_till`: DateTime<Utc> - When item/service expires
**Methods:**
- `total_with_tax()` - Returns the total amount including tax
**Builder:**
- `SaleItemBuilder` - Provides a fluent interface for creating SaleItem instances
#### Sale (Root Object)
Represents a complete sale transaction.
**Properties:**
- `id`: u32 - Unique identifier
- `company_id`: u32 - ID of the company making the sale
- `customer_id`: u32 - ID of the customer making the purchase (to be added)
- `buyer_name`: String - Name of the buyer
- `buyer_email`: String - Email of the buyer
- `subtotal_amount`: Currency - Total sale amount before tax
- `tax_amount`: Currency - Total tax amount for the sale
- `total_amount`: Currency - Total sale amount including tax
- `status`: SaleStatus - Current sale status
- `service_id`: Option<u32> - ID of the service created from this sale (to be added)
- `sale_date`: DateTime<Utc> - When sale occurred
- `created_at`: DateTime<Utc> - Creation timestamp
- `updated_at`: DateTime<Utc> - Last update timestamp
- `items`: Vec<SaleItem> - List of items in the sale
**Methods:**
- `add_item()` - Adds an item to the sale and updates totals
- `update_status()` - Updates the status of the sale
- `recalculate_totals()` - Recalculates all totals based on items
- `create_service()` - Creates a service from this sale (to be added)
**Builder:**
- `SaleBuilder` - Provides a fluent interface for creating Sale instances
**Database Implementation:**
- Implements `Storable` trait for serialization
- Implements `SledModel` trait with:
- `get_id()` - Returns the ID as a string
- `db_prefix()` - Returns "sale" as the database prefix
## Usage Examples
### Creating a Currency
```rust
let price = CurrencyBuilder::new()
.amount(29.99)
.currency_code("USD")
.build()
.expect("Failed to build currency");
```
### Creating a Product
```rust
// Create a currency using the builder
let price = CurrencyBuilder::new()
.amount(29.99)
.currency_code("USD")
.build()
.expect("Failed to build currency");
// Create a component using the builder
let component = ProductComponentBuilder::new()
.id(1)
.name("Basic Support")
.description("24/7 email support")
.quantity(1)
.build()
.expect("Failed to build product component");
// Create a product using the builder
let product = ProductBuilder::new()
.id(1)
.name("Premium Service")
.description("Our premium service offering")
.price(price)
.type_(ProductType::Service)
.category("Services")
.status(ProductStatus::Available)
.max_amount(100)
.validity_days(30)
.add_component(component)
.build()
.expect("Failed to build product");
```
### Creating a Sale
```rust
let now = Utc::now();
// Create a currency using the builder
let unit_price = CurrencyBuilder::new()
.amount(29.99)
.currency_code("USD")
.build()
.expect("Failed to build currency");
// Create a sale item using the builder
let item = SaleItemBuilder::new()
.id(1)
.sale_id(1)
.product_id(1)
.name("Premium Service")
.quantity(1)
.unit_price(unit_price)
.tax_rate(20.0) // 20% tax rate
.active_till(now + Duration::days(30))
.build()
.expect("Failed to build sale item");
// Create a sale using the builder
let mut sale = SaleBuilder::new()
.id(1)
.company_id(101)
.buyer_name("John Doe")
.buyer_email("john.doe@example.com")
.currency_code("USD")
.status(SaleStatus::Pending)
.add_item(item)
.build()
.expect("Failed to build sale");
// Update the sale status
sale.update_status(SaleStatus::Completed);
// The sale now contains:
// - subtotal_amount: The sum of all items before tax
// - tax_amount: The sum of all tax amounts
// - total_amount: The total including tax
```
### Relationship Between Sale and Invoice
The Sale model represents what is sold to a customer (products or services), including tax calculations. The Invoice model represents the billing document for that sale.
An InvoiceItem can be linked to a Sale via the `sale_id` field, establishing a connection between what was sold and how it's billed.
```rust
// Create an invoice item linked to a sale
let invoice_item = InvoiceItemBuilder::new()
.id(1)
.invoice_id(1)
.description("Premium Service")
.amount(sale.total_amount.clone()) // Use the total amount from the sale
.sale_id(sale.id) // Link to the sale
.build()
.expect("Failed to build invoice item");
```
## Database Operations
The library provides model-specific convenience methods for common database operations:
```rust
// Insert a product
db.insert_product(&product).expect("Failed to insert product");
// Retrieve a product by ID
let retrieved_product = db.get_product(1).expect("Failed to retrieve product");
// List all products
let all_products = db.list_products().expect("Failed to list products");
// Delete a product
db.delete_product(1).expect("Failed to delete product");
```
These methods are available for all root objects:
- `insert_product`, `get_product`, `delete_product`, `list_products` for Product
- `insert_currency`, `get_currency`, `delete_currency`, `list_currencies` for Currency
- `insert_sale`, `get_sale`, `delete_sale`, `list_sales` for Sale
- `insert_service`, `get_service`, `delete_service`, `list_services` for Service
- `insert_invoice`, `get_invoice`, `delete_invoice`, `list_invoices` for Invoice
- `insert_customer`, `get_customer`, `delete_customer`, `list_customers` for Customer
### Invoice (Root Object)
#### InvoiceStatus Enum
Tracks the status of an invoice:
- `Draft` - Invoice is in draft state
- `Sent` - Invoice has been sent to the customer
- `Paid` - Invoice has been paid
- `Overdue` - Invoice is past due date
- `Cancelled` - Invoice has been cancelled
#### PaymentStatus Enum
Tracks the payment status of an invoice:
- `Unpaid` - Invoice has not been paid
- `PartiallyPaid` - Invoice has been partially paid
- `Paid` - Invoice has been fully paid
#### Payment
Represents a payment made against an invoice.
**Properties:**
- `amount`: Currency - Payment amount
- `date`: DateTime<Utc> - Payment date
- `method`: String - Payment method
- `comment`: String - Payment comment
#### InvoiceItem
Represents an item in an invoice.
**Properties:**
- `id`: u32 - Unique identifier
- `invoice_id`: u32 - Parent invoice ID
- `description`: String - Item description
- `amount`: Currency - Item amount
- `service_id`: Option<u32> - ID of the service this item is for
- `sale_id`: Option<u32> - ID of the sale this item is for
**Methods:**
- `link_to_service()` - Links the invoice item to a service
- `link_to_sale()` - Links the invoice item to a sale
#### Invoice
Represents an invoice sent to a customer.
**Properties:**
- `id`: u32 - Unique identifier
- `customer_id`: u32 - ID of the customer being invoiced
- `total_amount`: Currency - Total invoice amount
- `balance_due`: Currency - Amount still due
- `status`: InvoiceStatus - Current invoice status
- `payment_status`: PaymentStatus - Current payment status
- `issue_date`: DateTime<Utc> - When invoice was issued
- `due_date`: DateTime<Utc> - When payment is due
- `created_at`: DateTime<Utc> - Creation timestamp
- `updated_at`: DateTime<Utc> - Last update timestamp
- `items`: Vec<InvoiceItem> - List of items in the invoice
- `payments`: Vec<Payment> - List of payments made
**Methods:**
- `add_item()` - Adds an item to the invoice
- `calculate_total()` - Calculates the total amount
- `add_payment()` - Adds a payment to the invoice
- `calculate_balance()` - Calculates the balance due
- `update_payment_status()` - Updates the payment status
- `update_status()` - Updates the status of the invoice
- `is_overdue()` - Checks if the invoice is overdue
- `check_if_overdue()` - Marks the invoice as overdue if past due date
### Relationships Between Models
#### Product/Service Templates and Instances
Products and Services can be marked as templates (`is_template=true`). When a customer purchases a product or service, a copy is created from the template with the specific details of what was sold.
#### Sale to Service Relationship
A SaleItem can be directly linked to a Service via the `service_id` field. This allows for selling existing services or creating new services as part of a sale:
```rust
// Create a SaleItem linked to a service
let sale_item = SaleItemBuilder::new()
.id(1)
.sale_id(1)
.service_id(Some(42)) // Link to service with ID 42
.product_id(None) // No product link since this is a service
.name("Premium Support")
.quantity(1)
.unit_price(unit_price)
.tax_rate(20.0)
.active_till(now + Duration::days(30))
.build()
.expect("Failed to build sale item");
```
#### Sale to Invoice Relationship
An Invoice is created from a Sale to handle billing and payment tracking:
```rust
// Create an invoice from a sale
let invoice = Invoice::from_sale(
invoice_id,
sale,
due_date
);
```
#### Customer-Centric View
The models allow tracking all customer interactions:
- What products/services they've purchased (via Sale records)
- What ongoing services they have (via Service records)
- What they've been invoiced for (via Invoice records)
- What they've paid (via Payment records in Invoices)
```rust
// Get all sales for a customer
let customer_sales = db.list_sales_by_customer(customer_id);
// Get all services for a customer
let customer_services = db.list_services_by_customer(customer_id);
// Get all invoices for a customer
let customer_invoices = db.list_invoices_by_customer(customer_id);
```

View File

@@ -0,0 +1,293 @@
use crate::db::{Model, Storable, IndexKey}; // Import Model trait and IndexKey from db module
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
/// ContractStatus represents the status of a contract
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ContractStatus {
Active,
Expired,
Terminated,
}
/// Contract represents a legal agreement between a customer and the business
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Contract {
pub id: u32,
pub customer_id: u32,
pub service_id: Option<u32>,
pub sale_id: Option<u32>,
pub terms: String,
pub start_date: DateTime<Utc>,
pub end_date: DateTime<Utc>,
pub auto_renewal: bool,
pub renewal_terms: String,
pub status: ContractStatus,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
impl Contract {
/// Create a new contract with default timestamps
pub fn new(
id: u32,
customer_id: u32,
terms: String,
start_date: DateTime<Utc>,
end_date: DateTime<Utc>,
auto_renewal: bool,
renewal_terms: String,
) -> Self {
let now = Utc::now();
Self {
id,
customer_id,
service_id: None,
sale_id: None,
terms,
start_date,
end_date,
auto_renewal,
renewal_terms,
status: ContractStatus::Active,
created_at: now,
updated_at: now,
}
}
/// Link the contract to a service
pub fn link_to_service(&mut self, service_id: u32) {
self.service_id = Some(service_id);
self.sale_id = None; // A contract can only be linked to either a service or a sale
self.updated_at = Utc::now();
}
/// Link the contract to a sale
pub fn link_to_sale(&mut self, sale_id: u32) {
self.sale_id = Some(sale_id);
self.service_id = None; // A contract can only be linked to either a service or a sale
self.updated_at = Utc::now();
}
/// Check if the contract is currently active
pub fn is_active(&self) -> bool {
let now = Utc::now();
self.status == ContractStatus::Active &&
now >= self.start_date &&
now <= self.end_date
}
/// Check if the contract has expired
pub fn is_expired(&self) -> bool {
let now = Utc::now();
now > self.end_date
}
/// Update the contract status
pub fn update_status(&mut self, status: ContractStatus) {
self.status = status;
self.updated_at = Utc::now();
}
/// Renew the contract based on renewal terms
pub fn renew(&mut self) -> Result<(), &'static str> {
if !self.auto_renewal {
return Err("Contract is not set for auto-renewal");
}
if self.status != ContractStatus::Active {
return Err("Cannot renew a non-active contract");
}
// Calculate new dates based on the current end date
let duration = self.end_date - self.start_date;
self.start_date = self.end_date;
self.end_date = self.end_date + duration;
self.updated_at = Utc::now();
Ok(())
}
}
/// Builder for Contract
pub struct ContractBuilder {
id: Option<u32>,
customer_id: Option<u32>,
service_id: Option<u32>,
sale_id: Option<u32>,
terms: Option<String>,
start_date: Option<DateTime<Utc>>,
end_date: Option<DateTime<Utc>>,
auto_renewal: Option<bool>,
renewal_terms: Option<String>,
status: Option<ContractStatus>,
created_at: Option<DateTime<Utc>>,
updated_at: Option<DateTime<Utc>>,
}
impl ContractBuilder {
/// Create a new ContractBuilder with all fields set to None
pub fn new() -> Self {
Self {
id: None,
customer_id: None,
service_id: None,
sale_id: None,
terms: None,
start_date: None,
end_date: None,
auto_renewal: None,
renewal_terms: None,
status: None,
created_at: None,
updated_at: None,
}
}
/// Set the id
pub fn id(mut self, id: u32) -> Self {
self.id = Some(id);
self
}
/// Set the customer_id
pub fn customer_id(mut self, customer_id: u32) -> Self {
self.customer_id = Some(customer_id);
self
}
/// Set the service_id
pub fn service_id(mut self, service_id: u32) -> Self {
self.service_id = Some(service_id);
self.sale_id = None; // A contract can only be linked to either a service or a sale
self
}
/// Set the sale_id
pub fn sale_id(mut self, sale_id: u32) -> Self {
self.sale_id = Some(sale_id);
self.service_id = None; // A contract can only be linked to either a service or a sale
self
}
/// Set the terms
pub fn terms<S: Into<String>>(mut self, terms: S) -> Self {
self.terms = Some(terms.into());
self
}
/// Set the start_date
pub fn start_date(mut self, start_date: DateTime<Utc>) -> Self {
self.start_date = Some(start_date);
self
}
/// Set the end_date
pub fn end_date(mut self, end_date: DateTime<Utc>) -> Self {
self.end_date = Some(end_date);
self
}
/// Set auto_renewal
pub fn auto_renewal(mut self, auto_renewal: bool) -> Self {
self.auto_renewal = Some(auto_renewal);
self
}
/// Set the renewal_terms
pub fn renewal_terms<S: Into<String>>(mut self, renewal_terms: S) -> Self {
self.renewal_terms = Some(renewal_terms.into());
self
}
/// Set the status
pub fn status(mut self, status: ContractStatus) -> Self {
self.status = Some(status);
self
}
/// Build the Contract object
pub fn build(self) -> Result<Contract, &'static str> {
let now = Utc::now();
// Validate that start_date is before end_date
let start_date = self.start_date.ok_or("start_date is required")?;
let end_date = self.end_date.ok_or("end_date is required")?;
if start_date >= end_date {
return Err("start_date must be before end_date");
}
Ok(Contract {
id: self.id.ok_or("id is required")?,
customer_id: self.customer_id.ok_or("customer_id is required")?,
service_id: self.service_id,
sale_id: self.sale_id,
terms: self.terms.ok_or("terms is required")?,
start_date,
end_date,
auto_renewal: self.auto_renewal.unwrap_or(false),
renewal_terms: self.renewal_terms.ok_or("renewal_terms is required")?,
status: self.status.unwrap_or(ContractStatus::Active),
created_at: self.created_at.unwrap_or(now),
updated_at: self.updated_at.unwrap_or(now),
})
}
}
impl Storable for Contract {}
// Implement Model trait
impl Model for Contract {
fn get_id(&self) -> u32 {
self.id
}
fn db_prefix() -> &'static str {
"contract"
}
fn db_keys(&self) -> Vec<IndexKey> {
let mut keys = Vec::new();
// Add an index for customer_id
keys.push(IndexKey {
name: "customer_id",
value: self.customer_id.to_string(),
});
// Add an index for service_id if present
if let Some(service_id) = self.service_id {
keys.push(IndexKey {
name: "service_id",
value: service_id.to_string(),
});
}
// Add an index for sale_id if present
if let Some(sale_id) = self.sale_id {
keys.push(IndexKey {
name: "sale_id",
value: sale_id.to_string(),
});
}
// Add an index for status
keys.push(IndexKey {
name: "status",
value: format!("{:?}", self.status),
});
// Add an index for active contracts
if self.is_active() {
keys.push(IndexKey {
name: "active",
value: "true".to_string(),
});
}
keys
}
}

View File

@@ -0,0 +1,114 @@
use crate::db::model::{Model, IndexKey};
use crate::db::{Storable, DbError, DbResult};
use chrono::{DateTime, Duration, Utc};
use rhai::{CustomType, EvalAltResult, TypeBuilder};
use serde::{Deserialize, Serialize};
/// Currency represents a monetary value with amount and currency code
#[derive(Debug, Clone, Serialize, Deserialize, CustomType)]
pub struct Currency {
pub id: u32,
pub amount: f64,
pub currency_code: String,
}
impl Currency {
/// Create a new currency with amount and code
pub fn new(id: u32, amount: f64, currency_code: String) -> Self {
Self {
id,
amount,
currency_code,
}
}
pub fn amount(&mut self) -> f64 {
self.amount
}
}
/// Builder for Currency
#[derive(Clone, CustomType)]
pub struct CurrencyBuilder {
id: Option<u32>,
amount: Option<f64>,
currency_code: Option<String>,
}
impl CurrencyBuilder {
/// Create a new CurrencyBuilder with all fields set to None
pub fn new() -> Self {
Self {
id: None,
amount: None,
currency_code: None,
}
}
/// Set the id
pub fn id(mut self, id: u32) -> Self {
self.id = Some(id);
self
}
/// Set the amount
pub fn amount(mut self, amount: f64) -> Self {
self.amount = Some(amount);
self
}
/// Set the currency code
pub fn currency_code<S: Into<String>>(mut self, currency_code: S) -> Self {
self.currency_code = Some(currency_code.into());
self
}
/// Build the Currency object
pub fn build(self) -> Result<Currency, Box<EvalAltResult>> {
Ok(Currency {
id: self.id.ok_or("id is required")?,
amount: self.amount.ok_or("amount is required")?,
currency_code: self.currency_code.ok_or("currency_code is required")?,
})
}
}
// Implement Storable trait
impl Storable for Currency {}
// Implement Model trait
impl Model for Currency {
fn get_id(&self) -> u32 {
self.id
}
fn db_prefix() -> &'static str {
"currency"
}
fn db_keys(&self) -> Vec<IndexKey> {
let mut keys = Vec::new();
// Add an index for currency_code
keys.push(IndexKey {
name: "currency_code",
value: self.currency_code.clone(),
});
// Add an index for amount range
// This allows finding currencies within specific ranges
let amount_range = match self.amount {
a if a < 100.0 => "0-100",
a if a < 1000.0 => "100-1000",
a if a < 10000.0 => "1000-10000",
_ => "10000+",
};
keys.push(IndexKey {
name: "amount_range",
value: amount_range.to_string(),
});
keys
}
}

View File

@@ -0,0 +1,166 @@
use crate::db::{Model, Storable, IndexKey}; // Import Model trait and IndexKey from db module
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
/// Customer represents a customer who can purchase products or services
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Customer {
pub id: u32,
pub name: String,
pub description: String,
pub pubkey: String,
pub contact_ids: Vec<u32>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
impl Customer {
/// Create a new customer with default timestamps
pub fn new(
id: u32,
name: String,
description: String,
pubkey: String,
) -> Self {
let now = Utc::now();
Self {
id,
name,
description,
pubkey,
contact_ids: Vec::new(),
created_at: now,
updated_at: now,
}
}
/// Add a contact ID to the customer
pub fn add_contact(&mut self, contact_id: u32) {
if !self.contact_ids.contains(&contact_id) {
self.contact_ids.push(contact_id);
self.updated_at = Utc::now();
}
}
/// Remove a contact ID from the customer
pub fn remove_contact(&mut self, contact_id: u32) -> bool {
let len = self.contact_ids.len();
self.contact_ids.retain(|&id| id != contact_id);
if self.contact_ids.len() < len {
self.updated_at = Utc::now();
true
} else {
false
}
}
}
/// Builder for Customer
pub struct CustomerBuilder {
id: Option<u32>,
name: Option<String>,
description: Option<String>,
pubkey: Option<String>,
contact_ids: Vec<u32>,
created_at: Option<DateTime<Utc>>,
updated_at: Option<DateTime<Utc>>,
}
impl CustomerBuilder {
/// Create a new CustomerBuilder with all fields set to None
pub fn new() -> Self {
Self {
id: None,
name: None,
description: None,
pubkey: None,
contact_ids: Vec::new(),
created_at: None,
updated_at: None,
}
}
/// Set the id
pub fn id(mut self, id: u32) -> Self {
self.id = Some(id);
self
}
/// Set the name
pub fn name<S: Into<String>>(mut self, name: S) -> Self {
self.name = Some(name.into());
self
}
/// Set the description
pub fn description<S: Into<String>>(mut self, description: S) -> Self {
self.description = Some(description.into());
self
}
/// Set the pubkey
pub fn pubkey<S: Into<String>>(mut self, pubkey: S) -> Self {
self.pubkey = Some(pubkey.into());
self
}
/// Add a contact ID
pub fn add_contact(mut self, contact_id: u32) -> Self {
self.contact_ids.push(contact_id);
self
}
/// Set multiple contact IDs
pub fn contact_ids(mut self, contact_ids: Vec<u32>) -> Self {
self.contact_ids = contact_ids;
self
}
/// Build the Customer object
pub fn build(self) -> Result<Customer, &'static str> {
let now = Utc::now();
Ok(Customer {
id: self.id.ok_or("id is required")?,
name: self.name.ok_or("name is required")?,
description: self.description.ok_or("description is required")?,
pubkey: self.pubkey.ok_or("pubkey is required")?,
contact_ids: self.contact_ids,
created_at: self.created_at.unwrap_or(now),
updated_at: self.updated_at.unwrap_or(now),
})
}
}
impl Storable for Customer {}
// Implement Model trait
impl Model for Customer {
fn get_id(&self) -> u32 {
self.id
}
fn db_prefix() -> &'static str {
"customer"
}
fn db_keys(&self) -> Vec<IndexKey> {
let mut keys = Vec::new();
// Add an index for the name
keys.push(IndexKey {
name: "name",
value: self.name.clone(),
});
// Add an index for the pubkey
keys.push(IndexKey {
name: "pubkey",
value: self.pubkey.clone(),
});
keys
}
}

View File

@@ -0,0 +1,178 @@
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use crate::db::model::{Model, Storable};
/// ExchangeRate represents an exchange rate between two currencies
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExchangeRate {
pub id: u32,
pub base_currency: String,
pub target_currency: String,
pub rate: f64,
pub timestamp: DateTime<Utc>,
}
impl ExchangeRate {
/// Create a new exchange rate
pub fn new(id: u32, base_currency: String, target_currency: String, rate: f64) -> Self {
Self {
id,
base_currency,
target_currency,
rate,
timestamp: Utc::now(),
}
}
}
/// Builder for ExchangeRate
pub struct ExchangeRateBuilder {
id: Option<u32>,
base_currency: Option<String>,
target_currency: Option<String>,
rate: Option<f64>,
timestamp: Option<DateTime<Utc>>,
}
impl ExchangeRateBuilder {
/// Create a new ExchangeRateBuilder with all fields set to None
pub fn new() -> Self {
Self {
id: None,
base_currency: None,
target_currency: None,
rate: None,
timestamp: None,
}
}
/// Set the id
pub fn id(mut self, id: u32) -> Self {
self.id = Some(id);
self
}
/// Set the base currency
pub fn base_currency<S: Into<String>>(mut self, base_currency: S) -> Self {
self.base_currency = Some(base_currency.into());
self
}
/// Set the target currency
pub fn target_currency<S: Into<String>>(mut self, target_currency: S) -> Self {
self.target_currency = Some(target_currency.into());
self
}
/// Set the rate
pub fn rate(mut self, rate: f64) -> Self {
self.rate = Some(rate);
self
}
/// Set the timestamp
pub fn timestamp(mut self, timestamp: DateTime<Utc>) -> Self {
self.timestamp = Some(timestamp);
self
}
/// Build the ExchangeRate object
pub fn build(self) -> Result<ExchangeRate, &'static str> {
let now = Utc::now();
Ok(ExchangeRate {
id: self.id.ok_or("id is required")?,
base_currency: self.base_currency.ok_or("base_currency is required")?,
target_currency: self.target_currency.ok_or("target_currency is required")?,
rate: self.rate.ok_or("rate is required")?,
timestamp: self.timestamp.unwrap_or(now),
})
}
}
impl Storable for ExchangeRate {}
// Implement Model trait
impl Model for ExchangeRate {
fn get_id(&self) -> u32 {
self.id
}
fn db_prefix() -> &'static str {
"exchange_rate"
}
}
/// ExchangeRateService provides methods to get and set exchange rates
#[derive(Clone)]
pub struct ExchangeRateService {
rates: Arc<Mutex<HashMap<String, ExchangeRate>>>,
}
impl ExchangeRateService {
/// Create a new exchange rate service
pub fn new() -> Self {
Self {
rates: Arc::new(Mutex::new(HashMap::new())),
}
}
/// Set an exchange rate
pub fn set_rate(&self, exchange_rate: ExchangeRate) {
let key = format!("{}_{}", exchange_rate.base_currency, exchange_rate.target_currency);
let mut rates = self.rates.lock().unwrap();
rates.insert(key, exchange_rate);
}
/// Get an exchange rate
pub fn get_rate(&self, base_currency: &str, target_currency: &str) -> Option<ExchangeRate> {
let key = format!("{}_{}", base_currency, target_currency);
let rates = self.rates.lock().unwrap();
rates.get(&key).cloned()
}
/// Convert an amount from one currency to another
pub fn convert(&self, amount: f64, from_currency: &str, to_currency: &str) -> Option<f64> {
// If the currencies are the same, return the amount
if from_currency == to_currency {
return Some(amount);
}
// Try to get the direct exchange rate
if let Some(rate) = self.get_rate(from_currency, to_currency) {
return Some(amount * rate.rate);
}
// Try to get the inverse exchange rate
if let Some(rate) = self.get_rate(to_currency, from_currency) {
return Some(amount / rate.rate);
}
// Try to convert via USD
if from_currency != "USD" && to_currency != "USD" {
if let Some(from_to_usd) = self.convert(amount, from_currency, "USD") {
return self.convert(from_to_usd, "USD", to_currency);
}
}
None
}
}
// Create a global instance of the exchange rate service
lazy_static::lazy_static! {
pub static ref EXCHANGE_RATE_SERVICE: ExchangeRateService = {
let service = ExchangeRateService::new();
// Set some default exchange rates
service.set_rate(ExchangeRate::new(1, "USD".to_string(), "EUR".to_string(), 0.85));
service.set_rate(ExchangeRate::new(2, "USD".to_string(), "GBP".to_string(), 0.75));
service.set_rate(ExchangeRate::new(3, "USD".to_string(), "JPY".to_string(), 110.0));
service.set_rate(ExchangeRate::new(4, "USD".to_string(), "CAD".to_string(), 1.25));
service.set_rate(ExchangeRate::new(5, "USD".to_string(), "AUD".to_string(), 1.35));
service
};
}

View File

@@ -0,0 +1,577 @@
use crate::models::biz::Currency; // Use crate:: for importing from the module
use crate::db::{Model, Storable, IndexKey}; // Import Model trait and IndexKey from db module
use chrono::{DateTime, Utc, Datelike};
use serde::{Deserialize, Serialize};
/// InvoiceStatus represents the status of an invoice
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum InvoiceStatus {
Draft,
Sent,
Paid,
Overdue,
Cancelled,
}
/// PaymentStatus represents the payment status of an invoice
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum PaymentStatus {
Unpaid,
PartiallyPaid,
Paid,
}
/// Payment represents a payment made against an invoice
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Payment {
pub amount: Currency,
pub date: DateTime<Utc>,
pub method: String,
pub comment: String,
}
impl Payment {
/// Create a new payment
pub fn new(amount: Currency, method: String, comment: String) -> Self {
Self {
amount,
date: Utc::now(),
method,
comment,
}
}
}
/// InvoiceItem represents an item in an invoice
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InvoiceItem {
pub id: u32,
pub invoice_id: u32,
pub description: String,
pub amount: Currency,
pub service_id: Option<u32>,
pub sale_id: Option<u32>,
}
impl InvoiceItem {
/// Create a new invoice item
pub fn new(
id: u32,
invoice_id: u32,
description: String,
amount: Currency,
) -> Self {
Self {
id,
invoice_id,
description,
amount,
service_id: None,
sale_id: None,
}
}
/// Link the invoice item to a service
pub fn link_to_service(&mut self, service_id: u32) {
self.service_id = Some(service_id);
self.sale_id = None; // An invoice item can only be linked to either a service or a sale
}
/// Link the invoice item to a sale
pub fn link_to_sale(&mut self, sale_id: u32) {
self.sale_id = Some(sale_id);
self.service_id = None; // An invoice item can only be linked to either a service or a sale
}
}
/// Builder for InvoiceItem
pub struct InvoiceItemBuilder {
id: Option<u32>,
invoice_id: Option<u32>,
description: Option<String>,
amount: Option<Currency>,
service_id: Option<u32>,
sale_id: Option<u32>,
}
impl InvoiceItemBuilder {
/// Create a new InvoiceItemBuilder with all fields set to None
pub fn new() -> Self {
Self {
id: None,
invoice_id: None,
description: None,
amount: None,
service_id: None,
sale_id: None,
}
}
/// Set the id
pub fn id(mut self, id: u32) -> Self {
self.id = Some(id);
self
}
/// Set the invoice_id
pub fn invoice_id(mut self, invoice_id: u32) -> Self {
self.invoice_id = Some(invoice_id);
self
}
/// Set the description
pub fn description<S: Into<String>>(mut self, description: S) -> Self {
self.description = Some(description.into());
self
}
/// Set the amount
pub fn amount(mut self, amount: Currency) -> Self {
self.amount = Some(amount);
self
}
/// Set the service_id
pub fn service_id(mut self, service_id: u32) -> Self {
self.service_id = Some(service_id);
self.sale_id = None; // An invoice item can only be linked to either a service or a sale
self
}
/// Set the sale_id
pub fn sale_id(mut self, sale_id: u32) -> Self {
self.sale_id = Some(sale_id);
self.service_id = None; // An invoice item can only be linked to either a service or a sale
self
}
/// Build the InvoiceItem object
pub fn build(self) -> Result<InvoiceItem, &'static str> {
Ok(InvoiceItem {
id: self.id.ok_or("id is required")?,
invoice_id: self.invoice_id.ok_or("invoice_id is required")?,
description: self.description.ok_or("description is required")?,
amount: self.amount.ok_or("amount is required")?,
service_id: self.service_id,
sale_id: self.sale_id,
})
}
}
/// Invoice represents an invoice sent to a customer
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Invoice {
pub id: u32,
pub customer_id: u32,
pub total_amount: Currency,
pub balance_due: Currency,
pub status: InvoiceStatus,
pub payment_status: PaymentStatus,
pub issue_date: DateTime<Utc>,
pub due_date: DateTime<Utc>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
pub items: Vec<InvoiceItem>,
pub payments: Vec<Payment>,
}
impl Invoice {
/// Create a new invoice with default timestamps
pub fn new(
id: u32,
customer_id: u32,
currency_code: String,
issue_date: DateTime<Utc>,
due_date: DateTime<Utc>,
) -> Self {
let now = Utc::now();
let zero_amount = Currency::new(
0, // Use 0 as a temporary ID for zero amounts
0.0,
currency_code.clone()
);
Self {
id,
customer_id,
total_amount: zero_amount.clone(),
balance_due: zero_amount,
status: InvoiceStatus::Draft,
payment_status: PaymentStatus::Unpaid,
issue_date,
due_date,
created_at: now,
updated_at: now,
items: Vec::new(),
payments: Vec::new(),
}
}
/// Add an item to the invoice and update the total amount
pub fn add_item(&mut self, item: InvoiceItem) {
// Make sure the item's invoice_id matches this invoice
assert_eq!(self.id, item.invoice_id, "Item invoice_id must match invoice id");
// Update the total amount
if self.items.is_empty() {
// First item, initialize the total amount with the same currency
self.total_amount = Currency::new(
0, // Use 0 as a temporary ID
item.amount.amount,
item.amount.currency_code.clone()
);
self.balance_due = Currency::new(
0, // Use 0 as a temporary ID
item.amount.amount,
item.amount.currency_code.clone()
);
} else {
// Add to the existing total
// (Assumes all items have the same currency)
self.total_amount.amount += item.amount.amount;
self.balance_due.amount += item.amount.amount;
}
// Add the item to the list
self.items.push(item);
// Update the invoice timestamp
self.updated_at = Utc::now();
}
/// Calculate the total amount based on all items
pub fn calculate_total(&mut self) {
if self.items.is_empty() {
return;
}
// Get the currency code from the first item
let currency_code = self.items[0].amount.currency_code.clone();
// Calculate the total amount
let mut total = 0.0;
for item in &self.items {
total += item.amount.amount;
}
// Update the total amount
self.total_amount = Currency::new(
0, // Use 0 as a temporary ID
total,
currency_code.clone()
);
// Recalculate the balance due
self.calculate_balance();
// Update the invoice timestamp
self.updated_at = Utc::now();
}
/// Add a payment to the invoice and update the balance due and payment status
pub fn add_payment(&mut self, payment: Payment) {
// Update the balance due
self.balance_due.amount -= payment.amount.amount;
// Add the payment to the list
self.payments.push(payment);
// Update the payment status
self.update_payment_status();
// Update the invoice timestamp
self.updated_at = Utc::now();
}
/// Calculate the balance due based on total amount and payments
pub fn calculate_balance(&mut self) {
// Start with the total amount
let mut balance = self.total_amount.amount;
// Subtract all payments
for payment in &self.payments {
balance -= payment.amount.amount;
}
// Update the balance due
self.balance_due = Currency::new(
0, // Use 0 as a temporary ID
balance,
self.total_amount.currency_code.clone()
);
// Update the payment status
self.update_payment_status();
}
/// Update the payment status based on the balance due
fn update_payment_status(&mut self) {
if self.balance_due.amount <= 0.0 {
self.payment_status = PaymentStatus::Paid;
// If fully paid, also update the invoice status
if self.status != InvoiceStatus::Cancelled {
self.status = InvoiceStatus::Paid;
}
} else if self.payments.is_empty() {
self.payment_status = PaymentStatus::Unpaid;
} else {
self.payment_status = PaymentStatus::PartiallyPaid;
}
}
/// Update the status of the invoice
pub fn update_status(&mut self, status: InvoiceStatus) {
self.status = status;
self.updated_at = Utc::now();
// If the invoice is cancelled, don't change the payment status
if status != InvoiceStatus::Cancelled {
// Re-evaluate the payment status
self.update_payment_status();
}
}
/// Check if the invoice is overdue
pub fn is_overdue(&self) -> bool {
let now = Utc::now();
self.payment_status != PaymentStatus::Paid &&
now > self.due_date &&
self.status != InvoiceStatus::Cancelled
}
/// Mark the invoice as overdue if it's past the due date
pub fn check_if_overdue(&mut self) -> bool {
if self.is_overdue() && self.status != InvoiceStatus::Overdue {
self.status = InvoiceStatus::Overdue;
self.updated_at = Utc::now();
true
} else {
false
}
}
}
/// Builder for Invoice
pub struct InvoiceBuilder {
id: Option<u32>,
customer_id: Option<u32>,
total_amount: Option<Currency>,
balance_due: Option<Currency>,
status: Option<InvoiceStatus>,
payment_status: Option<PaymentStatus>,
issue_date: Option<DateTime<Utc>>,
due_date: Option<DateTime<Utc>>,
created_at: Option<DateTime<Utc>>,
updated_at: Option<DateTime<Utc>>,
items: Vec<InvoiceItem>,
payments: Vec<Payment>,
currency_code: Option<String>,
}
impl InvoiceBuilder {
/// Create a new InvoiceBuilder with all fields set to None
pub fn new() -> Self {
Self {
id: None,
customer_id: None,
total_amount: None,
balance_due: None,
status: None,
payment_status: None,
issue_date: None,
due_date: None,
created_at: None,
updated_at: None,
items: Vec::new(),
payments: Vec::new(),
currency_code: None,
}
}
/// Set the id
pub fn id(mut self, id: u32) -> Self {
self.id = Some(id);
self
}
/// Set the customer_id
pub fn customer_id(mut self, customer_id: u32) -> Self {
self.customer_id = Some(customer_id);
self
}
/// Set the currency_code
pub fn currency_code<S: Into<String>>(mut self, currency_code: S) -> Self {
self.currency_code = Some(currency_code.into());
self
}
/// Set the status
pub fn status(mut self, status: InvoiceStatus) -> Self {
self.status = Some(status);
self
}
/// Set the issue_date
pub fn issue_date(mut self, issue_date: DateTime<Utc>) -> Self {
self.issue_date = Some(issue_date);
self
}
/// Set the due_date
pub fn due_date(mut self, due_date: DateTime<Utc>) -> Self {
self.due_date = Some(due_date);
self
}
/// Add an item to the invoice
pub fn add_item(mut self, item: InvoiceItem) -> Self {
self.items.push(item);
self
}
/// Add a payment to the invoice
pub fn add_payment(mut self, payment: Payment) -> Self {
self.payments.push(payment);
self
}
/// Build the Invoice object
pub fn build(self) -> Result<Invoice, &'static str> {
let now = Utc::now();
let id = self.id.ok_or("id is required")?;
let currency_code = self.currency_code.ok_or("currency_code is required")?;
// Initialize with empty total amount and balance due
let mut total_amount = Currency::new(
0, // Use 0 as a temporary ID
0.0,
currency_code.clone()
);
// Calculate total amount from items
for item in &self.items {
// Make sure the item's invoice_id matches this invoice
if item.invoice_id != id {
return Err("Item invoice_id must match invoice id");
}
total_amount.amount += item.amount.amount;
}
// Calculate balance due (total minus payments)
let mut balance_due = total_amount.clone();
for payment in &self.payments {
balance_due.amount -= payment.amount.amount;
}
// Determine payment status
let payment_status = if balance_due.amount <= 0.0 {
PaymentStatus::Paid
} else if self.payments.is_empty() {
PaymentStatus::Unpaid
} else {
PaymentStatus::PartiallyPaid
};
// Determine invoice status if not provided
let status = if let Some(status) = self.status {
status
} else if payment_status == PaymentStatus::Paid {
InvoiceStatus::Paid
} else {
InvoiceStatus::Draft
};
Ok(Invoice {
id,
customer_id: self.customer_id.ok_or("customer_id is required")?,
total_amount: self.total_amount.unwrap_or(total_amount),
balance_due: self.balance_due.unwrap_or(balance_due),
status,
payment_status,
issue_date: self.issue_date.ok_or("issue_date is required")?,
due_date: self.due_date.ok_or("due_date is required")?,
created_at: self.created_at.unwrap_or(now),
updated_at: self.updated_at.unwrap_or(now),
items: self.items,
payments: self.payments,
})
}
}
impl Storable for Invoice {}
// Implement Model trait
impl Model for Invoice {
fn get_id(&self) -> u32 {
self.id
}
fn db_prefix() -> &'static str {
"invoice"
}
fn db_keys(&self) -> Vec<IndexKey> {
let mut keys = Vec::new();
// Add an index for customer_id
keys.push(IndexKey {
name: "customer_id",
value: self.customer_id.to_string(),
});
// Add an index for status
keys.push(IndexKey {
name: "status",
value: format!("{:?}", self.status),
});
// Add an index for payment_status
keys.push(IndexKey {
name: "payment_status",
value: format!("{:?}", self.payment_status),
});
// Add an index for currency code
keys.push(IndexKey {
name: "currency",
value: self.total_amount.currency_code.clone(),
});
// Add an index for amount range
let amount_range = match self.total_amount.amount {
a if a < 100.0 => "0-100",
a if a < 1000.0 => "100-1000",
a if a < 10000.0 => "1000-10000",
_ => "10000+",
};
keys.push(IndexKey {
name: "amount_range",
value: amount_range.to_string(),
});
// Add an index for issue date (year-month)
keys.push(IndexKey {
name: "issue_date",
value: format!("{}", self.issue_date.format("%Y-%m")),
});
// Add an index for due date (year-month)
keys.push(IndexKey {
name: "due_date",
value: format!("{}", self.due_date.format("%Y-%m")),
});
// Add an index for overdue invoices
if self.is_overdue() {
keys.push(IndexKey {
name: "overdue",
value: "true".to_string(),
});
}
keys
}
}

View File

@@ -0,0 +1,30 @@
pub mod user;
pub mod vote;
pub mod company;
pub mod meeting;
pub mod product;
pub mod sale;
pub mod shareholder;
// pub mod db; // Moved to src/zaz/db
// pub mod migration; // Removed
// Re-export all model types for convenience
pub use user::User;
pub use vote::{Vote, VoteOption, Ballot, VoteStatus};
pub use company::{Company, CompanyStatus, BusinessType};
pub use meeting::Meeting;
pub use product::{Product, ProductComponent, ProductType, ProductStatus};
pub use sale::Sale;
pub use shareholder::Shareholder;
// Re-export builder types
pub use product::{ProductBuilder, ProductComponentBuilder};
pub use sale::{SaleBuilder, SaleItemBuilder};
// Re-export Currency and its builder
pub use product::Currency;
pub use currency::CurrencyBuilder;
// Re-export database components
// Re-export database components from db module
pub use crate::db::{DB, DBBuilder, Model, Storable, DbError, DbResult, ModelRegistration, ModelRegistrar};

View File

@@ -0,0 +1,28 @@
pub mod currency;
pub mod product;
pub mod sale;
pub mod exchange_rate;
pub mod service;
pub mod customer;
pub mod contract;
pub mod invoice;
// Re-export all model types for convenience
pub use product::{Product, ProductComponent, ProductType, ProductStatus};
pub use sale::{Sale, SaleItem, SaleStatus};
pub use currency::Currency;
pub use exchange_rate::{ExchangeRate, ExchangeRateService, EXCHANGE_RATE_SERVICE};
pub use service::{Service, ServiceItem, ServiceStatus, BillingFrequency};
pub use customer::Customer;
pub use contract::{Contract, ContractStatus};
pub use invoice::{Invoice, InvoiceItem, InvoiceStatus, PaymentStatus, Payment};
// Re-export builder types
pub use product::{ProductBuilder, ProductComponentBuilder};
pub use sale::{SaleBuilder, SaleItemBuilder};
pub use currency::CurrencyBuilder;
pub use exchange_rate::ExchangeRateBuilder;
pub use service::{ServiceBuilder, ServiceItemBuilder};
pub use customer::CustomerBuilder;
pub use contract::ContractBuilder;
pub use invoice::{InvoiceBuilder, InvoiceItemBuilder};

View File

@@ -0,0 +1,425 @@
use crate::db::model::{Model, IndexKey};
use crate::db::Storable;
use chrono::{DateTime, Duration, Utc};
use rhai::{CustomType, EvalAltResult, TypeBuilder, export_module};
use serde::{Deserialize, Serialize};
/// ProductType represents the type of a product
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ProductType {
Product,
Service,
}
/// ProductStatus represents the status of a product
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ProductStatus {
Available,
Unavailable,
}
/// ProductComponent represents a component of a product
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProductComponent {
pub id: u32,
pub name: String,
pub description: String,
pub quantity: i64,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
impl ProductComponent {
/// Create a new product component with default timestamps
pub fn new(id: u32, name: String, description: String, quantity: i64) -> Self {
let now = Utc::now();
Self {
id,
name,
description,
quantity,
created_at: now,
updated_at: now,
}
}
}
/// Builder for ProductComponent
#[derive(Clone, CustomType)]
pub struct ProductComponentBuilder {
id: Option<u32>,
name: Option<String>,
description: Option<String>,
quantity: Option<i64>,
created_at: Option<DateTime<Utc>>,
updated_at: Option<DateTime<Utc>>,
}
impl ProductComponentBuilder {
/// Create a new ProductComponentBuilder with all fields set to None
pub fn new() -> Self {
Self {
id: None,
name: None,
description: None,
quantity: None,
created_at: None,
updated_at: None,
}
}
/// Set the id
pub fn id(mut self, id: u32) -> Self {
self.id = Some(id);
self
}
/// Set the name
pub fn name<S: Into<String>>(mut self, name: S) -> Self {
self.name = Some(name.into());
self
}
/// Set the description
pub fn description<S: Into<String>>(mut self, description: S) -> Self {
self.description = Some(description.into());
self
}
/// Set the quantity
pub fn quantity(mut self, quantity: i64) -> Self {
self.quantity = Some(quantity);
self
}
/// Set the created_at timestamp
pub fn created_at(mut self, created_at: DateTime<Utc>) -> Self {
self.created_at = Some(created_at);
self
}
/// Set the updated_at timestamp
pub fn updated_at(mut self, updated_at: DateTime<Utc>) -> Self {
self.updated_at = Some(updated_at);
self
}
/// Build the ProductComponent object
pub fn build(self) -> Result<ProductComponent, Box<EvalAltResult>> {
let now = Utc::now();
Ok(ProductComponent {
id: self.id.ok_or("id is required")?,
name: self.name.ok_or("name is required")?,
description: self.description.ok_or("description is required")?,
quantity: self.quantity.ok_or("quantity is required")?,
created_at: self.created_at.unwrap_or(now),
updated_at: self.updated_at.unwrap_or(now),
})
}
}
/// Product represents a product or service offered in the system
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Product {
pub id: u32,
pub name: String,
pub description: String,
pub price: Currency,
pub type_: ProductType,
pub category: String,
pub status: ProductStatus,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
pub max_amount: i64, // means allows us to define how many max of this there are
pub purchase_till: DateTime<Utc>,
pub active_till: DateTime<Utc>, // after this product no longer active if e.g. a service
pub components: Vec<ProductComponent>,
}
impl Product {
/// Create a new product with default timestamps
pub fn new(
id: u32,
name: String,
description: String,
price: Currency,
type_: ProductType,
category: String,
status: ProductStatus,
max_amount: i64,
validity_days: i64, // How many days the product is valid after purchase
) -> Self {
let now = Utc::now();
// Default: purchasable for 1 year, active for specified validity days after purchase
Self {
id,
name,
description,
price,
type_,
category,
status,
created_at: now,
updated_at: now,
max_amount,
purchase_till: now + Duration::days(365),
active_till: now + Duration::days(validity_days),
components: Vec::new(),
}
}
/// Add a component to this product
pub fn add_component(&mut self, component: ProductComponent) {
self.components.push(component);
self.updated_at = Utc::now();
}
/// Update the purchase availability timeframe
pub fn set_purchase_period(&mut self, purchase_till: DateTime<Utc>) {
self.purchase_till = purchase_till;
self.updated_at = Utc::now();
}
/// Update the active timeframe
pub fn set_active_period(&mut self, active_till: DateTime<Utc>) {
self.active_till = active_till;
self.updated_at = Utc::now();
}
/// Check if the product is available for purchase
pub fn is_purchasable(&self) -> bool {
self.status == ProductStatus::Available && Utc::now() <= self.purchase_till
}
/// Check if the product is still active (for services)
pub fn is_active(&self) -> bool {
Utc::now() <= self.active_till
}
}
/// Builder for Product
#[derive(Clone, CustomType)]
pub struct ProductBuilder {
id: Option<u32>,
name: Option<String>,
description: Option<String>,
price: Option<Currency>,
type_: Option<ProductType>,
category: Option<String>,
status: Option<ProductStatus>,
created_at: Option<DateTime<Utc>>,
updated_at: Option<DateTime<Utc>>,
max_amount: Option<i64>,
purchase_till: Option<DateTime<Utc>>,
active_till: Option<DateTime<Utc>>,
components: Vec<ProductComponent>,
validity_days: Option<i64>,
}
impl ProductBuilder {
/// Create a new ProductBuilder with all fields set to None
pub fn new() -> Self {
Self {
id: None,
name: None,
description: None,
price: None,
type_: None,
category: None,
status: None,
created_at: None,
updated_at: None,
max_amount: None,
purchase_till: None,
active_till: None,
components: Vec::new(),
validity_days: None,
}
}
/// Set the id
pub fn id(mut self, id: u32) -> Self {
self.id = Some(id);
self
}
/// Set the name
pub fn name<S: Into<String>>(mut self, name: S) -> Self {
self.name = Some(name.into());
self
}
/// Set the description
pub fn description<S: Into<String>>(mut self, description: S) -> Self {
self.description = Some(description.into());
self
}
/// Set the price
pub fn price(mut self, price: Currency) -> Self {
self.price = Some(price);
self
}
/// Set the product type
pub fn type_(mut self, type_: ProductType) -> Self {
self.type_ = Some(type_);
self
}
/// Set the category
pub fn category<S: Into<String>>(mut self, category: S) -> Self {
self.category = Some(category.into());
self
}
/// Set the status
pub fn status(mut self, status: ProductStatus) -> Self {
self.status = Some(status);
self
}
/// Set the max amount
pub fn max_amount(mut self, max_amount: i64) -> Self {
self.max_amount = Some(max_amount);
self
}
/// Set the validity days
pub fn validity_days(mut self, validity_days: i64) -> Self {
self.validity_days = Some(validity_days);
self
}
/// Set the purchase_till date directly
pub fn purchase_till(mut self, purchase_till: DateTime<Utc>) -> Self {
self.purchase_till = Some(purchase_till);
self
}
/// Set the active_till date directly
pub fn active_till(mut self, active_till: DateTime<Utc>) -> Self {
self.active_till = Some(active_till);
self
}
/// Add a component to the product
pub fn add_component(mut self, component: ProductComponent) -> Self {
self.components.push(component);
self
}
/// Build the Product object
pub fn build(self) -> Result<Product, &'static str> {
let now = Utc::now();
let created_at = self.created_at.unwrap_or(now);
let updated_at = self.updated_at.unwrap_or(now);
// Calculate purchase_till and active_till based on validity_days if not set directly
let purchase_till = self.purchase_till.unwrap_or(now + Duration::days(365));
let active_till = if let Some(validity_days) = self.validity_days {
self.active_till
.unwrap_or(now + Duration::days(validity_days))
} else {
self.active_till
.ok_or("Either active_till or validity_days must be provided")?
};
Ok(Product {
id: self.id.ok_or("id is required")?,
name: self.name.ok_or("name is required")?,
description: self.description.ok_or("description is required")?,
price: self.price.ok_or("price is required")?,
type_: self.type_.ok_or("type_ is required")?,
category: self.category.ok_or("category is required")?,
status: self.status.ok_or("status is required")?,
created_at,
updated_at,
max_amount: self.max_amount.ok_or("max_amount is required")?,
purchase_till,
active_till,
components: self.components,
})
}
}
// Implement Storable trait
impl Storable for Product {}
// Implement Model trait
impl Model for Product {
fn get_id(&self) -> u32 {
self.id
}
fn db_prefix() -> &'static str {
"product"
}
fn db_keys(&self) -> Vec<IndexKey> {
let mut keys = Vec::new();
// Add an index for name
keys.push(IndexKey {
name: "name",
value: self.name.clone(),
});
// Add an index for category
keys.push(IndexKey {
name: "category",
value: self.category.clone(),
});
// Add an index for product type
keys.push(IndexKey {
name: "type",
value: format!("{:?}", self.type_),
});
// Add an index for status
keys.push(IndexKey {
name: "status",
value: format!("{:?}", self.status),
});
// Add an index for price range
let price_range = match self.price.amount {
a if a < 100.0 => "0-100",
a if a < 1000.0 => "100-1000",
a if a < 10000.0 => "1000-10000",
_ => "10000+",
};
keys.push(IndexKey {
name: "price_range",
value: price_range.to_string(),
});
// Add an index for currency code
keys.push(IndexKey {
name: "currency",
value: self.price.currency_code.clone(),
});
// Add indexes for purchasable and active products
if self.is_purchasable() {
keys.push(IndexKey {
name: "purchasable",
value: "true".to_string(),
});
}
if self.is_active() {
keys.push(IndexKey {
name: "active",
value: "true".to_string(),
});
}
keys
}
}
// Import Currency from the currency module
use crate::models::biz::Currency;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,19 @@
[package]
name = "biz_rhai"
version = "0.1.0"
edition = "2021"
[dependencies]
rhai = "1.21.0"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
chrono = "0.4"
herodb = { path = "../../../.." }
[lib]
name = "biz_rhai"
path = "src/lib.rs"
[[example]]
name = "example"
path = "examples/example.rs"

View File

@@ -0,0 +1,168 @@
// Example script demonstrating the use of Business module operations
// Custom repeat function since Rhai doesn't have a built-in repeat method
fn repeat_str(str, count) {
let result = "";
for i in 0..count {
result += str;
}
return result;
}
// Print a section header
fn print_section(title) {
let line = repeat_str("=", 50);
print(line);
print(" " + title);
print(line);
}
print_section("BUSINESS MODULE OPERATIONS EXAMPLE");
// Currency Operations
print_section("CURRENCY OPERATIONS");
// Create a currency
print("\nCreating currencies...");
let usd = create_currency(100.0, "USD");
print("USD Currency created: " + usd.amount + " " + usd.currency_code);
// Convert currencies
print("\nConverting currencies...");
let eur = create_currency(150.0, "EUR");
print("EUR Currency created: " + eur.amount + " " + eur.currency_code);
let eur_to_usd = convert_currency(eur, "USD");
print("EUR to USD: " + eur.amount + " EUR = " + eur_to_usd.amount + " USD");
// Product Component Operations
print_section("PRODUCT COMPONENT OPERATIONS");
// Create a product component
print("\nCreating product components...");
let component1 = create_product_component(1, "CPU", "Intel i7 Processor", 1);
print("Component created: " + component1.name);
print(" ID: " + component1.id);
print(" Description: " + component1.description);
print(" Quantity: " + component1.quantity);
// Create another component
let component2 = create_product_component(2, "RAM", "32GB DDR4 Memory", 2);
print("Component created: " + component2.name);
print(" ID: " + component2.id);
print(" Description: " + component2.description);
print(" Quantity: " + component2.quantity);
// Product Operations
print_section("PRODUCT OPERATIONS");
// Create a product using builder pattern
print("\nCreating a product...");
let product = create_product_builder()
.product_name("High-End Gaming PC")
.product_description("Ultimate gaming experience")
.product_price(create_currency(1999.99, "USD"))
.product_validity_days(365)
.build();
print("Product created: " + product.name);
print(" ID: " + product.id);
print(" Description: " + product.description);
print(" Price: " + product.price.amount + " " + product.price.currency_code);
// Add components to the product
print("\nAdding components to product...");
let product_with_components = product
.add_component(component1)
.add_component(component2);
print("Components added to product");
// Get components from the product
let components = get_product_components(product_with_components);
print("Number of components: " + components.len());
// Customer Operations
print_section("CUSTOMER OPERATIONS");
// Create a customer
print("\nCreating a customer...");
let customer = create_customer("John Doe", "john@example.com", "+1234567890", "123 Main St");
print("Customer created: " + customer.name);
print(" Email: " + customer.email);
print(" Phone: " + customer.phone);
print(" Address: " + customer.address);
// Sale Operations
print_section("SALE OPERATIONS");
// Create a sale
print("\nCreating a sale...");
let sale = create_sale(customer, "2025-04-19");
print("Sale created for customer: " + sale.customer.name);
print(" Date: " + sale.date);
// Add product to sale
let sale_with_item = add_sale_item(sale, product_with_components, 1);
print("Added product to sale: " + product_with_components.name);
// Service Operations
print_section("SERVICE OPERATIONS");
// Create a service
print("\nCreating a service...");
let service = create_service(
"Premium Support",
"24/7 Technical Support",
create_currency(49.99, "USD"),
30
);
print("Service created: " + service.name);
print(" Description: " + service.description);
print(" Price: " + service.price.amount + " " + service.price.currency_code);
print(" Duration: " + service.duration + " days");
// Contract Operations
print_section("CONTRACT OPERATIONS");
// Create a contract
print("\nCreating a contract...");
let contract = create_contract(
"Support Agreement",
"Annual support contract",
"2025-04-19",
"2026-04-19",
create_currency(599.99, "USD"),
"Active"
);
print("Contract created: " + contract.title);
print(" Description: " + contract.description);
print(" Start Date: " + contract.start_date);
print(" End Date: " + contract.end_date);
print(" Value: " + contract.value.amount + " " + contract.value.currency_code);
print(" Status: " + contract.status);
// Invoice Operations
print_section("INVOICE OPERATIONS");
// Create an invoice
print("\nCreating an invoice...");
let invoice = create_invoice(
"INV-2025-001",
"2025-04-19",
"2025-05-19",
customer,
create_currency(2499.99, "USD"),
"Issued",
"Pending"
);
print("Invoice created: " + invoice.number);
print(" Date: " + invoice.date);
print(" Due Date: " + invoice.due_date);
print(" Customer: " + invoice.customer.name);
print(" Amount: " + invoice.amount.amount + " " + invoice.amount.currency_code);
print(" Status: " + invoice.status);
print(" Payment Status: " + invoice.payment_status);
print_section("EXAMPLE COMPLETED");
print("All business module operations completed successfully!");

View File

@@ -0,0 +1,41 @@
use std::{fs, path::Path};
use biz_rhai::create_rhai_engine;
fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("=== Business Module Rhai Wrapper Example ===");
// Create a Rhai engine with business module functionality
let mut engine = create_rhai_engine();
println!("Successfully created Rhai engine");
// Get the path to the example.rhai script
let script_path = get_script_path()?;
println!("Loading script from: {}", script_path.display());
// Load the script content
let script = fs::read_to_string(&script_path)
.map_err(|e| format!("Failed to read script file: {}", e))?;
// Run the script
println!("\n=== Running Rhai script ===");
match engine.eval::<()>(&script) {
Ok(_) => println!("\nScript executed successfully!"),
Err(e) => println!("\nScript execution error: {}", e),
}
println!("\nExample completed!");
Ok(())
}
fn get_script_path() -> Result<std::path::PathBuf, String> {
// When running with cargo run --example, the script will be in the examples directory
let script_path = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("examples")
.join("example.rhai");
if script_path.exists() {
Ok(script_path)
} else {
Err(format!("Could not find example.rhai script at {}", script_path.display()))
}
}

View File

@@ -0,0 +1,88 @@
use rhai::{Engine, EvalAltResult, Map, Dynamic, Array};
use crate::wrapper::*;
use crate::generic_wrapper::ToRhai;
/// Create a new Rhai engine with business module functionality
pub fn create_rhai_engine() -> Engine {
let mut engine = Engine::new();
// Register business module types and functions
register_business_types(&mut engine);
engine
}
/// Register business module types and functions
fn register_business_types(engine: &mut Engine) {
// Currency functions
engine.register_fn("create_currency", create_currency);
engine.register_fn("convert_currency", convert_currency);
// Product functions
engine.register_fn("create_product_builder", create_product_builder);
engine.register_fn("product_name", product_builder_name);
engine.register_fn("product_description", product_builder_description);
engine.register_fn("product_price", product_builder_price);
engine.register_fn("product_validity_days", product_builder_validity_days);
engine.register_fn("add_component", product_builder_add_component);
engine.register_fn("build", product_builder_build);
// Product component functions
engine.register_fn("create_product_component", create_product_component);
engine.register_fn("component_name", product_component_name);
engine.register_fn("component_description", product_component_description);
engine.register_fn("component_quantity", product_component_quantity);
// Sale functions
engine.register_fn("create_sale", create_sale);
engine.register_fn("add_sale_item", add_sale_item);
engine.register_fn("sale_customer", sale_customer);
engine.register_fn("sale_date", sale_date);
engine.register_fn("sale_status", sale_status);
// Customer functions
engine.register_fn("create_customer", create_customer);
engine.register_fn("customer_name", customer_name);
engine.register_fn("customer_email", customer_email);
engine.register_fn("customer_phone", customer_phone);
engine.register_fn("customer_address", customer_address);
// Service functions
engine.register_fn("create_service", create_service);
engine.register_fn("service_name", service_name);
engine.register_fn("service_description", service_description);
engine.register_fn("service_price", service_price);
engine.register_fn("service_duration", service_duration);
// Contract functions
engine.register_fn("create_contract", create_contract);
engine.register_fn("contract_title", contract_title);
engine.register_fn("contract_description", contract_description);
engine.register_fn("contract_start_date", contract_start_date);
engine.register_fn("contract_end_date", contract_end_date);
engine.register_fn("contract_value", contract_value);
engine.register_fn("contract_status", contract_status);
// Invoice functions
engine.register_fn("create_invoice", create_invoice);
engine.register_fn("invoice_number", invoice_number);
engine.register_fn("invoice_date", invoice_date);
engine.register_fn("invoice_due_date", invoice_due_date);
engine.register_fn("invoice_customer", invoice_customer);
engine.register_fn("invoice_amount", invoice_amount);
engine.register_fn("invoice_status", invoice_status);
engine.register_fn("invoice_payment_status", invoice_payment_status);
// Helper function to get components from a product
engine.register_fn("get_product_components", |product_map: Map| -> Array {
let mut array = Array::new();
if let Some(components) = product_map.get("components") {
if let Some(components_array) = components.clone().try_cast::<Array>() {
return components_array;
}
}
array
});
}

View File

@@ -0,0 +1,132 @@
use std::collections::HashMap;
use rhai::{Dynamic, Map, Array};
/// Local wrapper trait for sal::rhai::ToRhai to avoid orphan rule violations
pub trait ToRhai {
/// Convert to a Rhai Dynamic value
fn to_rhai(&self) -> Dynamic;
}
// Implementation of ToRhai for Dynamic
impl ToRhai for Dynamic {
fn to_rhai(&self) -> Dynamic {
self.clone()
}
}
/// Generic trait for wrapping Rust functions to be used with Rhai
pub trait RhaiWrapper {
/// Wrap a function that takes ownership of self
fn wrap_consuming<F, R>(self, f: F) -> Dynamic
where
Self: Sized + Clone,
F: FnOnce(Self) -> R,
R: ToRhai;
/// Wrap a function that takes a mutable reference to self
fn wrap_mut<F, R>(&mut self, f: F) -> Dynamic
where
Self: Sized + Clone,
F: FnOnce(&mut Self) -> R,
R: ToRhai;
/// Wrap a function that takes an immutable reference to self
fn wrap<F, R>(&self, f: F) -> Dynamic
where
Self: Sized + Clone,
F: FnOnce(&Self) -> R,
R: ToRhai;
}
/// Implementation of RhaiWrapper for any type
impl<T> RhaiWrapper for T {
fn wrap_consuming<F, R>(self, f: F) -> Dynamic
where
Self: Sized + Clone,
F: FnOnce(Self) -> R,
R: ToRhai,
{
let result = f(self);
result.to_rhai()
}
fn wrap_mut<F, R>(&mut self, f: F) -> Dynamic
where
Self: Sized + Clone,
F: FnOnce(&mut Self) -> R,
R: ToRhai,
{
let result = f(self);
result.to_rhai()
}
fn wrap<F, R>(&self, f: F) -> Dynamic
where
Self: Sized + Clone,
F: FnOnce(&Self) -> R,
R: ToRhai,
{
let result = f(self);
result.to_rhai()
}
}
/// Convert a Rhai Map to a Rust HashMap
pub fn map_to_hashmap(map: &Map) -> HashMap<String, String> {
let mut result = HashMap::new();
for (key, value) in map.iter() {
let k = key.clone().to_string();
let v = value.clone().to_string();
if !k.is_empty() && !v.is_empty() {
result.insert(k, v);
}
}
result
}
/// Convert a HashMap<String, String> to a Rhai Map
pub fn hashmap_to_map(map: &HashMap<String, String>) -> Map {
let mut result = Map::new();
for (key, value) in map.iter() {
result.insert(key.clone().into(), Dynamic::from(value.clone()));
}
result
}
/// Convert a Rhai Array to a Vec of strings
pub fn array_to_vec_string(array: &Array) -> Vec<String> {
array.iter()
.filter_map(|item| {
let s = item.clone().to_string();
if !s.is_empty() { Some(s) } else { None }
})
.collect()
}
/// Helper function to convert Dynamic to Option<String>
pub fn dynamic_to_string_option(value: &Dynamic) -> Option<String> {
if value.is_string() {
Some(value.clone().to_string())
} else {
None
}
}
/// Helper function to convert Dynamic to Option<u32>
pub fn dynamic_to_u32_option(value: &Dynamic) -> Option<u32> {
if value.is_int() {
Some(value.as_int().unwrap() as u32)
} else {
None
}
}
/// Helper function to convert Dynamic to Option<&str> with lifetime management
pub fn dynamic_to_str_option<'a>(value: &Dynamic, storage: &'a mut String) -> Option<&'a str> {
if value.is_string() {
*storage = value.clone().to_string();
Some(storage.as_str())
} else {
None
}
}

View File

@@ -0,0 +1,11 @@
// Re-export the utility modules
pub mod generic_wrapper;
pub mod wrapper;
pub mod engine;
// Re-export the utility traits and functions
pub use generic_wrapper::{RhaiWrapper, map_to_hashmap, array_to_vec_string,
dynamic_to_string_option, hashmap_to_map};
pub use engine::create_rhai_engine;
// The create_rhai_engine function is now in the engine module

View File

@@ -0,0 +1,640 @@
//! Rhai wrappers for Business module functions
//!
//! This module provides Rhai wrappers for the functions in the Business module.
use rhai::{Engine, EvalAltResult, Array, Dynamic, Map, Position};
use std::collections::HashMap;
use crate::generic_wrapper::{ToRhai, RhaiWrapper, map_to_hashmap, dynamic_to_string_option, dynamic_to_u32_option};
// Import business module types
use chrono::{DateTime, Utc, Duration};
use herodb::models::biz::{
Currency, CurrencyBuilder,
Product, ProductBuilder, ProductComponent, ProductComponentBuilder, ProductType, ProductStatus,
Customer, CustomerBuilder,
Sale, SaleBuilder, SaleItem, SaleItemBuilder, SaleStatus,
Service, ServiceBuilder, ServiceItem, ServiceItemBuilder, ServiceStatus, BillingFrequency,
ExchangeRate, ExchangeRateBuilder, ExchangeRateService, EXCHANGE_RATE_SERVICE,
Contract, ContractBuilder, ContractStatus,
Invoice, InvoiceBuilder, InvoiceItem, InvoiceItemBuilder, InvoiceStatus, PaymentStatus, Payment
};
// Business module ToRhai implementations
// Currency ToRhai implementation
impl ToRhai for Currency {
fn to_rhai(&self) -> Dynamic {
let mut map = Map::new();
map.insert("amount".into(), Dynamic::from(self.amount));
map.insert("currency_code".into(), Dynamic::from(self.currency_code.clone()));
Dynamic::from_map(map)
}
}
// ProductType ToRhai implementation
impl ToRhai for ProductType {
fn to_rhai(&self) -> Dynamic {
let value = match self {
ProductType::Product => "Product",
ProductType::Service => "Service",
};
Dynamic::from(value)
}
}
// ProductStatus ToRhai implementation
impl ToRhai for ProductStatus {
fn to_rhai(&self) -> Dynamic {
let value = match self {
ProductStatus::Active => "Active",
ProductStatus::Error => "Error",
ProductStatus::EndOfLife => "EndOfLife",
ProductStatus::Paused => "Paused",
ProductStatus::Available => "Available",
ProductStatus::Unavailable => "Unavailable",
};
Dynamic::from(value)
}
}
// ProductComponent ToRhai implementation
impl ToRhai for ProductComponent {
fn to_rhai(&self) -> Dynamic {
let mut map = Map::new();
map.insert("id".into(), Dynamic::from(self.id));
map.insert("name".into(), Dynamic::from(self.name.clone()));
map.insert("description".into(), Dynamic::from(self.description.clone()));
map.insert("quantity".into(), Dynamic::from(self.quantity));
map.insert("created_at".into(), Dynamic::from(self.created_at.to_string()));
map.insert("updated_at".into(), Dynamic::from(self.updated_at.to_string()));
map.insert("energy_usage".into(), Dynamic::from(self.energy_usage));
map.insert("cost".into(), self.cost.to_rhai());
Dynamic::from_map(map)
}
}
// Product ToRhai implementation
impl ToRhai for Product {
fn to_rhai(&self) -> Dynamic {
let mut map = Map::new();
map.insert("id".into(), Dynamic::from(self.id));
map.insert("name".into(), Dynamic::from(self.name.clone()));
map.insert("description".into(), Dynamic::from(self.description.clone()));
map.insert("price".into(), self.price.to_rhai());
map.insert("type".into(), self.type_.to_rhai());
map.insert("category".into(), Dynamic::from(self.category.clone()));
map.insert("status".into(), self.status.to_rhai());
map.insert("created_at".into(), Dynamic::from(self.created_at.to_string()));
map.insert("updated_at".into(), Dynamic::from(self.updated_at.to_string()));
map.insert("max_amount".into(), Dynamic::from(self.max_amount));
map.insert("purchase_till".into(), Dynamic::from(self.purchase_till.to_string()));
map.insert("active_till".into(), Dynamic::from(self.active_till.to_string()));
// Convert components to an array
let components_array: Array = self.components.iter()
.map(|component| component.to_rhai())
.collect();
map.insert("components".into(), Dynamic::from(components_array));
Dynamic::from_map(map)
}
}
// SaleStatus ToRhai implementation
impl ToRhai for SaleStatus {
fn to_rhai(&self) -> Dynamic {
let value = match self {
SaleStatus::Pending => "Pending",
SaleStatus::Completed => "Completed",
SaleStatus::Cancelled => "Cancelled",
SaleStatus::Refunded => "Refunded",
};
Dynamic::from(value)
}
}
// SaleItem ToRhai implementation
impl ToRhai for SaleItem {
fn to_rhai(&self) -> Dynamic {
let mut map = Map::new();
map.insert("product_id".into(), Dynamic::from(self.product_id));
map.insert("quantity".into(), Dynamic::from(self.quantity));
map.insert("price".into(), self.price.to_rhai());
map.insert("discount".into(), Dynamic::from(self.discount));
Dynamic::from_map(map)
}
}
// Sale ToRhai implementation
impl ToRhai for Sale {
fn to_rhai(&self) -> Dynamic {
let mut map = Map::new();
map.insert("id".into(), Dynamic::from(self.id));
map.insert("customer_id".into(), Dynamic::from(self.customer_id));
map.insert("date".into(), Dynamic::from(self.date.to_string()));
map.insert("status".into(), self.status.to_rhai());
// Convert items to an array
let items_array: Array = self.items.iter()
.map(|item| item.to_rhai())
.collect();
map.insert("items".into(), Dynamic::from(items_array));
map.insert("total".into(), self.total.to_rhai());
map.insert("created_at".into(), Dynamic::from(self.created_at.to_string()));
map.insert("updated_at".into(), Dynamic::from(self.updated_at.to_string()));
Dynamic::from_map(map)
}
}
// Customer ToRhai implementation
impl ToRhai for Customer {
fn to_rhai(&self) -> Dynamic {
let mut map = Map::new();
map.insert("id".into(), Dynamic::from(self.id));
map.insert("name".into(), Dynamic::from(self.name.clone()));
map.insert("email".into(), Dynamic::from(self.email.clone()));
map.insert("phone".into(), Dynamic::from(self.phone.clone()));
map.insert("address".into(), Dynamic::from(self.address.clone()));
map.insert("created_at".into(), Dynamic::from(self.created_at.to_string()));
map.insert("updated_at".into(), Dynamic::from(self.updated_at.to_string()));
Dynamic::from_map(map)
}
}
// ServiceStatus ToRhai implementation
impl ToRhai for ServiceStatus {
fn to_rhai(&self) -> Dynamic {
let value = match self {
ServiceStatus::Active => "Active",
ServiceStatus::Inactive => "Inactive",
ServiceStatus::Pending => "Pending",
ServiceStatus::Cancelled => "Cancelled",
};
Dynamic::from(value)
}
}
// BillingFrequency ToRhai implementation
impl ToRhai for BillingFrequency {
fn to_rhai(&self) -> Dynamic {
let value = match self {
BillingFrequency::OneTime => "OneTime",
BillingFrequency::Daily => "Daily",
BillingFrequency::Weekly => "Weekly",
BillingFrequency::Monthly => "Monthly",
BillingFrequency::Quarterly => "Quarterly",
BillingFrequency::Yearly => "Yearly",
};
Dynamic::from(value)
}
}
// ServiceItem ToRhai implementation
impl ToRhai for ServiceItem {
fn to_rhai(&self) -> Dynamic {
let mut map = Map::new();
map.insert("id".into(), Dynamic::from(self.id));
map.insert("name".into(), Dynamic::from(self.name.clone()));
map.insert("description".into(), Dynamic::from(self.description.clone()));
map.insert("price".into(), self.price.to_rhai());
map.insert("created_at".into(), Dynamic::from(self.created_at.to_string()));
map.insert("updated_at".into(), Dynamic::from(self.updated_at.to_string()));
Dynamic::from_map(map)
}
}
// Service ToRhai implementation
impl ToRhai for Service {
fn to_rhai(&self) -> Dynamic {
let mut map = Map::new();
map.insert("id".into(), Dynamic::from(self.id));
map.insert("customer_id".into(), Dynamic::from(self.customer_id));
map.insert("name".into(), Dynamic::from(self.name.clone()));
map.insert("description".into(), Dynamic::from(self.description.clone()));
map.insert("status".into(), self.status.to_rhai());
map.insert("start_date".into(), Dynamic::from(self.start_date.to_string()));
map.insert("end_date".into(), Dynamic::from(self.end_date.to_string()));
map.insert("billing_frequency".into(), self.billing_frequency.to_rhai());
// Convert items to an array
let items_array: Array = self.items.iter()
.map(|item| item.to_rhai())
.collect();
map.insert("items".into(), Dynamic::from(items_array));
map.insert("total".into(), self.total.to_rhai());
map.insert("created_at".into(), Dynamic::from(self.created_at.to_string()));
map.insert("updated_at".into(), Dynamic::from(self.updated_at.to_string()));
Dynamic::from_map(map)
}
}
// ContractStatus ToRhai implementation
impl ToRhai for ContractStatus {
fn to_rhai(&self) -> Dynamic {
let value = match self {
ContractStatus::Draft => "Draft",
ContractStatus::Pending => "Pending",
ContractStatus::Active => "Active",
ContractStatus::Completed => "Completed",
ContractStatus::Cancelled => "Cancelled",
ContractStatus::Expired => "Expired",
};
Dynamic::from(value)
}
}
// Contract ToRhai implementation
impl ToRhai for Contract {
fn to_rhai(&self) -> Dynamic {
let mut map = Map::new();
map.insert("id".into(), Dynamic::from(self.id));
map.insert("customer_id".into(), Dynamic::from(self.customer_id));
map.insert("title".into(), Dynamic::from(self.title.clone()));
map.insert("description".into(), Dynamic::from(self.description.clone()));
map.insert("status".into(), self.status.to_rhai());
map.insert("start_date".into(), Dynamic::from(self.start_date.to_string()));
map.insert("end_date".into(), Dynamic::from(self.end_date.to_string()));
map.insert("value".into(), self.value.to_rhai());
map.insert("created_at".into(), Dynamic::from(self.created_at.to_string()));
map.insert("updated_at".into(), Dynamic::from(self.updated_at.to_string()));
Dynamic::from_map(map)
}
}
// InvoiceStatus ToRhai implementation
impl ToRhai for InvoiceStatus {
fn to_rhai(&self) -> Dynamic {
let value = match self {
InvoiceStatus::Draft => "Draft",
InvoiceStatus::Sent => "Sent",
InvoiceStatus::Paid => "Paid",
InvoiceStatus::Overdue => "Overdue",
InvoiceStatus::Cancelled => "Cancelled",
};
Dynamic::from(value)
}
}
// PaymentStatus ToRhai implementation
impl ToRhai for PaymentStatus {
fn to_rhai(&self) -> Dynamic {
let value = match self {
PaymentStatus::Pending => "Pending",
PaymentStatus::Completed => "Completed",
PaymentStatus::Failed => "Failed",
PaymentStatus::Refunded => "Refunded",
};
Dynamic::from(value)
}
}
// Payment ToRhai implementation
impl ToRhai for Payment {
fn to_rhai(&self) -> Dynamic {
let mut map = Map::new();
map.insert("id".into(), Dynamic::from(self.id));
map.insert("amount".into(), self.amount.to_rhai());
map.insert("date".into(), Dynamic::from(self.date.to_string()));
map.insert("method".into(), Dynamic::from(self.method.clone()));
map.insert("status".into(), self.status.to_rhai());
map.insert("reference".into(), Dynamic::from(self.reference.clone()));
Dynamic::from_map(map)
}
}
// InvoiceItem ToRhai implementation
impl ToRhai for InvoiceItem {
fn to_rhai(&self) -> Dynamic {
let mut map = Map::new();
map.insert("id".into(), Dynamic::from(self.id));
map.insert("description".into(), Dynamic::from(self.description.clone()));
map.insert("quantity".into(), Dynamic::from(self.quantity));
map.insert("unit_price".into(), self.unit_price.to_rhai());
map.insert("total".into(), self.total.to_rhai());
Dynamic::from_map(map)
}
}
// Invoice ToRhai implementation
impl ToRhai for Invoice {
fn to_rhai(&self) -> Dynamic {
let mut map = Map::new();
map.insert("id".into(), Dynamic::from(self.id));
map.insert("customer_id".into(), Dynamic::from(self.customer_id));
map.insert("date".into(), Dynamic::from(self.date.to_string()));
map.insert("due_date".into(), Dynamic::from(self.due_date.to_string()));
map.insert("status".into(), self.status.to_rhai());
// Convert items to an array
let items_array: Array = self.items.iter()
.map(|item| item.to_rhai())
.collect();
map.insert("items".into(), Dynamic::from(items_array));
// Convert payments to an array
let payments_array: Array = self.payments.iter()
.map(|payment| payment.to_rhai())
.collect();
map.insert("payments".into(), Dynamic::from(payments_array));
map.insert("subtotal".into(), self.subtotal.to_rhai());
map.insert("tax".into(), self.tax.to_rhai());
map.insert("total".into(), self.total.to_rhai());
map.insert("created_at".into(), Dynamic::from(self.created_at.to_string()));
map.insert("updated_at".into(), Dynamic::from(self.updated_at.to_string()));
Dynamic::from_map(map)
}
}
// ExchangeRate ToRhai implementation
impl ToRhai for ExchangeRate {
fn to_rhai(&self) -> Dynamic {
let mut map = Map::new();
map.insert("from_currency".into(), Dynamic::from(self.from_currency.clone()));
map.insert("to_currency".into(), Dynamic::from(self.to_currency.clone()));
map.insert("rate".into(), Dynamic::from(self.rate));
map.insert("date".into(), Dynamic::from(self.date.to_string()));
Dynamic::from_map(map)
}
}
//
// Business Module Function Wrappers
//
// Currency Functions
pub fn currency_new(amount: f64, currency_code: &str) -> Currency {
Currency::new(amount, currency_code.to_string())
}
pub fn currency_to_usd(currency: &Currency) -> Result<Dynamic, Box<EvalAltResult>> {
match currency.to_usd() {
Some(usd) => Ok(usd.to_rhai()),
None => Err(Box::new(EvalAltResult::ErrorRuntime(
"Failed to convert currency to USD".into(),
Position::NONE
)))
}
}
pub fn currency_to_currency(currency: &Currency, target_currency: &str) -> Result<Dynamic, Box<EvalAltResult>> {
match currency.to_currency(target_currency) {
Some(converted) => Ok(converted.to_rhai()),
None => Err(Box::new(EvalAltResult::ErrorRuntime(
format!("Failed to convert currency to {}", target_currency).into(),
Position::NONE
)))
}
}
// CurrencyBuilder Functions
pub fn currency_builder_new() -> CurrencyBuilder {
CurrencyBuilder::new()
}
pub fn currency_builder_amount(builder: CurrencyBuilder, amount: f64) -> CurrencyBuilder {
builder.amount(amount)
}
pub fn currency_builder_currency_code(builder: CurrencyBuilder, currency_code: &str) -> CurrencyBuilder {
builder.currency_code(currency_code)
}
pub fn currency_builder_build(builder: CurrencyBuilder) -> Result<Currency, Box<EvalAltResult>> {
builder.build().map_err(|e| {
Box::new(EvalAltResult::ErrorRuntime(
e.into(),
Position::NONE
))
})
}
// ProductComponent Functions
pub fn product_component_new(id: i64, name: &str, description: &str, quantity: i64) -> ProductComponent {
ProductComponent::new(id as u32, name.to_string(), description.to_string(), quantity as i32)
}
pub fn product_component_total_energy_usage(component: &ProductComponent) -> f64 {
component.total_energy_usage()
}
pub fn product_component_total_cost(component: &ProductComponent) -> Currency {
component.total_cost()
}
// ProductComponentBuilder Functions
pub fn product_component_builder_new() -> ProductComponentBuilder {
ProductComponentBuilder::new()
}
pub fn product_component_builder_id(builder: ProductComponentBuilder, id: i64) -> ProductComponentBuilder {
builder.id(id as u32)
}
pub fn product_component_builder_name(builder: ProductComponentBuilder, name: &str) -> ProductComponentBuilder {
builder.name(name)
}
pub fn product_component_builder_description(builder: ProductComponentBuilder, description: &str) -> ProductComponentBuilder {
builder.description(description)
}
pub fn product_component_builder_quantity(builder: ProductComponentBuilder, quantity: i64) -> ProductComponentBuilder {
builder.quantity(quantity as i32)
}
pub fn product_component_builder_energy_usage(builder: ProductComponentBuilder, energy_usage: f64) -> ProductComponentBuilder {
builder.energy_usage(energy_usage)
}
pub fn product_component_builder_cost(builder: ProductComponentBuilder, cost: Currency) -> ProductComponentBuilder {
builder.cost(cost)
}
pub fn product_component_builder_build(builder: ProductComponentBuilder) -> Result<ProductComponent, Box<EvalAltResult>> {
builder.build().map_err(|e| {
Box::new(EvalAltResult::ErrorRuntime(
e.into(),
Position::NONE
))
})
}
// Product Functions
pub fn product_type_product() -> ProductType {
ProductType::Product
}
pub fn product_type_service() -> ProductType {
ProductType::Service
}
pub fn product_status_active() -> ProductStatus {
ProductStatus::Active
}
pub fn product_status_error() -> ProductStatus {
ProductStatus::Error
}
pub fn product_status_end_of_life() -> ProductStatus {
ProductStatus::EndOfLife
}
pub fn product_status_paused() -> ProductStatus {
ProductStatus::Paused
}
pub fn product_status_available() -> ProductStatus {
ProductStatus::Available
}
pub fn product_status_unavailable() -> ProductStatus {
ProductStatus::Unavailable
}
pub fn product_add_component(product: &mut Product, component: ProductComponent) {
product.add_component(component);
}
pub fn product_set_purchase_period(product: &mut Product, purchase_till_days: i64) {
let purchase_till = Utc::now() + Duration::days(purchase_till_days);
product.set_purchase_period(purchase_till);
}
pub fn product_set_active_period(product: &mut Product, active_till_days: i64) {
let active_till = Utc::now() + Duration::days(active_till_days);
product.set_active_period(active_till);
}
pub fn product_is_purchasable(product: &Product) -> bool {
product.is_purchasable()
}
pub fn product_is_active(product: &Product) -> bool {
product.is_active()
}
pub fn product_cost_in_currency(product: &Product, currency_code: &str) -> Result<Dynamic, Box<EvalAltResult>> {
match product.cost_in_currency(currency_code) {
Some(cost) => Ok(cost.to_rhai()),
None => Err(Box::new(EvalAltResult::ErrorRuntime(
format!("Failed to calculate cost in {}", currency_code).into(),
Position::NONE
)))
}
}
pub fn product_cost_in_usd(product: &Product) -> Result<Dynamic, Box<EvalAltResult>> {
match product.cost_in_usd() {
Some(cost) => Ok(cost.to_rhai()),
None => Err(Box::new(EvalAltResult::ErrorRuntime(
"Failed to calculate cost in USD".into(),
Position::NONE
)))
}
}
pub fn product_total_energy_usage(product: &Product) -> f64 {
product.total_energy_usage()
}
pub fn product_components_cost(product: &Product, currency_code: &str) -> Result<Dynamic, Box<EvalAltResult>> {
match product.components_cost(currency_code) {
Some(cost) => Ok(cost.to_rhai()),
None => Err(Box::new(EvalAltResult::ErrorRuntime(
format!("Failed to calculate components cost in {}", currency_code).into(),
Position::NONE
)))
}
}
pub fn product_components_cost_in_usd(product: &Product) -> Result<Dynamic, Box<EvalAltResult>> {
match product.components_cost_in_usd() {
Some(cost) => Ok(cost.to_rhai()),
None => Err(Box::new(EvalAltResult::ErrorRuntime(
"Failed to calculate components cost in USD".into(),
Position::NONE
)))
}
}
// ProductBuilder Functions
pub fn product_builder_new() -> ProductBuilder {
ProductBuilder::new()
}
pub fn product_builder_id(builder: ProductBuilder, id: i64) -> ProductBuilder {
builder.id(id as u32)
}
pub fn product_builder_name(builder: ProductBuilder, name: &str) -> ProductBuilder {
builder.name(name)
}
pub fn product_builder_description(builder: ProductBuilder, description: &str) -> ProductBuilder {
builder.description(description)
}
pub fn product_builder_price(builder: ProductBuilder, price: Currency) -> ProductBuilder {
builder.price(price)
}
pub fn product_builder_type(builder: ProductBuilder, type_: ProductType) -> ProductBuilder {
builder.type_(type_)
}
pub fn product_builder_category(builder: ProductBuilder, category: &str) -> ProductBuilder {
builder.category(category)
}
pub fn product_builder_status(builder: ProductBuilder, status: ProductStatus) -> ProductBuilder {
builder.status(status)
}
pub fn product_builder_max_amount(builder: ProductBuilder, max_amount: i64) -> ProductBuilder {
builder.max_amount(max_amount as u16)
}
pub fn product_builder_validity_days(builder: ProductBuilder, validity_days: i64) -> ProductBuilder {
builder.validity_days(validity_days)
}
pub fn product_builder_add_component(builder: ProductBuilder, component: ProductComponent) -> ProductBuilder {
builder.add_component(component)
}
pub fn product_builder_build(builder: ProductBuilder) -> Result<Product, Box<EvalAltResult>> {
builder.build().map_err(|e| {
Box::new(EvalAltResult::ErrorRuntime(
e.into(),
Position::NONE
))
})
}
// Exchange Rate Service Functions
pub fn exchange_rate_convert(amount: f64, from_currency: &str, to_currency: &str) -> Result<f64, Box<EvalAltResult>> {
match EXCHANGE_RATE_SERVICE.convert(amount, from_currency, to_currency) {
Some(converted) => Ok(converted),
None => Err(Box::new(EvalAltResult::ErrorRuntime(
format!("Failed to convert {} {} to {}", amount, from_currency, to_currency).into(),
Position::NONE
)))
}
}
pub fn exchange_rate_get_rate(from_currency: &str, to_currency: &str) -> Result<f64, Box<EvalAltResult>> {
match EXCHANGE_RATE_SERVICE.get_rate(from_currency, to_currency) {
Some(rate) => Ok(rate),
None => Err(Box::new(EvalAltResult::ErrorRuntime(
format!("Failed to get exchange rate from {} to {}", from_currency, to_currency).into(),
Position::NONE
)))
}
}

View File

@@ -0,0 +1,579 @@
use crate::db::{Model, Storable, DbError, DbResult, IndexKey};
use crate::models::biz::Currency; // Use crate:: for importing from the module
// use super::db::Model; // Removed old Model trait import
use chrono::{DateTime, Utc};
use rhai::{CustomType, TypeBuilder};
use serde::{Deserialize, Serialize};
// use std::collections::HashMap; // Removed unused import
/// SaleStatus represents the status of a sale
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum SaleStatus {
Pending,
Completed,
Cancelled,
}
/// SaleItem represents an item in a sale
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SaleItem {
pub id: u32,
pub sale_id: u32,
pub product_id: Option<u32>, // ID of the product sold (if this is a product sale)
pub service_id: Option<u32>, // ID of the service sold (if this is a service sale)
pub name: String,
pub description: String, // Description of the item
pub comments: String, // Additional comments about the item
pub quantity: i32,
pub unit_price: Currency,
pub subtotal: Currency,
pub tax_rate: f64, // Tax rate as a percentage (e.g., 20.0 for 20%)
pub tax_amount: Currency, // Calculated tax amount
pub active_till: DateTime<Utc>, // after this product no longer active if e.g. a service
}
impl SaleItem {
/// Create a new sale item
pub fn new(
id: u32,
sale_id: u32,
product_id: Option<u32>,
service_id: Option<u32>,
name: String,
description: String,
comments: String,
quantity: i32,
unit_price: Currency,
tax_rate: f64,
active_till: DateTime<Utc>,
) -> Self {
// Validate that either product_id or service_id is provided, but not both
assert!(
(product_id.is_some() && service_id.is_none()) ||
(product_id.is_none() && service_id.is_some()),
"Either product_id or service_id must be provided, but not both"
);
// Calculate subtotal (before tax)
let amount = unit_price.amount * quantity as f64;
let subtotal = Currency::new(
0, // Use 0 as a temporary ID
amount,
unit_price.currency_code.clone()
);
// Calculate tax amount
let tax_amount_value = subtotal.amount * (tax_rate / 100.0);
let tax_amount = Currency::new(
0, // Use 0 as a temporary ID
tax_amount_value,
unit_price.currency_code.clone()
);
Self {
id,
sale_id,
product_id,
service_id,
name,
description,
comments,
quantity,
unit_price,
subtotal,
tax_rate,
tax_amount,
active_till,
}
}
/// Get the total amount including tax
pub fn total_with_tax(&self) -> Currency {
Currency::new(
0, // Use 0 as a temporary ID
self.subtotal.amount + self.tax_amount.amount,
self.subtotal.currency_code.clone()
)
}
}
/// Builder for SaleItem
#[derive(Clone, CustomType)]
pub struct SaleItemBuilder {
id: Option<u32>,
sale_id: Option<u32>,
product_id: Option<u32>,
service_id: Option<u32>,
name: Option<String>,
description: Option<String>,
comments: Option<String>,
quantity: Option<i32>,
unit_price: Option<Currency>,
subtotal: Option<Currency>,
tax_rate: Option<f64>,
tax_amount: Option<Currency>,
active_till: Option<DateTime<Utc>>,
}
impl SaleItemBuilder {
/// Create a new SaleItemBuilder with all fields set to None
pub fn new() -> Self {
Self {
id: None,
sale_id: None,
product_id: None,
service_id: None,
name: None,
description: None,
comments: None,
quantity: None,
unit_price: None,
subtotal: None,
tax_rate: None,
tax_amount: None,
active_till: None,
}
}
/// Set the id
pub fn id(mut self, id: u32) -> Self {
self.id = Some(id);
self
}
/// Set the sale_id
pub fn sale_id(mut self, sale_id: u32) -> Self {
self.sale_id = Some(sale_id);
self
}
/// Set the product_id
pub fn product_id(mut self, product_id: Option<u32>) -> Self {
// If setting product_id, ensure service_id is None
if product_id.is_some() {
self.service_id = None;
}
self.product_id = product_id;
self
}
/// Set the service_id
pub fn service_id(mut self, service_id: Option<u32>) -> Self {
// If setting service_id, ensure product_id is None
if service_id.is_some() {
self.product_id = None;
}
self.service_id = service_id;
self
}
/// Set the name
pub fn name<S: Into<String>>(mut self, name: S) -> Self {
self.name = Some(name.into());
self
}
/// Set the description
pub fn description<S: Into<String>>(mut self, description: S) -> Self {
self.description = Some(description.into());
self
}
/// Set the comments
pub fn comments<S: Into<String>>(mut self, comments: S) -> Self {
self.comments = Some(comments.into());
self
}
/// Set the quantity
pub fn quantity(mut self, quantity: i32) -> Self {
self.quantity = Some(quantity);
self
}
/// Set the unit_price
pub fn unit_price(mut self, unit_price: Currency) -> Self {
self.unit_price = Some(unit_price);
self
}
/// Set the tax_rate
pub fn tax_rate(mut self, tax_rate: f64) -> Self {
self.tax_rate = Some(tax_rate);
self
}
/// Set the active_till
pub fn active_till(mut self, active_till: DateTime<Utc>) -> Self {
self.active_till = Some(active_till);
self
}
/// Build the SaleItem object
pub fn build(self) -> Result<SaleItem, &'static str> {
let unit_price = self.unit_price.ok_or("unit_price is required")?;
let quantity = self.quantity.ok_or("quantity is required")?;
let tax_rate = self.tax_rate.unwrap_or(0.0); // Default to 0% tax if not specified
// Validate that either product_id or service_id is provided, but not both
if self.product_id.is_none() && self.service_id.is_none() {
return Err("Either product_id or service_id must be provided");
}
if self.product_id.is_some() && self.service_id.is_some() {
return Err("Only one of product_id or service_id can be provided");
}
// Calculate subtotal
let amount = unit_price.amount * quantity as f64;
let subtotal = Currency::new(
0, // Use 0 as a temporary ID
amount,
unit_price.currency_code.clone()
);
// Calculate tax amount
let tax_amount_value = subtotal.amount * (tax_rate / 100.0);
let tax_amount = Currency::new(
0, // Use 0 as a temporary ID
tax_amount_value,
unit_price.currency_code.clone()
);
Ok(SaleItem {
id: self.id.ok_or("id is required")?,
sale_id: self.sale_id.ok_or("sale_id is required")?,
product_id: self.product_id,
service_id: self.service_id,
name: self.name.ok_or("name is required")?,
description: self.description.unwrap_or_default(),
comments: self.comments.unwrap_or_default(),
quantity,
unit_price,
subtotal,
tax_rate,
tax_amount,
active_till: self.active_till.ok_or("active_till is required")?,
})
}
}
/// Sale represents a sale of products or services
#[derive(Debug, Clone, Serialize, Deserialize, CustomType)]
pub struct Sale {
pub id: u32,
pub customer_id: u32, // ID of the customer making the purchase
pub subtotal_amount: Currency, // Total before tax
pub tax_amount: Currency, // Total tax
pub total_amount: Currency, // Total including tax
pub status: SaleStatus,
pub service_id: Option<u32>, // ID of the service created from this sale (if applicable)
pub sale_date: DateTime<Utc>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
pub items: Vec<SaleItem>,
}
// Removed old Model trait implementation
impl Sale {
/// Create a new sale with default timestamps
pub fn new(
id: u32,
customer_id: u32,
currency_code: String,
status: SaleStatus,
) -> Self {
let now = Utc::now();
let zero_currency = Currency::new(
0, // Use 0 as a temporary ID
0.0,
currency_code.clone()
);
Self {
id,
customer_id,
subtotal_amount: zero_currency.clone(),
tax_amount: zero_currency.clone(),
total_amount: zero_currency,
status,
service_id: None,
sale_date: now,
created_at: now,
updated_at: now,
items: Vec::new(),
}
}
/// Add an item to the sale and update the total amount
pub fn add_item(&mut self, item: SaleItem) {
// Make sure the item's sale_id matches this sale
assert_eq!(self.id, item.sale_id, "Item sale_id must match sale id");
// Update the amounts
if self.items.is_empty() {
// First item, initialize the amounts with the same currency
self.subtotal_amount = Currency::new(
0, // Use 0 as a temporary ID
item.subtotal.amount,
item.subtotal.currency_code.clone()
);
self.tax_amount = Currency::new(
0, // Use 0 as a temporary ID
item.tax_amount.amount,
item.tax_amount.currency_code.clone()
);
self.total_amount = Currency::new(
0, // Use 0 as a temporary ID
item.subtotal.amount + item.tax_amount.amount,
item.subtotal.currency_code.clone()
);
} else {
// Add to the existing totals
// (Assumes all items have the same currency)
self.subtotal_amount.amount += item.subtotal.amount;
self.tax_amount.amount += item.tax_amount.amount;
self.total_amount.amount = self.subtotal_amount.amount + self.tax_amount.amount;
}
// Add the item to the list
self.items.push(item);
// Update the sale timestamp
self.updated_at = Utc::now();
}
/// Recalculate all totals based on items
pub fn recalculate_totals(&mut self) {
if self.items.is_empty() {
return;
}
// Get the currency code from the first item
let currency_code = self.items[0].subtotal.currency_code.clone();
// Calculate the totals
let mut subtotal = 0.0;
let mut tax_total = 0.0;
for item in &self.items {
subtotal += item.subtotal.amount;
tax_total += item.tax_amount.amount;
}
// Update the amounts
self.subtotal_amount = Currency::new(
0, // Use 0 as a temporary ID
subtotal,
currency_code.clone()
);
self.tax_amount = Currency::new(
0, // Use 0 as a temporary ID
tax_total,
currency_code.clone()
);
self.total_amount = Currency::new(
0, // Use 0 as a temporary ID
subtotal + tax_total,
currency_code
);
// Update the timestamp
self.updated_at = Utc::now();
}
/// Update the status of the sale
pub fn update_status(&mut self, status: SaleStatus) {
self.status = status;
self.updated_at = Utc::now();
}
/// Link this sale to an existing service
pub fn link_to_service(&mut self, service_id: u32) {
self.service_id = Some(service_id);
self.updated_at = Utc::now();
}
}
/// Builder for Sale
#[derive(Clone, CustomType)]
pub struct SaleBuilder {
id: Option<u32>,
company_id: Option<u32>,
customer_id: Option<u32>,
buyer_name: Option<String>,
buyer_email: Option<String>,
subtotal_amount: Option<Currency>,
tax_amount: Option<Currency>,
total_amount: Option<Currency>,
status: Option<SaleStatus>,
service_id: Option<u32>,
sale_date: Option<DateTime<Utc>>,
created_at: Option<DateTime<Utc>>,
updated_at: Option<DateTime<Utc>>,
items: Vec<SaleItem>,
currency_code: Option<String>,
}
impl SaleBuilder {
/// Create a new SaleBuilder with all fields set to None
pub fn new() -> Self {
Self {
id: None,
company_id: None,
customer_id: None,
buyer_name: None,
buyer_email: None,
subtotal_amount: None,
tax_amount: None,
total_amount: None,
status: None,
service_id: None,
sale_date: None,
created_at: None,
updated_at: None,
items: Vec::new(),
currency_code: None,
}
}
/// Set the id
pub fn id(mut self, id: u32) -> Self {
self.id = Some(id);
self
}
/// Set the company_id
pub fn company_id(mut self, company_id: u32) -> Self {
self.company_id = Some(company_id);
self
}
/// Set the customer_id
pub fn customer_id(mut self, customer_id: u32) -> Self {
self.customer_id = Some(customer_id);
self
}
/// Set the buyer_name
pub fn buyer_name<S: Into<String>>(mut self, buyer_name: S) -> Self {
self.buyer_name = Some(buyer_name.into());
self
}
/// Set the buyer_email
pub fn buyer_email<S: Into<String>>(mut self, buyer_email: S) -> Self {
self.buyer_email = Some(buyer_email.into());
self
}
/// Set the currency_code
pub fn currency_code<S: Into<String>>(mut self, currency_code: S) -> Self {
self.currency_code = Some(currency_code.into());
self
}
/// Set the status
pub fn status(mut self, status: SaleStatus) -> Self {
self.status = Some(status);
self
}
/// Set the service_id
pub fn service_id(mut self, service_id: u32) -> Self {
self.service_id = Some(service_id);
self
}
/// Set the sale_date
pub fn sale_date(mut self, sale_date: DateTime<Utc>) -> Self {
self.sale_date = Some(sale_date);
self
}
/// Add an item to the sale
pub fn add_item(mut self, item: SaleItem) -> Self {
self.items.push(item);
self
}
/// Build the Sale object
pub fn build(self) -> Result<Sale, &'static str> {
let now = Utc::now();
let id = self.id.ok_or("id is required")?;
let currency_code = self.currency_code.ok_or("currency_code is required")?;
// Initialize with empty amounts
let mut subtotal_amount = Currency::new(
0, // Use 0 as a temporary ID
0.0,
currency_code.clone()
);
let mut tax_amount = Currency::new(
0, // Use 0 as a temporary ID
0.0,
currency_code.clone()
);
let mut total_amount = Currency::new(
0, // Use 0 as a temporary ID
0.0,
currency_code.clone()
);
// Calculate amounts from items
for item in &self.items {
// Make sure the item's sale_id matches this sale
if item.sale_id != id {
return Err("Item sale_id must match sale id");
}
subtotal_amount.amount += item.subtotal.amount;
tax_amount.amount += item.tax_amount.amount;
}
// Calculate total amount
total_amount.amount = subtotal_amount.amount + tax_amount.amount;
Ok(Sale {
id,
customer_id: self.customer_id.ok_or("customer_id is required")?,
subtotal_amount: self.subtotal_amount.unwrap_or(subtotal_amount),
tax_amount: self.tax_amount.unwrap_or(tax_amount),
total_amount: self.total_amount.unwrap_or(total_amount),
status: self.status.ok_or("status is required")?,
service_id: self.service_id,
sale_date: self.sale_date.unwrap_or(now),
created_at: self.created_at.unwrap_or(now),
updated_at: self.updated_at.unwrap_or(now),
items: self.items,
})
}
}
// Implement Storable trait
impl Storable for Sale {}
// Implement Model trait
impl Model for Sale {
fn get_id(&self) -> u32 {
self.id
}
fn db_prefix() -> &'static str {
"sale"
}
fn db_keys(&self) -> Vec<IndexKey> {
let mut keys = Vec::new();
// Add an index for customer_id
keys.push(IndexKey {
name: "customer_id",
value: self.customer_id.to_string(),
});
keys
}
}

View File

@@ -0,0 +1,478 @@
use crate::models::biz::Currency; // Use crate:: for importing from the module
use crate::db::{Model, Storable, DbError, DbResult, IndexKey}; // Import Model trait and IndexKey from db module
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
/// BillingFrequency represents the frequency of billing for a service
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum BillingFrequency {
Hourly,
Daily,
Weekly,
Monthly,
Yearly,
}
/// ServiceStatus represents the status of a service
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ServiceStatus {
Active,
Paused,
Cancelled,
Completed,
}
/// ServiceItem represents an item in a service
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ServiceItem {
pub id: u32,
pub service_id: u32,
pub product_id: u32,
pub name: String,
pub description: String, // Description of the service item
pub comments: String, // Additional comments about the service item
pub quantity: i32,
pub unit_price: Currency,
pub subtotal: Currency,
pub active_till: DateTime<Utc>,
}
impl ServiceItem {
/// Create a new service item
pub fn new(
id: u32,
service_id: u32,
product_id: u32,
name: String,
description: String,
comments: String,
quantity: i32,
unit_price: Currency,
active_till: DateTime<Utc>,
) -> Self {
// Calculate subtotal
let amount = unit_price.amount * quantity as f64;
let subtotal = Currency::new(
0, // Use 0 as a temporary ID
amount,
unit_price.currency_code.clone()
);
Self {
id,
service_id,
product_id,
name,
description,
comments,
quantity,
unit_price,
subtotal,
active_till,
}
}
/// Calculate the subtotal based on quantity and unit price
pub fn calculate_subtotal(&mut self) {
let amount = self.unit_price.amount * self.quantity as f64;
self.subtotal = Currency::new(
0, // Use 0 as a temporary ID
amount,
self.unit_price.currency_code.clone()
);
}
}
/// Builder for ServiceItem
pub struct ServiceItemBuilder {
id: Option<u32>,
service_id: Option<u32>,
product_id: Option<u32>,
name: Option<String>,
description: Option<String>,
comments: Option<String>,
quantity: Option<i32>,
unit_price: Option<Currency>,
subtotal: Option<Currency>,
tax_rate: Option<f64>,
tax_amount: Option<Currency>,
is_taxable: Option<bool>,
active_till: Option<DateTime<Utc>>,
}
impl ServiceItemBuilder {
/// Create a new ServiceItemBuilder with all fields set to None
pub fn new() -> Self {
Self {
id: None,
service_id: None,
product_id: None,
name: None,
description: None,
comments: None,
quantity: None,
unit_price: None,
subtotal: None,
tax_rate: None,
tax_amount: None,
is_taxable: None,
active_till: None,
}
}
/// Set the id
pub fn id(mut self, id: u32) -> Self {
self.id = Some(id);
self
}
/// Set the service_id
pub fn service_id(mut self, service_id: u32) -> Self {
self.service_id = Some(service_id);
self
}
/// Set the product_id
pub fn product_id(mut self, product_id: u32) -> Self {
self.product_id = Some(product_id);
self
}
/// Set the name
pub fn name<S: Into<String>>(mut self, name: S) -> Self {
self.name = Some(name.into());
self
}
/// Set the description
pub fn description<S: Into<String>>(mut self, description: S) -> Self {
self.description = Some(description.into());
self
}
/// Set the comments
pub fn comments<S: Into<String>>(mut self, comments: S) -> Self {
self.comments = Some(comments.into());
self
}
/// Set the quantity
pub fn quantity(mut self, quantity: i32) -> Self {
self.quantity = Some(quantity);
self
}
/// Set the unit_price
pub fn unit_price(mut self, unit_price: Currency) -> Self {
self.unit_price = Some(unit_price);
self
}
/// Set the tax_rate
pub fn tax_rate(mut self, tax_rate: f64) -> Self {
self.tax_rate = Some(tax_rate);
self
}
/// Set is_taxable
pub fn is_taxable(mut self, is_taxable: bool) -> Self {
self.is_taxable = Some(is_taxable);
self
}
/// Set the active_till
pub fn active_till(mut self, active_till: DateTime<Utc>) -> Self {
self.active_till = Some(active_till);
self
}
/// Build the ServiceItem object
pub fn build(self) -> Result<ServiceItem, &'static str> {
let unit_price = self.unit_price.ok_or("unit_price is required")?;
let quantity = self.quantity.ok_or("quantity is required")?;
let tax_rate = self.tax_rate.unwrap_or(0.0);
let is_taxable = self.is_taxable.unwrap_or(false);
// Calculate subtotal
let amount = unit_price.amount * quantity as f64;
let subtotal = Currency::new(
0, // Use 0 as a temporary ID
amount,
unit_price.currency_code.clone()
);
// Calculate tax amount if taxable
let tax_amount = if is_taxable {
Currency::new(
0, // Use 0 as a temporary ID
subtotal.amount * tax_rate,
unit_price.currency_code.clone()
)
} else {
Currency::new(
0, // Use 0 as a temporary ID
0.0,
unit_price.currency_code.clone()
)
};
Ok(ServiceItem {
id: self.id.ok_or("id is required")?,
service_id: self.service_id.ok_or("service_id is required")?,
product_id: self.product_id.ok_or("product_id is required")?,
name: self.name.ok_or("name is required")?,
description: self.description.unwrap_or_default(),
comments: self.comments.unwrap_or_default(),
quantity,
unit_price,
subtotal,
active_till: self.active_till.ok_or("active_till is required")?,
})
}
}
/// Service represents a recurring service with billing frequency
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Service {
pub id: u32,
pub customer_id: u32,
pub total_amount: Currency,
pub status: ServiceStatus,
pub billing_frequency: BillingFrequency,
pub service_date: DateTime<Utc>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
pub items: Vec<ServiceItem>,
}
impl Service {
/// Create a new service with default timestamps
pub fn new(
id: u32,
customer_id: u32,
currency_code: String,
status: ServiceStatus,
billing_frequency: BillingFrequency,
) -> Self {
let now = Utc::now();
Self {
id,
customer_id,
total_amount: Currency::new(0, 0.0, currency_code),
status,
billing_frequency,
service_date: now,
created_at: now,
updated_at: now,
items: Vec::new(),
}
}
/// Add an item to the service and update the total amount
pub fn add_item(&mut self, item: ServiceItem) {
// Make sure the item's service_id matches this service
assert_eq!(self.id, item.service_id, "Item service_id must match service id");
// Update the total amount
if self.items.is_empty() {
// First item, initialize the total amount with the same currency
self.total_amount = Currency::new(
0, // Use 0 as a temporary ID
item.subtotal.amount ,
item.subtotal.currency_code.clone()
);
} else {
// Add to the existing total
// (Assumes all items have the same currency)
self.total_amount.amount += item.subtotal.amount;
}
// Add the item to the list
self.items.push(item);
// Update the service timestamp
self.updated_at = Utc::now();
}
/// Calculate the total amount based on all items
pub fn calculate_total(&mut self) {
if self.items.is_empty() {
return;
}
// Get the currency code from the first item
let currency_code = self.items[0].subtotal.currency_code.clone();
// Calculate the total amount
let mut total = 0.0;
for item in &self.items {
total += item.subtotal.amount;
}
// Update the total amount
self.total_amount = Currency::new(
0, // Use 0 as a temporary ID
total,
currency_code
);
// Update the service timestamp
self.updated_at = Utc::now();
}
/// Update the status of the service
pub fn update_status(&mut self, status: ServiceStatus) {
self.status = status;
self.updated_at = Utc::now();
}
}
/// Builder for Service
pub struct ServiceBuilder {
id: Option<u32>,
customer_id: Option<u32>,
total_amount: Option<Currency>,
status: Option<ServiceStatus>,
billing_frequency: Option<BillingFrequency>,
service_date: Option<DateTime<Utc>>,
created_at: Option<DateTime<Utc>>,
updated_at: Option<DateTime<Utc>>,
items: Vec<ServiceItem>,
currency_code: Option<String>,
}
impl ServiceBuilder {
/// Create a new ServiceBuilder with all fields set to None
pub fn new() -> Self {
Self {
id: None,
customer_id: None,
total_amount: None,
status: None,
billing_frequency: None,
service_date: None,
created_at: None,
updated_at: None,
items: Vec::new(),
currency_code: None,
}
}
/// Set the id
pub fn id(mut self, id: u32) -> Self {
self.id = Some(id);
self
}
/// Set the customer_id
pub fn customer_id(mut self, customer_id: u32) -> Self {
self.customer_id = Some(customer_id);
self
}
/// Set the currency_code
pub fn currency_code<S: Into<String>>(mut self, currency_code: S) -> Self {
self.currency_code = Some(currency_code.into());
self
}
/// Set the status
pub fn status(mut self, status: ServiceStatus) -> Self {
self.status = Some(status);
self
}
/// Set the billing_frequency
pub fn billing_frequency(mut self, billing_frequency: BillingFrequency) -> Self {
self.billing_frequency = Some(billing_frequency);
self
}
/// Set the service_date
pub fn service_date(mut self, service_date: DateTime<Utc>) -> Self {
self.service_date = Some(service_date);
self
}
/// Add an item to the service
pub fn add_item(mut self, item: ServiceItem) -> Self {
self.items.push(item);
self
}
/// Build the Service object
pub fn build(self) -> Result<Service, &'static str> {
let now = Utc::now();
let id = self.id.ok_or("id is required")?;
let currency_code = self.currency_code.ok_or("currency_code is required")?;
// Initialize with empty total amount
let mut total_amount = Currency::new(
0, // Use 0 as a temporary ID
0.0,
currency_code.clone()
);
// Calculate total amount from items
for item in &self.items {
// Make sure the item's service_id matches this service
if item.service_id != id {
return Err("Item service_id must match service id");
}
if total_amount.amount == 0.0 {
// First item, initialize the total amount with the same currency
total_amount = Currency::new(
0, // Use 0 as a temporary ID
item.subtotal.amount,
item.subtotal.currency_code.clone()
);
} else {
// Add to the existing total
// (Assumes all items have the same currency)
total_amount.amount += item.subtotal.amount ;
}
}
Ok(Service {
id,
customer_id: self.customer_id.ok_or("customer_id is required")?,
total_amount: self.total_amount.unwrap_or(total_amount),
status: self.status.ok_or("status is required")?,
billing_frequency: self.billing_frequency.ok_or("billing_frequency is required")?,
service_date: self.service_date.unwrap_or(now),
created_at: self.created_at.unwrap_or(now),
updated_at: self.updated_at.unwrap_or(now),
items: self.items,
})
}
}
// Implement Storable trait
impl Storable for Service {
}
// Implement Model trait
impl Model for Service {
fn get_id(&self) -> u32 {
self.id
}
fn db_prefix() -> &'static str {
"service"
}
fn db_keys(&self) -> Vec<IndexKey> {
let mut keys = Vec::new();
// Add an index for customer_id
keys.push(IndexKey {
name: "customer_id",
value: self.customer_id.to_string(),
});
keys
}
}

View File

@@ -0,0 +1,35 @@
# Circles Core Models
This directory contains the core data structures used in the herolib circles module. These models serve as the foundation for the circles functionality, providing essential data structures for circles and name management.
## Overview
The core models implement the Serde traits (Serialize/Deserialize) and crate database traits (Storable, SledModel), which allows them to be stored and retrieved using the generic SledDB implementation. Each model provides:
- A struct definition with appropriate fields
- Serde serialization through derive macros
- Methods for database integration through the SledModel trait
- Utility methods for common operations
## Core Models
### Circle (`circle.rs`)
The Circle model represents a collection of members (users or other circles):
- **Circle**: Main struct with fields for identification and member management
- **Member**: Represents a member of a circle with personal information and role
- **Role**: Enum for possible member roles (Admin, Stakeholder, Member, Contributor, Guest)
### Name (`name.rs`)
The Name model provides DNS record management:
- **Name**: Main struct for domain management with records and administrators
- **Record**: Represents a DNS record with name, text, category, and addresses
- **RecordType**: Enum for DNS record types (A, AAAA, CNAME, MX, etc.)
## Usage
These models are used by the circles module to manage circles and DNS records. They are typically accessed through the database handlers that implement the generic SledDB interface.

View File

@@ -0,0 +1,43 @@
use serde::{Deserialize, Serialize};
use crate::db::{Model, Storable, DbError, DbResult};
use std::collections::HashMap;
/// Circle represents a collection of members (users or other circles)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Circle {
pub id: u32, // unique id
pub name: String, // name of the circle
pub description: String, // optional description
}
impl Circle {
/// Create a new circle
pub fn new(id: u32, name: String, description: String) -> Self {
Self {
id,
name,
description,
}
}
/// Returns a map of index keys for this circle
pub fn index_keys(&self) -> HashMap<String, String> {
let mut keys = HashMap::new();
keys.insert("name".to_string(), self.name.clone());
keys
}
}
// Implement Storable trait
impl Storable for Circle {}
// Implement Model trait
impl Model for Circle {
fn get_id(&self) -> u32 {
self.id
}
fn db_prefix() -> &'static str {
"circle"
}
}

View File

@@ -0,0 +1,9 @@
pub mod circle;
pub mod name;
// Re-export all model types for convenience
pub use circle::{Circle, Member, Role};
pub use name::{Name, Record, RecordType};
// Re-export database components
pub use crate::db::{DB, DBBuilder, Model, Storable, DbError, DbResult, ModelRegistration, ModelRegistrar};

View File

@@ -0,0 +1,83 @@
use serde::{Deserialize, Serialize};
use crate::db::{Model, Storable, DbError, DbResult};
use std::collections::HashMap;
/// Role represents the role of a member in a circle
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum Role {
Admin,
Stakeholder,
Member,
Contributor,
Guest,
}
/// Member represents a member of a circle
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Member {
pub id: u32, // unique id
pub emails: Vec<String>, // list of emails
pub name: String, // name of the member
pub description: String, // optional description
pub role: Role, // role of the member in the circle
pub contact_ids: Vec<u32>, // IDs of contacts linked to this member
pub wallet_ids: Vec<u32>, // IDs of wallets owned by this member
}
impl Member {
/// Create a new member
pub fn new(id: u32, name: String, description: String, role: Role) -> Self {
Self {
id,
emails: Vec::new(),
name,
description,
role,
contact_ids: Vec::new(),
wallet_ids: Vec::new(),
}
}
/// Add an email to this member
pub fn add_email(&mut self, email: String) {
if !self.emails.contains(&email) {
self.emails.push(email);
}
}
/// Link a contact to this member
pub fn link_contact(&mut self, contact_id: u32) {
if !self.contact_ids.contains(&contact_id) {
self.contact_ids.push(contact_id);
}
}
/// Link a wallet to this member
pub fn link_wallet(&mut self, wallet_id: u32) {
if !self.wallet_ids.contains(&wallet_id) {
self.wallet_ids.push(wallet_id);
}
}
/// Returns a map of index keys for this member
pub fn index_keys(&self) -> HashMap<String, String> {
let mut keys = HashMap::new();
keys.insert("name".to_string(), self.name.clone());
keys
}
}
// Implement Storable trait
impl Storable for Member {
}
// Implement Model trait
impl Model for Member {
fn get_id(&self) -> u32 {
self.id
}
fn db_prefix() -> &'static str {
"member"
}
}

View File

@@ -0,0 +1,13 @@
pub mod circle;
pub mod member;
pub mod name;
pub mod wallet;
// Re-export all model types for convenience
pub use circle::Circle;
pub use member::{Member, Role};
pub use name::{Name, Record, RecordType};
pub use wallet::{Wallet, Asset};
// Re-export database components from db module
pub use crate::db::{DB, DBBuilder, Model, Storable, DbError, DbResult, ModelRegistration, ModelRegistrar};

View File

@@ -0,0 +1,73 @@
use serde::{Deserialize, Serialize};
use crate::db::{Model, Storable, DbError, DbResult};
/// Record types for a DNS record
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum RecordType {
A,
AAAA,
CNAME,
MX,
NS,
PTR,
SOA,
SRV,
TXT,
}
/// Represents a DNS record
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Record {
pub name: String, // name of the record
pub text: String,
pub category: RecordType, // role of the member in the circle
pub addr: Vec<String>, // the multiple ipaddresses for this record
}
/// Name represents a DNS domain and its records
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Name {
pub id: u32, // unique id
pub domain: String,
pub description: String, // optional description
pub records: Vec<Record>, // DNS records
pub admins: Vec<String>, // pubkeys who can change it
}
impl Name {
/// Create a new domain name entry
pub fn new(id: u32, domain: String, description: String) -> Self {
Self {
id,
domain,
description,
records: Vec::new(),
admins: Vec::new(),
}
}
/// Add a record to this domain name
pub fn add_record(&mut self, record: Record) {
self.records.push(record);
}
/// Add an admin pubkey
pub fn add_admin(&mut self, pubkey: String) {
self.admins.push(pubkey);
}
}
// Implement Storable trait
impl Storable for Name {
}
// Implement Model trait
impl Model for Name {
fn get_id(&self) -> u32 {
self.id
}
fn db_prefix() -> &'static str {
"name"
}
}

View File

@@ -0,0 +1,85 @@
use serde::{Deserialize, Serialize};
use crate::db::{Model, Storable, DbError, DbResult};
use std::collections::HashMap;
/// Asset represents a cryptocurrency asset in a wallet
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Asset {
pub name: String, // Asset name (e.g., "USDC")
pub amount: f64, // Amount of the asset
}
impl Asset {
/// Create a new asset
pub fn new(name: String, amount: f64) -> Self {
Self {
name,
amount,
}
}
}
/// Wallet represents a cryptocurrency wallet
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Wallet {
pub id: u32, // unique id
pub name: String, // name of the wallet
pub description: String, // optional description
pub blockchain_name: String, // name of the blockchain
pub pubkey: String, // public key of the wallet
pub assets: Vec<Asset>, // assets in the wallet
}
impl Wallet {
/// Create a new wallet
pub fn new(id: u32, name: String, description: String, blockchain_name: String, pubkey: String) -> Self {
Self {
id,
name,
description,
blockchain_name,
pubkey,
assets: Vec::new(),
}
}
/// Set an asset in the wallet (replaces if exists, adds if not)
pub fn set_asset(&mut self, name: String, amount: f64) {
// Check if the asset already exists
if let Some(asset) = self.assets.iter_mut().find(|a| a.name == name) {
// Update the amount
asset.amount = amount;
} else {
// Add a new asset
self.assets.push(Asset::new(name, amount));
}
}
/// Get the total value of all assets in the wallet
pub fn total_value(&self) -> f64 {
self.assets.iter().map(|a| a.amount).sum()
}
/// Returns a map of index keys for this wallet
pub fn index_keys(&self) -> HashMap<String, String> {
let mut keys = HashMap::new();
keys.insert("name".to_string(), self.name.clone());
keys.insert("blockchain".to_string(), self.blockchain_name.clone());
keys
}
}
// Implement Storable trait
impl Storable for Wallet {
}
// Implement Model trait
impl Model for Wallet {
fn get_id(&self) -> u32 {
self.id
}
fn db_prefix() -> &'static str {
"wallet"
}
}

View File

@@ -0,0 +1,355 @@
# Corporate Governance Module
This directory contains the core data structures used for corporate governance functionality. These models serve as the foundation for managing companies, shareholders, meetings, voting, resolutions, committees, and more in any organizational context.
## Overview
The governance models implement the Serde traits (Serialize/Deserialize) and database traits (Storable, SledModel), which allows them to be stored and retrieved using the generic SledDB implementation. Each model provides:
- A struct definition with appropriate fields
- Serde serialization through derive macros
- Methods for database integration through the SledModel trait
- Utility methods for common operations
## Core Models
### Company (`company.rs`)
The Company model represents a company entity with its basic information:
- **Company**: Main struct with fields for company information
- Basic details: name, registration number, incorporation date
- Contact information: email, phone, website, address
- Business information: business type, industry, description
- Status tracking: current status, timestamps
- **CompanyStatus**: Enum for possible company statuses (Active, Inactive, Suspended)
- **BusinessType**: String-based type with validation for business types (Corporation, Partnership, LLC, etc.)
Key methods:
- `add_shareholder()`: Add a shareholder to the company
- `link_to_circle()`: Link the company to a Circle for access control
- `link_to_customer()`: Link the company to a Customer in the biz module
- `get_resolutions()`: Get all resolutions for this company
### Shareholder (`shareholder.rs`)
The Shareholder model represents a shareholder of a company:
- **Shareholder**: Main struct with fields for shareholder information
- Identifiers: id, company_id, user_id
- Ownership details: shares, percentage
- Type and timestamps: shareholder type, since date, created/updated timestamps
- **ShareholderType**: Enum for possible shareholder types (Individual, Corporate)
Key methods:
- `update_shares()`: Update the shares owned by this shareholder
### Meeting (`meeting.rs`)
The Meeting model represents a board meeting of a company:
- **Meeting**: Main struct with fields for meeting information
- Basic details: id, company_id, title, date, location, description
- Status and content: meeting status, minutes
- Timestamps and attendees: created/updated timestamps, list of attendees
- **Attendee**: Represents an attendee of a meeting
- Details: id, meeting_id, user_id, name, role, status, created timestamp
- **MeetingStatus**: Enum for possible meeting statuses (Scheduled, Completed, Cancelled)
- **AttendeeRole**: Enum for possible attendee roles (Coordinator, Member, Secretary, etc.)
- **AttendeeStatus**: Enum for possible attendee statuses (Confirmed, Pending, Declined)
Key methods:
- `add_attendee()`: Add an attendee to the meeting
- `update_status()`: Update the status of the meeting
- `update_minutes()`: Update the meeting minutes
- `find_attendee_by_user_id()`: Find an attendee by user ID
- `confirmed_attendees()`: Get all confirmed attendees
- `link_to_event()`: Link the meeting to a Calendar Event
- `get_resolutions()`: Get all resolutions discussed in this meeting
### User (`user.rs`)
The User model represents a user in the governance system:
- **User**: Main struct with fields for user information
- Basic details: id, name, email, password
- Role information: company, role
- Timestamps: created/updated timestamps
### Vote (`vote.rs`)
The Vote model represents a voting item for corporate decision-making:
- **Vote**: Main struct with fields for vote information
- Basic details: id, company_id, title, description
- Timing: start_date, end_date
- Status and timestamps: vote status, created/updated timestamps
- Options and results: list of vote options, list of ballots, private group
- **VoteOption**: Represents an option in a vote
- Details: id, vote_id, text, count, min_valid
- **Ballot**: Represents a ballot cast by a user
- Details: id, vote_id, user_id, vote_option_id, shares_count, created timestamp
- **VoteStatus**: Enum for possible vote statuses (Open, Closed, Cancelled)
Key methods:
- `add_option()`: Add a voting option to this vote
- `add_ballot()`: Add a ballot to this vote
- `get_resolution()`: Get the resolution associated with this vote
### Resolution (`resolution.rs`)
The Resolution model represents a board resolution:
- **Resolution**: Main struct with fields for resolution information
- Identifiers: id, company_id, meeting_id, vote_id
- Content: title, description, text
- Status and tracking: resolution status, proposed_by, proposed_at, approved_at, rejected_at
- Timestamps and approvals: created/updated timestamps, list of approvals
- **Approval**: Represents an approval of a resolution by a board member
- Details: id, resolution_id, user_id, name, approved, comments, created timestamp
- **ResolutionStatus**: Enum for possible resolution statuses (Draft, Proposed, Approved, Rejected, Withdrawn)
Key methods:
- `propose()`: Propose the resolution
- `approve()`: Approve the resolution
- `reject()`: Reject the resolution
- `withdraw()`: Withdraw the resolution
- `add_approval()`: Add an approval to the resolution
- `find_approval_by_user_id()`: Find an approval by user ID
- `get_approvals()`: Get all approvals
- `approval_count()`: Get approval count
- `rejection_count()`: Get rejection count
- `link_to_meeting()`: Link this resolution to a meeting
- `link_to_vote()`: Link this resolution to a vote
- `get_meeting()`: Get the meeting associated with this resolution
- `get_vote()`: Get the vote associated with this resolution
### Committee (`committee.rs`)
The Committee model represents a board committee:
- **Committee**: Main struct with fields for committee information
- Basic details: id, company_id, name, description, purpose
- Integration: circle_id
- Timestamps and members: created/updated timestamps, list of members
- **CommitteeMember**: Represents a member of a committee
- Details: id, committee_id, user_id, name, role, since, created timestamp
- **CommitteeRole**: Enum for possible committee roles (Chair, ViceChair, Secretary, Member, Advisor, Observer)
Key methods:
- `add_member()`: Add a member to the committee
- `find_member_by_user_id()`: Find a member by user ID
- `remove_member()`: Remove a member from the committee
- `link_to_circle()`: Link this committee to a Circle for access control
- `get_member_users()`: Get all users who are members of this committee
## Model Relationships
The following diagram illustrates the relationships between the governance models:
```mermaid
graph TD
Company --> |has many| Shareholder
Company --> |has many| Meeting
Company --> |has many| Resolution
Company --> |has many| Vote
Company --> |has many| Committee
Meeting --> |has many| Attendee
Attendee --> |is a| User
Resolution --> |can be linked to| Meeting
Resolution --> |can be linked to| Vote
Resolution --> |has many| Approval
Vote --> |has many| VoteOption
Vote --> |has many| Ballot
Ballot --> |cast by| User
Committee --> |has many| CommitteeMember
CommitteeMember --> |is a| User
```
## Key Relationships
- **Company-Shareholder**: A company has multiple shareholders who own shares in the company
- **Company-Meeting**: A company holds multiple meetings for governance purposes
- **Company-Resolution**: A company creates resolutions that need to be approved
- **Company-Vote**: A company conducts votes on various matters
- **Company-Committee**: A company can have multiple committees for specialized governance functions
- **Meeting-Resolution**: Resolutions can be discussed and approved in meetings
- **Resolution-Vote**: Resolutions can be subject to formal voting
- **User-Governance**: Users participate in governance as shareholders, meeting attendees, committee members, and by casting votes
## Integration with Other Modules
The governance module integrates with other modules in the system:
### Integration with Biz Module
- **Company-Customer**: Companies can be linked to customers in the biz module
- **Company-Contract**: Companies can be linked to contracts in the biz module
- **Shareholder-Customer**: Shareholders can be linked to customers in the biz module
- **Meeting-Invoice**: Meetings can be linked to invoices for expense tracking
### Integration with MCC Module
- **Meeting-Calendar/Event**: Meetings can be linked to calendar events in the mcc module
- **User-Contact**: Users can be linked to contacts in the mcc module
- **Vote-Message**: Votes can be linked to messages for notifications
### Integration with Circle Module
- **Company-Circle**: Companies can be linked to circles for group-based access control
- **User-Member**: Users can be linked to members for role-based permissions
## Detailed Data Model
A more detailed class diagram showing the fields and methods of each model:
```mermaid
classDiagram
class Company {
+u32 id
+String name
+String registration_number
+DateTime incorporation_date
+String fiscal_year_end
+String email
+String phone
+String website
+String address
+BusinessType business_type
+String industry
+String description
+CompanyStatus status
+DateTime created_at
+DateTime updated_at
+add_shareholder()
+link_to_circle()
+link_to_customer()
+get_resolutions()
}
class Shareholder {
+u32 id
+u32 company_id
+u32 user_id
+String name
+f64 shares
+f64 percentage
+ShareholderType type_
+DateTime since
+DateTime created_at
+DateTime updated_at
+update_shares()
}
class Meeting {
+u32 id
+u32 company_id
+String title
+DateTime date
+String location
+String description
+MeetingStatus status
+String minutes
+DateTime created_at
+DateTime updated_at
+Vec~Attendee~ attendees
+add_attendee()
+update_status()
+update_minutes()
+find_attendee_by_user_id()
+confirmed_attendees()
+link_to_event()
+get_resolutions()
}
class User {
+u32 id
+String name
+String email
+String password
+String company
+String role
+DateTime created_at
+DateTime updated_at
}
class Vote {
+u32 id
+u32 company_id
+String title
+String description
+DateTime start_date
+DateTime end_date
+VoteStatus status
+DateTime created_at
+DateTime updated_at
+Vec~VoteOption~ options
+Vec~Ballot~ ballots
+Vec~u32~ private_group
+add_option()
+add_ballot()
+get_resolution()
}
class Resolution {
+u32 id
+u32 company_id
+Option~u32~ meeting_id
+Option~u32~ vote_id
+String title
+String description
+String text
+ResolutionStatus status
+u32 proposed_by
+DateTime proposed_at
+Option~DateTime~ approved_at
+Option~DateTime~ rejected_at
+DateTime created_at
+DateTime updated_at
+Vec~Approval~ approvals
+propose()
+approve()
+reject()
+add_approval()
+link_to_meeting()
+link_to_vote()
}
class Committee {
+u32 id
+u32 company_id
+String name
+String description
+String purpose
+Option~u32~ circle_id
+DateTime created_at
+DateTime updated_at
+Vec~CommitteeMember~ members
+add_member()
+find_member_by_user_id()
+remove_member()
+link_to_circle()
+get_member_users()
}
Company "1" -- "many" Shareholder: has
Company "1" -- "many" Meeting: holds
Company "1" -- "many" Vote: conducts
Company "1" -- "many" Resolution: issues
Company "1" -- "many" Committee: establishes
Meeting "1" -- "many" Attendee: has
Meeting "1" -- "many" Resolution: discusses
Vote "1" -- "many" VoteOption: has
Vote "1" -- "many" Ballot: collects
Vote "1" -- "1" Resolution: decides
Resolution "1" -- "many" Approval: receives
Committee "1" -- "many" CommitteeMember: has
```
## Usage
These models are used by the governance module to manage corporate governance. They are typically accessed through the database handlers that implement the generic SledDB interface.

View File

@@ -0,0 +1,149 @@
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use crate::db::{Model, Storable, DB, DbError, DbResult};
use crate::models::gov::User;
/// CommitteeRole represents the role of a member in a committee
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum CommitteeRole {
Chair,
ViceChair,
Secretary,
Member,
Advisor,
Observer,
}
/// CommitteeMember represents a member of a committee
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct CommitteeMember {
pub id: u32,
pub committee_id: u32,
pub user_id: u32,
pub name: String,
pub role: CommitteeRole,
pub since: DateTime<Utc>,
pub created_at: DateTime<Utc>,
}
/// Committee represents a board committee
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Committee {
pub id: u32,
pub company_id: u32,
pub name: String,
pub description: String,
pub purpose: String,
pub circle_id: Option<u32>, // Link to Circle for access control
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
pub members: Vec<CommitteeMember>,
}
impl Committee {
/// Create a new committee with default values
pub fn new(
id: u32,
company_id: u32,
name: String,
description: String,
purpose: String,
) -> Self {
let now = Utc::now();
Self {
id,
company_id,
name,
description,
purpose,
circle_id: None,
created_at: now,
updated_at: now,
members: Vec::new(),
}
}
/// Add a member to the committee
pub fn add_member(&mut self, user_id: u32, name: String, role: CommitteeRole) -> &CommitteeMember {
let id = if self.members.is_empty() {
1
} else {
self.members.iter().map(|m| m.id).max().unwrap_or(0) + 1
};
let now = Utc::now();
let member = CommitteeMember {
id,
committee_id: self.id,
user_id,
name,
role,
since: now,
created_at: now,
};
self.members.push(member);
self.updated_at = now;
self.members.last().unwrap()
}
/// Find a member by user ID
pub fn find_member_by_user_id(&self, user_id: u32) -> Option<&CommitteeMember> {
self.members.iter().find(|m| m.user_id == user_id)
}
/// Find a member by user ID (mutable version)
pub fn find_member_by_user_id_mut(&mut self, user_id: u32) -> Option<&mut CommitteeMember> {
self.members.iter_mut().find(|m| m.user_id == user_id)
}
/// Remove a member from the committee
pub fn remove_member(&mut self, member_id: u32) -> bool {
let len = self.members.len();
self.members.retain(|m| m.id != member_id);
let removed = self.members.len() < len;
if removed {
self.updated_at = Utc::now();
}
removed
}
/// Link this committee to a Circle for access control
pub fn link_to_circle(&mut self, circle_id: u32) {
self.circle_id = Some(circle_id);
self.updated_at = Utc::now();
}
/// Get all users who are members of this committee
pub fn get_member_users(&self, db: &DB) -> DbResult<Vec<User>> {
let mut users = Vec::new();
for member in &self.members {
if let Ok(user) = db.get::<User>(member.user_id) {
users.push(user);
}
}
Ok(users)
}
}
// Implement Storable trait
impl Storable for Committee {
}
impl Storable for CommitteeMember {
}
// Implement Model trait
impl Model for Committee {
fn get_id(&self) -> u32 {
self.id
}
fn db_prefix() -> &'static str {
"committee"
}
}

View File

@@ -0,0 +1,185 @@
use crate::db::{Model, Storable, DbResult};
use crate::db::db::DB;
use super::shareholder::Shareholder; // Use super:: for sibling module
use super::Resolution;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::fmt::Debug;
/// CompanyStatus represents the status of a company
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum CompanyStatus {
Active,
Inactive,
Suspended,
}
/// BusinessType represents the type of a business
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct BusinessType(pub String);
impl BusinessType {
pub const CORPORATION: &'static str = "Corporation";
pub const PARTNERSHIP: &'static str = "Partnership";
pub const LLC: &'static str = "LLC";
pub const COOP: &'static str = "Coop";
pub const SINGLE: &'static str = "Single";
pub const TWIN: &'static str = "Twin";
pub const STARTER: &'static str = "Starter";
pub const GLOBAL: &'static str = "Global";
/// Create a new BusinessType, validating that the type is one of the predefined types
pub fn new(type_str: String) -> Result<Self, String> {
if Self::is_valid(&type_str) {
Ok(BusinessType(type_str))
} else {
Err(format!("Invalid business type: {}. Valid types are: {}",
type_str, Self::valid_types().join(", ")))
}
}
/// Create a new BusinessType without validation (use with caution)
pub fn new_unchecked(type_str: String) -> Self {
BusinessType(type_str)
}
/// Get the string value of the business type
pub fn as_str(&self) -> &str {
&self.0
}
/// Check if a string is a valid business type
pub fn is_valid(type_str: &str) -> bool {
Self::valid_types().contains(&type_str.to_string())
}
/// Get a list of all valid business types
pub fn valid_types() -> Vec<String> {
vec![
Self::CORPORATION.to_string(),
Self::PARTNERSHIP.to_string(),
Self::LLC.to_string(),
Self::COOP.to_string(),
Self::SINGLE.to_string(),
Self::TWIN.to_string(),
Self::STARTER.to_string(),
Self::GLOBAL.to_string(),
]
}
}
/// Company represents a company entity
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] // Added PartialEq
pub struct Company {
pub id: u32,
pub name: String,
pub registration_number: String,
pub incorporation_date: DateTime<Utc>,
pub fiscal_year_end: String,
pub email: String,
pub phone: String,
pub website: String,
pub address: String,
pub business_type: BusinessType,
pub industry: String,
pub description: String,
pub status: CompanyStatus,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
// Removed shareholders property
}
impl Storable for Company{}
// Model requires get_id and db_prefix
impl Model for Company {
fn get_id(&self) -> u32 {
self.id
}
fn db_prefix() -> &'static str {
"company" // Prefix for company records in the database
}
}
impl Company {
/// Create a new company with default timestamps
pub fn new(
id: u32,
name: String,
registration_number: String,
incorporation_date: DateTime<Utc>,
fiscal_year_end: String,
email: String,
phone: String,
website: String,
address: String,
business_type: BusinessType,
industry: String,
description: String,
status: CompanyStatus,
) -> Self {
let now = Utc::now();
Self {
id,
name,
registration_number,
incorporation_date,
fiscal_year_end,
email,
phone,
website,
address,
business_type,
industry,
description,
status,
created_at: now,
updated_at: now,
}
}
/// Add a shareholder to the company, saving it to the database
pub fn add_shareholder(
&mut self,
db: &mut DB, // Pass in the DB instance
mut shareholder: Shareholder,
) -> DbResult<()> {
shareholder.company_id = self.id; // Set the company_id
db.set(&shareholder)?; // Insert the shareholder into the DB
self.updated_at = Utc::now();
Ok(())
}
/// Link this company to a Circle for access control
pub fn link_to_circle(&mut self, _circle_id: u32) {
// Implementation would involve updating a mapping in a separate database
// For now, we'll just update the timestamp to indicate the change
self.updated_at = Utc::now();
}
/// Link this company to a Customer in the biz module
pub fn link_to_customer(&mut self, _customer_id: u32) {
// Implementation would involve updating a mapping in a separate database
// For now, we'll just update the timestamp to indicate the change
self.updated_at = Utc::now();
}
/// Get all resolutions for this company
pub fn get_resolutions(&self, db: &DB) -> DbResult<Vec<Resolution>> {
let all_resolutions = db.list::<Resolution>()?;
let company_resolutions = all_resolutions
.into_iter()
.filter(|resolution| resolution.company_id == self.id)
.collect();
Ok(company_resolutions)
}
// Future methods:
// /// Get all committees for this company
// pub fn get_committees(&self, db: &DB) -> DbResult<Vec<Committee>> { ... }
//
// /// Get all compliance requirements for this company
// pub fn get_compliance_requirements(&self, db: &DB) -> DbResult<Vec<ComplianceRequirement>> { ... }
}

View File

@@ -0,0 +1,188 @@
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use crate::db::{Model, Storable, DB, DbError, DbResult}; // Import traits from db module
// use std::collections::HashMap; // Removed unused import
// use super::db::Model; // Removed old Model trait import
/// MeetingStatus represents the status of a meeting
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum MeetingStatus {
Scheduled,
Completed,
Cancelled,
}
/// AttendeeRole represents the role of an attendee in a meeting
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum AttendeeRole {
Coordinator,
Member,
Secretary,
Participant,
Advisor,
Admin,
}
/// AttendeeStatus represents the status of an attendee's participation
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum AttendeeStatus {
Confirmed,
Pending,
Declined,
}
/// Attendee represents an attendee of a board meeting
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Attendee {
pub id: u32,
pub meeting_id: u32,
pub user_id: u32,
pub name: String,
pub role: AttendeeRole,
pub status: AttendeeStatus,
pub created_at: DateTime<Utc>,
}
impl Attendee {
/// Create a new attendee with default values
pub fn new(
id: u32,
meeting_id: u32,
user_id: u32,
name: String,
role: AttendeeRole,
) -> Self {
Self {
id,
meeting_id,
user_id,
name,
role,
status: AttendeeStatus::Pending,
created_at: Utc::now(),
}
}
/// Update the status of an attendee
pub fn update_status(&mut self, status: AttendeeStatus) {
self.status = status;
}
}
/// Meeting represents a board meeting of a company or other meeting
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Meeting {
pub id: u32,
pub company_id: u32,
pub title: String,
pub date: DateTime<Utc>,
pub location: String,
pub description: String,
pub status: MeetingStatus,
pub minutes: String,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
pub attendees: Vec<Attendee>,
}
// Removed old Model trait implementation
impl Meeting {
/// Create a new meeting with default values
pub fn new(
id: u32,
company_id: u32,
title: String,
date: DateTime<Utc>,
location: String,
description: String,
) -> Self {
let now = Utc::now();
Self {
id,
company_id,
title,
date,
location,
description,
status: MeetingStatus::Scheduled,
minutes: String::new(),
created_at: now,
updated_at: now,
attendees: Vec::new(),
}
}
/// Add an attendee to the meeting
pub fn add_attendee(&mut self, attendee: Attendee) {
// Make sure the attendee's meeting_id matches this meeting
assert_eq!(self.id, attendee.meeting_id, "Attendee meeting_id must match meeting id");
// Check if the attendee already exists
if !self.attendees.iter().any(|a| a.id == attendee.id) {
self.attendees.push(attendee);
self.updated_at = Utc::now();
}
}
/// Update the status of the meeting
pub fn update_status(&mut self, status: MeetingStatus) {
self.status = status;
self.updated_at = Utc::now();
}
/// Update the meeting minutes
pub fn update_minutes(&mut self, minutes: String) {
self.minutes = minutes;
self.updated_at = Utc::now();
}
/// Find an attendee by user ID
pub fn find_attendee_by_user_id(&self, user_id: u32) -> Option<&Attendee> {
self.attendees.iter().find(|a| a.user_id == user_id)
}
/// Find an attendee by user ID (mutable version)
pub fn find_attendee_by_user_id_mut(&mut self, user_id: u32) -> Option<&mut Attendee> {
self.attendees.iter_mut().find(|a| a.user_id == user_id)
}
/// Get all confirmed attendees
pub fn confirmed_attendees(&self) -> Vec<&Attendee> {
self.attendees
.iter()
.filter(|a| a.status == AttendeeStatus::Confirmed)
.collect()
}
/// Link this meeting to a Calendar Event in the mcc module
pub fn link_to_event(&mut self, _event_id: u32) -> DbResult<()> {
// Implementation would involve updating a mapping in a separate database
// For now, we'll just update the timestamp to indicate the change
self.updated_at = Utc::now();
Ok(())
}
/// Get all resolutions discussed in this meeting
pub fn get_resolutions(&self, db: &DB) -> DbResult<Vec<super::Resolution>> {
let all_resolutions = db.list::<super::Resolution>()?;
let meeting_resolutions = all_resolutions
.into_iter()
.filter(|resolution| resolution.meeting_id == Some(self.id))
.collect();
Ok(meeting_resolutions)
}
}
impl Storable for Meeting{}
// Implement Model trait
impl Model for Meeting {
fn get_id(&self) -> u32 {
self.id
}
fn db_prefix() -> &'static str {
"meeting"
}
}

View File

@@ -0,0 +1,20 @@
pub mod company;
pub mod shareholder;
pub mod meeting;
pub mod user;
pub mod vote;
pub mod resolution;
// All modules:
pub mod committee;
// Re-export all model types for convenience
pub use company::{Company, CompanyStatus, BusinessType};
pub use shareholder::{Shareholder, ShareholderType};
pub use meeting::{Meeting, Attendee, MeetingStatus, AttendeeRole, AttendeeStatus};
pub use user::User;
pub use vote::{Vote, VoteOption, Ballot, VoteStatus};
pub use resolution::{Resolution, ResolutionStatus, Approval};
pub use committee::{Committee, CommitteeMember, CommitteeRole};
// Re-export database components from db module
pub use crate::db::{DB, DBBuilder, Model, Storable, DbError, DbResult};

View File

@@ -0,0 +1,195 @@
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use crate::db::{Model, Storable, DB, DbError, DbResult};
use crate::models::gov::{Meeting, Vote};
/// ResolutionStatus represents the status of a resolution
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ResolutionStatus {
Draft,
Proposed,
Approved,
Rejected,
Withdrawn,
}
/// Resolution represents a board resolution
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Resolution {
pub id: u32,
pub company_id: u32,
pub meeting_id: Option<u32>,
pub vote_id: Option<u32>,
pub title: String,
pub description: String,
pub text: String,
pub status: ResolutionStatus,
pub proposed_by: u32, // User ID
pub proposed_at: DateTime<Utc>,
pub approved_at: Option<DateTime<Utc>>,
pub rejected_at: Option<DateTime<Utc>>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
pub approvals: Vec<Approval>,
}
/// Approval represents an approval of a resolution by a board member
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Approval {
pub id: u32,
pub resolution_id: u32,
pub user_id: u32,
pub name: String,
pub approved: bool,
pub comments: String,
pub created_at: DateTime<Utc>,
}
impl Resolution {
/// Create a new resolution with default values
pub fn new(
id: u32,
company_id: u32,
title: String,
description: String,
text: String,
proposed_by: u32,
) -> Self {
let now = Utc::now();
Self {
id,
company_id,
meeting_id: None,
vote_id: None,
title,
description,
text,
status: ResolutionStatus::Draft,
proposed_by,
proposed_at: now,
approved_at: None,
rejected_at: None,
created_at: now,
updated_at: now,
approvals: Vec::new(),
}
}
/// Propose the resolution
pub fn propose(&mut self) {
self.status = ResolutionStatus::Proposed;
self.proposed_at = Utc::now();
self.updated_at = Utc::now();
}
/// Approve the resolution
pub fn approve(&mut self) {
self.status = ResolutionStatus::Approved;
self.approved_at = Some(Utc::now());
self.updated_at = Utc::now();
}
/// Reject the resolution
pub fn reject(&mut self) {
self.status = ResolutionStatus::Rejected;
self.rejected_at = Some(Utc::now());
self.updated_at = Utc::now();
}
/// Withdraw the resolution
pub fn withdraw(&mut self) {
self.status = ResolutionStatus::Withdrawn;
self.updated_at = Utc::now();
}
/// Add an approval to the resolution
pub fn add_approval(&mut self, user_id: u32, name: String, approved: bool, comments: String) -> &Approval {
let id = if self.approvals.is_empty() {
1
} else {
self.approvals.iter().map(|a| a.id).max().unwrap_or(0) + 1
};
let approval = Approval {
id,
resolution_id: self.id,
user_id,
name,
approved,
comments,
created_at: Utc::now(),
};
self.approvals.push(approval);
self.updated_at = Utc::now();
self.approvals.last().unwrap()
}
/// Find an approval by user ID
pub fn find_approval_by_user_id(&self, user_id: u32) -> Option<&Approval> {
self.approvals.iter().find(|a| a.user_id == user_id)
}
/// Get all approvals
pub fn get_approvals(&self) -> &[Approval] {
&self.approvals
}
/// Get approval count
pub fn approval_count(&self) -> usize {
self.approvals.iter().filter(|a| a.approved).count()
}
/// Get rejection count
pub fn rejection_count(&self) -> usize {
self.approvals.iter().filter(|a| !a.approved).count()
}
/// Link this resolution to a meeting
pub fn link_to_meeting(&mut self, meeting_id: u32) {
self.meeting_id = Some(meeting_id);
self.updated_at = Utc::now();
}
/// Link this resolution to a vote
pub fn link_to_vote(&mut self, vote_id: u32) {
self.vote_id = Some(vote_id);
self.updated_at = Utc::now();
}
/// Get the meeting associated with this resolution
pub fn get_meeting(&self, db: &DB) -> DbResult<Option<Meeting>> {
match self.meeting_id {
Some(meeting_id) => {
let meeting = db.get::<Meeting>(meeting_id)?;
Ok(Some(meeting))
}
None => Ok(None),
}
}
/// Get the vote associated with this resolution
pub fn get_vote(&self, db: &DB) -> DbResult<Option<Vote>> {
match self.vote_id {
Some(vote_id) => {
let vote = db.get::<Vote>(vote_id)?;
Ok(Some(vote))
}
None => Ok(None),
}
}
}
impl Storable for Resolution{}
// Implement Model trait
impl Model for Resolution {
fn get_id(&self) -> u32 {
self.id
}
fn db_prefix() -> &'static str {
"resolution"
}
}

View File

@@ -0,0 +1,77 @@
use crate::db::{Model, Storable}; // Import db traits
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
// use std::collections::HashMap; // Removed unused import
// use super::db::Model; // Removed old Model trait import
/// ShareholderType represents the type of shareholder
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ShareholderType {
Individual,
Corporate,
}
/// Shareholder represents a shareholder of a company
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] // Added PartialEq
pub struct Shareholder {
pub id: u32,
pub company_id: u32,
pub user_id: u32,
pub name: String,
pub shares: f64,
pub percentage: f64,
pub type_: ShareholderType,
pub since: DateTime<Utc>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
// Removed old Model trait implementation
impl Shareholder {
/// Create a new shareholder with default timestamps
pub fn new(
id: u32,
company_id: u32,
user_id: u32,
name: String,
shares: f64,
percentage: f64,
type_: ShareholderType,
) -> Self {
let now = Utc::now();
Self {
id,
company_id,
user_id,
name,
shares,
percentage,
type_,
since: now,
created_at: now,
updated_at: now,
}
}
/// Update the shares owned by this shareholder
pub fn update_shares(&mut self, shares: f64, percentage: f64) {
self.shares = shares;
self.percentage = percentage;
self.updated_at = Utc::now();
}
}
impl Storable for Shareholder{}
// Implement Model trait
impl Model for Shareholder {
fn get_id(&self) -> u32 {
self.id
}
fn db_prefix() -> &'static str {
"shareholder"
}
}

View File

@@ -0,0 +1,56 @@
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use crate::db::{Model, Storable}; // Import db traits
// use std::collections::HashMap; // Removed unused import
/// User represents a user in the governance system
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct User {
pub id: u32,
pub name: String,
pub email: String,
pub password: String,
pub company: String, // here its just a best effort
pub role: String,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
// Removed old Model trait implementation
impl User {
/// Create a new user with default timestamps
pub fn new(
id: u32,
name: String,
email: String,
password: String,
company: String,
role: String,
) -> Self {
let now = Utc::now();
Self {
id,
name,
email,
password,
company,
role,
created_at: now,
updated_at: now,
}
}
}
impl Storable for User{}
// Implement Model trait
impl Model for User {
fn get_id(&self) -> u32 {
self.id
}
fn db_prefix() -> &'static str {
"user"
}
}

View File

@@ -0,0 +1,150 @@
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use crate::db::{Model, Storable, DB, DbError, DbResult}; // Import traits from db module
// use std::collections::HashMap; // Removed unused import
// use super::db::Model; // Removed old Model trait import
/// VoteStatus represents the status of a vote
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum VoteStatus {
Open,
Closed,
Cancelled,
}
/// Vote represents a voting item for corporate decision-making
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Vote {
pub id: u32,
pub company_id: u32,
pub title: String,
pub description: String,
pub start_date: DateTime<Utc>,
pub end_date: DateTime<Utc>,
pub status: VoteStatus,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
pub options: Vec<VoteOption>,
pub ballots: Vec<Ballot>,
pub private_group: Vec<u32>, // user id's only people who can vote
}
/// VoteOption represents an option in a vote
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VoteOption {
pub id: u8,
pub vote_id: u32,
pub text: String,
pub count: i32,
pub min_valid: i32, // min votes we need to make total vote count
}
/// The vote as done by the user
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Ballot {
pub id: u32,
pub vote_id: u32,
pub user_id: u32,
pub vote_option_id: u8,
pub shares_count: i32,
pub created_at: DateTime<Utc>,
}
impl Storable for Vote{}
impl Vote {
/// Create a new vote with default timestamps
pub fn new(
id: u32,
company_id: u32,
title: String,
description: String,
start_date: DateTime<Utc>,
end_date: DateTime<Utc>,
status: VoteStatus,
) -> Self {
let now = Utc::now();
Self {
id,
company_id,
title,
description,
start_date,
end_date,
status,
created_at: now,
updated_at: now,
options: Vec::new(),
ballots: Vec::new(),
private_group: Vec::new(),
}
}
/// Add a voting option to this vote
pub fn add_option(&mut self, text: String, min_valid: i32) -> &VoteOption {
let id = if self.options.is_empty() {
1
} else {
self.options.iter().map(|o| o.id).max().unwrap_or(0) + 1
};
let option = VoteOption {
id,
vote_id: self.id,
text,
count: 0,
min_valid,
};
self.options.push(option);
self.options.last().unwrap()
}
/// Add a ballot to this vote
pub fn add_ballot(&mut self, user_id: u32, vote_option_id: u8, shares_count: i32) -> &Ballot {
let id = if self.ballots.is_empty() {
1
} else {
self.ballots.iter().map(|b| b.id).max().unwrap_or(0) + 1
};
let ballot = Ballot {
id,
vote_id: self.id,
user_id,
vote_option_id,
shares_count,
created_at: Utc::now(),
};
// Update the vote count for the selected option
if let Some(option) = self.options.iter_mut().find(|o| o.id == vote_option_id) {
option.count += shares_count;
}
self.ballots.push(ballot);
self.ballots.last().unwrap()
}
/// Get the resolution associated with this vote
pub fn get_resolution(&self, db: &DB) -> DbResult<Option<super::Resolution>> {
let all_resolutions = db.list::<super::Resolution>()?;
let vote_resolution = all_resolutions
.into_iter()
.find(|resolution| resolution.vote_id == Some(self.id));
Ok(vote_resolution)
}
}
// Implement Model trait
impl Model for Vote {
fn get_id(&self) -> u32 {
self.id
}
fn db_prefix() -> &'static str {
"vote"
}
}

View File

@@ -0,0 +1,21 @@
in @src/models/circle/circle.rs
- member us now new rootobject, check implementation
- a member is linked to one or more contacts id's (from src/models/mcc/contacts.rs)
- create a new rootobject called wallet
- has a name, description, blockchainname (string), pubkey
- a wallet has embedded struct for asset which is name e.g. USDC and float which is the amount of money in the asset
- a member has one or more wallets, in member link to the id's of the wallet
in@src/models/biz add a ticket module
user can have more than 1 ticket which is to ask support from the org
a ticket has following fields
- subject
- description
- creation/update date
- assignees (based on memberid see above)
-

View File

@@ -0,0 +1,96 @@
# MCC (Mail, Calendar, Contacts) Core Models
This directory contains the core data structures used in the herolib MCC module. These models serve as the foundation for the mail, calendar, and contacts functionality.
## Overview
The core models implement the Serde traits (Serialize/Deserialize) and crate database traits (Storable, SledModel), which allows them to be stored and retrieved using the generic SledDB implementation. Each model provides:
- A struct definition with appropriate fields
- Serde serialization through derive macros
- Methods for database integration through the SledModel trait
- Utility methods for common operations
## Core Models
### Mail (`mail.rs`)
The Mail models provide email and IMAP functionality:
- **Email**: Main struct for email messages with IMAP metadata
- **Attachment**: Represents a file attachment with file information
- **Envelope**: Represents an IMAP envelope structure with message headers
### Message (`message.rs`)
The Message models provide chat functionality:
- **Message**: Main struct for chat messages with thread and recipient information
- **MessageMeta**: Contains metadata for message status, editing, and reactions
- **MessageStatus**: Enum representing the status of a message (Sent, Delivered, Read, Failed)
### Calendar (`calendar.rs`)
The Calendar model represents a container for calendar events:
- **Calendar**: Main struct with fields for identification and description
### Event (`event.rs`)
The Event model provides calendar event management:
- **Event**: Main struct for calendar events with time and attendee information
- **EventMeta**: Contains additional metadata for synchronization and display
### Contacts (`contacts.rs`)
The Contacts model provides contact management:
- **Contact**: Main struct for contact information with personal details and grouping
## Group Support
All models now support linking to multiple groups (Circle IDs):
- Each model has a `groups: Vec<u32>` field to store multiple group IDs
- Utility methods for adding, removing, and filtering by groups
- Groups are defined in the Circle module
## Utility Methods
Each model provides utility methods for:
### Filtering/Searching
- `filter_by_groups(groups: &[u32]) -> bool`: Filter by groups
- `search_by_subject/content/name/email(query: &str) -> bool`: Search by various fields
### Format Conversion
- `to_message()`: Convert Email to Message
### Relationship Management
- `get_events()`: Get events associated with a calendar or contact
- `get_calendar()`: Get the calendar an event belongs to
- `get_attendee_contacts()`: Get contacts for event attendees
- `get_thread_messages()`: Get all messages in the same thread
## Usage
These models are used by the MCC module to manage emails, calendar events, and contacts. They are typically accessed through the database handlers that implement the generic SledDB interface.
## Serialization
All models use Serde for serialization:
- Each model implements Serialize and Deserialize traits through derive macros
- Binary serialization is handled automatically by the database layer
- JSON serialization is available for API responses and other use cases
## Database Integration
The models are designed to work with the SledDB implementation through:
- The `Storable` trait for serialization/deserialization
- The `SledModel` trait for database operations:
- `get_id()` method for unique identification
- `db_prefix()` method to specify the collection prefix
- Implementation of custom utility methods where needed

View File

@@ -0,0 +1,56 @@
use serde::{Deserialize, Serialize};
use crate::models::mcc::event::Event;
use crate::db::model::impl_get_id;
/// Calendar represents a calendar container for events
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Calendar {
pub id: u32, // Unique identifier
pub title: String, // Calendar title
pub description: String, // Calendar details
pub groups: Vec<u32>, // Groups this calendar belongs to (references Circle IDs)
}
impl Calendar {
/// Create a new calendar
pub fn new(id: u32, title: String, description: String) -> Self {
Self {
id,
title,
description,
groups: Vec::new(),
}
}
/// Add a group to this calendar
pub fn add_group(&mut self, group_id: u32) {
if !self.groups.contains(&group_id) {
self.groups.push(group_id);
}
}
/// Remove a group from this calendar
pub fn remove_group(&mut self, group_id: u32) {
self.groups.retain(|&id| id != group_id);
}
/// Filter by groups - returns true if this calendar belongs to any of the specified groups
pub fn filter_by_groups(&self, groups: &[u32]) -> bool {
groups.iter().any(|g| self.groups.contains(g))
}
/// Filter events by this calendar's ID
pub fn filter_events<'a>(&self, events: &'a [Event]) -> Vec<&'a Event> {
events.iter()
.filter(|event| event.calendar_id == self.id)
.collect()
}
/// Get the database prefix for this model type
pub fn db_prefix() -> &'static str {
"calendar"
}
}
// Automatically implement GetId trait for Calendar
impl_get_id!(Calendar);

View File

@@ -0,0 +1,90 @@
use serde::{Deserialize, Serialize};
use crate::models::mcc::event::Event;
use crate::db::model::impl_get_id;
use chrono::Utc;
/// Contact represents a contact entry in an address book
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Contact {
// Database ID
pub id: u32, // Database ID (assigned by DBHandler)
// Content fields
pub created_at: i64, // Unix epoch timestamp
pub modified_at: i64, // Unix epoch timestamp
pub first_name: String,
pub last_name: String,
pub emails: Vec<String>, // Changed from []String to Vec<String>
}
impl Contact {
/// Create a new contact
pub fn new(id: u32, first_name: String, last_name: String, emails: Vec<String>) -> Self {
let now = Utc::now().timestamp();
Self {
id,
created_at: now,
modified_at: now,
first_name,
last_name,
emails : emails,
}
}
/// Search by name - returns true if the name contains the query (case-insensitive)
pub fn search_by_name(&self, query: &str) -> bool {
let full_name = self.full_name().to_lowercase();
query.to_lowercase().split_whitespace().all(|word| full_name.contains(word))
}
/// Search by email - returns true if the email contains the query (case-insensitive)
pub fn search_by_email(&self, query: &str) -> bool {
self.email.to_lowercase().contains(&query.to_lowercase())
}
/// Filter events where this contact is an attendee
pub fn filter_events<'a>(&self, events: &'a [Event]) -> Vec<&'a Event> {
events.iter()
.filter(|event| event.attendees.contains(&self.email))
.collect()
}
/// Update the contact's information
pub fn update(&mut self, first_name: Option<String>, last_name: Option<String>, email: Option<String>, group: Option<String>) {
if let Some(first_name) = first_name {
self.first_name = first_name;
}
if let Some(last_name) = last_name {
self.last_name = last_name;
}
if let Some(email) = email {
self.email = email;
}
if let Some(group) = group {
self.group = group;
}
self.modified_at = Utc::now().timestamp();
}
/// Update the contact's groups
pub fn update_groups(&mut self, groups: Vec<u32>) {
self.groups = groups;
self.modified_at = Utc::now().timestamp();
}
/// Get the full name of the contact
pub fn full_name(&self) -> String {
format!("{} {}", self.first_name, self.last_name)
}
/// Get the database prefix for this model type
pub fn db_prefix() -> &'static str {
"contact"
}
}
// Automatically implement GetId trait for Contact
impl_get_id!(Contact);

View File

@@ -0,0 +1,131 @@
use serde::{Deserialize, Serialize};
use crate::models::mcc::calendar::Calendar;
use crate::models::mcc::contacts::Contact;
use crate::db::model::impl_get_id;
use chrono::{DateTime, Utc};
/// EventMeta contains additional metadata for a calendar event
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EventMeta {
pub caldav_uid: String, // CalDAV UID for syncing
pub sync_token: String, // Sync token for tracking changes
pub etag: String, // ETag for caching
pub color: String, // User-friendly color categorization
}
/// Represents a calendar event with all its properties
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Event {
pub id: u32, // Unique identifier
pub calendar_id: u32, // ID of the calendar this event belongs to
pub title: String, // Event title
pub description: String, // Event details
pub location: String, // Event location
pub start_time: DateTime<Utc>, // Start time
pub end_time: DateTime<Utc>, // End time
pub all_day: bool, // True if it's an all-day event
pub recurrence: String, // RFC 5545 Recurrence Rule (e.g., "FREQ=DAILY;COUNT=10")
pub attendees: Vec<String>, // List of emails or user IDs
pub organizer: String, // Organizer email
pub status: String, // "CONFIRMED", "CANCELLED", "TENTATIVE"
pub meta: EventMeta, // Additional metadata
pub groups: Vec<u32>, // Groups this event belongs to (references Circle IDs)
}
impl Event {
/// Create a new event
pub fn new(
id: u32,
calendar_id: u32,
title: String,
description: String,
location: String,
start_time: DateTime<Utc>,
end_time: DateTime<Utc>,
organizer: String,
) -> Self {
Self {
id,
calendar_id,
title,
description,
location,
start_time,
end_time,
all_day: false,
recurrence: String::new(),
attendees: Vec::new(),
organizer,
status: "CONFIRMED".to_string(),
meta: EventMeta {
caldav_uid: String::new(),
sync_token: String::new(),
etag: String::new(),
color: String::new(),
},
groups: Vec::new(),
}
}
/// Add a group to this event
pub fn add_group(&mut self, group_id: u32) {
if !self.groups.contains(&group_id) {
self.groups.push(group_id);
}
}
/// Remove a group from this event
pub fn remove_group(&mut self, group_id: u32) {
self.groups.retain(|&id| id != group_id);
}
/// Filter by groups - returns true if this event belongs to any of the specified groups
pub fn filter_by_groups(&self, groups: &[u32]) -> bool {
groups.iter().any(|g| self.groups.contains(g))
}
/// Find the calendar this event belongs to
pub fn find_calendar<'a>(&self, calendars: &'a [Calendar]) -> Option<&'a Calendar> {
calendars.iter().find(|cal| cal.id == self.calendar_id)
}
/// Filter contacts that are attendees of this event
pub fn filter_attendee_contacts<'a>(&self, contacts: &'a [Contact]) -> Vec<&'a Contact> {
contacts.iter()
.filter(|contact| self.attendees.contains(&contact.email))
.collect()
}
/// Add an attendee to this event
pub fn add_attendee(&mut self, attendee: String) {
self.attendees.push(attendee);
}
/// Set event to all day
pub fn set_all_day(&mut self, all_day: bool) {
self.all_day = all_day;
}
/// Set event status
pub fn set_status(&mut self, status: &str) {
self.status = status.to_string();
}
/// Search by title - returns true if the title contains the query (case-insensitive)
pub fn search_by_title(&self, query: &str) -> bool {
self.title.to_lowercase().contains(&query.to_lowercase())
}
/// Search by description - returns true if the description contains the query (case-insensitive)
pub fn search_by_description(&self, query: &str) -> bool {
self.description.to_lowercase().contains(&query.to_lowercase())
}
/// Get the database prefix for this model type
pub fn db_prefix() -> &'static str {
"event"
}
}
// Automatically implement GetId trait for Event
impl_get_id!(Event);

View File

@@ -0,0 +1,12 @@
pub mod calendar;
pub mod event;
pub mod mail;
pub mod contacts;
pub mod message;
// Re-export all model types for convenience
pub use calendar::Calendar;
pub use event::{Event, EventMeta};
pub use mail::{Email, Attachment, Envelope};
pub use contacts::Contact;
pub use message::{Message, MessageMeta, MessageStatus};

View File

@@ -0,0 +1,129 @@
use serde::{Deserialize, Serialize};
use crate::db::model::impl_get_id;
use chrono::Utc;
/// Email represents an email message with all its metadata and content
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Email {
// Database ID
pub id: u32,
pub message: String, // The email body content
pub attachments: Vec<Attachment>, // Any file attachments
pub flags: Vec<String>, // IMAP flags like \Seen, \Deleted, etc.
pub receivetime: i64, // Unix timestamp when the email was received
pub envelope: Option<Envelope>, // IMAP envelope structure
}
/// Attachment represents an email attachment
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Attachment {
pub filename: String,
pub content_type: String,
pub hash: String, // In each circle we have unique dedupe DB, this is the hash of the fileobject
pub size: u32, // Size in kb of the attachment
}
/// Envelope represents an IMAP envelope structure
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Envelope {
pub date: i64,
pub subject: String,
pub from: Vec<String>,
pub sender: Vec<String>,
pub reply_to: Vec<String>,
pub to: Vec<String>,
pub cc: Vec<String>,
pub bcc: Vec<String>,
pub in_reply_to: String,
}
impl Email {
/// Create a new email
pub fn new(id: u32, uid: u32, seq_num: u32, mailbox: String, message: String) -> Self {
Self {
id,
message,
attachments: Vec::new(),
flags: Vec::new(),
receivetime: chrono::Utc::now().timestamp(),
envelope: None,
}
}
/// Add an attachment to this email
pub fn add_attachment(&mut self, attachment: Attachment) {
self.attachments.push(attachment);
}
/// Search by subject - returns true if the subject contains the query (case-insensitive)
pub fn search_by_subject(&self, query: &str) -> bool {
if let Some(env) = &self.envelope {
env.subject.to_lowercase().contains(&query.to_lowercase())
} else {
false
}
}
/// Search by content - returns true if the message content contains the query (case-insensitive)
pub fn search_by_content(&self, query: &str) -> bool {
self.message.to_lowercase().contains(&query.to_lowercase())
}
/// Set the envelope for this email
pub fn set_envelope(&mut self, envelope: Envelope) {
self.envelope = Some(envelope);
}
/// Convert this email to a Message (for chat)
pub fn to_message(&self, id: u32, thread_id: String) -> crate::models::mcc::message::Message {
use crate::models::mcc::message::Message;
let _now = Utc::now();
let sender = if let Some(env) = &self.envelope {
if !env.from.is_empty() {
env.from[0].clone()
} else {
"unknown@example.com".to_string()
}
} else {
"unknown@example.com".to_string()
};
let subject = if let Some(env) = &self.envelope {
env.subject.clone()
} else {
"No Subject".to_string()
};
let recipients = if let Some(env) = &self.envelope {
env.to.clone()
} else {
Vec::new()
};
let content = if !subject.is_empty() {
format!("{}\n\n{}", subject, self.message)
} else {
self.message.clone()
};
let mut message = Message::new(id, thread_id, sender, content);
message.recipients = recipients;
message.groups = self.groups.clone();
// Convert attachments to references
for attachment in &self.attachments {
message.add_attachment(attachment.filename.clone());
}
message
}
/// Get the database prefix for this model type
pub fn db_prefix() -> &'static str {
"email"
}
}
// Automatically implement GetId trait for Email
impl_get_id!(Email);

View File

@@ -0,0 +1,122 @@
use serde::{Deserialize, Serialize};
use crate::impl_get_id;
use chrono::{DateTime, Utc};
/// MessageStatus represents the status of a message
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum MessageStatus {
Sent,
Delivered,
Read,
Failed,
}
/// MessageMeta contains metadata for a chat message
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MessageMeta {
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
pub status: MessageStatus,
pub is_edited: bool,
pub reactions: Vec<String>,
}
/// Message represents a chat message
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Message {
pub id: u32, // Unique identifier
pub thread_id: String, // Thread/conversation identifier
pub sender_id: String, // Sender identifier
pub recipients: Vec<String>, // List of recipient identifiers
pub content: String, // Message content
pub attachments: Vec<String>, // References to attachments
pub meta: MessageMeta, // Message metadata
}
impl Message {
/// Create a new message
pub fn new(id: u32, thread_id: String, sender_id: String, content: String) -> Self {
let now = Utc::now();
Self {
id,
thread_id,
sender_id,
recipients: Vec::new(),
content,
attachments: Vec::new(),
meta: MessageMeta {
created_at: now,
updated_at: now,
status: MessageStatus::Sent,
is_edited: false,
reactions: Vec::new(),
},
}
}
/// Add a recipient to this message
pub fn add_recipient(&mut self, recipient: String) {
self.recipients.push(recipient);
}
/// Add an attachment to this message
pub fn add_attachment(&mut self, attachment: String) {
self.attachments.push(attachment);
}
/// Add a group to this message
pub fn add_group(&mut self, group_id: u32) {
if !self.groups.contains(&group_id) {
self.groups.push(group_id);
}
}
/// Remove a group from this message
pub fn remove_group(&mut self, group_id: u32) {
self.groups.retain(|&id| id != group_id);
}
/// Filter by groups - returns true if this message belongs to any of the specified groups
pub fn filter_by_groups(&self, groups: &[u32]) -> bool {
groups.iter().any(|g| self.groups.contains(g))
}
/// Search by content - returns true if the content contains the query (case-insensitive)
pub fn search_by_content(&self, query: &str) -> bool {
self.content.to_lowercase().contains(&query.to_lowercase())
}
/// Update message status
pub fn update_status(&mut self, status: MessageStatus) {
self.meta.status = status;
self.meta.updated_at = Utc::now();
}
/// Edit message content
pub fn edit_content(&mut self, new_content: String) {
self.content = new_content;
self.meta.is_edited = true;
self.meta.updated_at = Utc::now();
}
/// Add a reaction to the message
pub fn add_reaction(&mut self, reaction: String) {
self.meta.reactions.push(reaction);
self.meta.updated_at = Utc::now();
}
/// Filter messages that are in the same thread as this message
pub fn filter_thread_messages<'a>(&self, messages: &'a [Message]) -> Vec<&'a Message> {
messages.iter()
.filter(|msg| msg.thread_id == self.thread_id)
.collect()
}
/// Get the database prefix for this model type
pub fn db_prefix() -> &'static str {
"message"
}
}
// Automatically implement GetId trait for Message
impl_get_id!(Message);

View File

@@ -0,0 +1,12 @@
pub mod calendar;
pub mod event;
pub mod mail;
pub mod contacts;
pub mod message;
// Re-export all model types for convenience
pub use calendar::Calendar;
pub use event::{Event, EventMeta};
pub use mail::{Email, Attachment, Envelope};
pub use contacts::Contact;
pub use message::{Message, MessageMeta, MessageStatus};

View File

@@ -0,0 +1,4 @@
pub mod biz;
pub mod mcc;
pub mod circle;
pub mod gov;

View File

@@ -0,0 +1,131 @@
# Business Models Python Port
This directory contains a Python port of the business models from the Rust codebase, using SQLModel for database integration.
## Overview
This project includes:
1. Python port of Rust business models using SQLModel
2. FastAPI server with OpenAPI/Swagger documentation
3. CRUD operations for all models
4. Convenience endpoints for common operations
The models ported from Rust to Python include:
- **Currency**: Represents a monetary value with amount and currency code
- **Customer**: Represents a customer who can purchase products or services
- **Product**: Represents a product or service offered
- **ProductComponent**: Represents a component of a product
- **SaleItem**: Represents an item in a sale
- **Sale**: Represents a sale of products or services
## Structure
- `models.py`: Contains the SQLModel definitions for all business models
- `example.py`: Demonstrates how to use the models with a sample application
- `install_and_run.sh`: Bash script to install dependencies using `uv` and run the example
- `api.py`: FastAPI server providing CRUD operations for all models
- `server.sh`: Bash script to start the FastAPI server
## Requirements
- Python 3.7+
- [uv](https://github.com/astral-sh/uv) for dependency management
## Installation
The project uses `uv` for dependency management. To install dependencies and run the example:
```bash
./install_and_run.sh
```
## API Server
The project includes a FastAPI server that provides CRUD operations for all models and some convenience endpoints.
### Starting the Server
To start the API server:
```bash
./server.sh
```
This script will:
1. Create a virtual environment if it doesn't exist
2. Install the required dependencies using `uv`
3. Start the FastAPI server with hot reloading enabled
### API Documentation
Once the server is running, you can access the OpenAPI documentation at:
- Swagger UI: http://localhost:8000/docs
- ReDoc: http://localhost:8000/redoc
### Available Endpoints
The API provides the following endpoints:
#### Currencies
- `GET /currencies/`: List all currencies
- `POST /currencies/`: Create a new currency
- `GET /currencies/{currency_id}`: Get a specific currency
- `PUT /currencies/{currency_id}`: Update a currency
- `DELETE /currencies/{currency_id}`: Delete a currency
#### Customers
- `GET /customers/`: List all customers
- `POST /customers/`: Create a new customer
- `GET /customers/{customer_id}`: Get a specific customer
- `PUT /customers/{customer_id}`: Update a customer
- `DELETE /customers/{customer_id}`: Delete a customer
- `GET /customers/{customer_id}/sales/`: Get all sales for a customer
#### Products
- `GET /products/`: List all products
- `POST /products/`: Create a new product
- `GET /products/{product_id}`: Get a specific product
- `PUT /products/{product_id}`: Update a product
- `DELETE /products/{product_id}`: Delete a product
- `GET /products/available/`: Get all available products
- `POST /products/{product_id}/components/`: Add a component to a product
- `GET /products/{product_id}/components/`: Get all components for a product
#### Sales
- `GET /sales/`: List all sales
- `POST /sales/`: Create a new sale
- `GET /sales/{sale_id}`: Get a specific sale
- `PUT /sales/{sale_id}`: Update a sale
- `DELETE /sales/{sale_id}`: Delete a sale
- `PUT /sales/{sale_id}/status/{status}`: Update the status of a sale
- `POST /sales/{sale_id}/items/`: Add an item to a sale
- `GET /sales/{sale_id}/items/`: Get all items for a sale
## Dependencies
- SQLModel: For database models and ORM functionality
- Pydantic: For data validation (used by SQLModel)
- FastAPI: For creating the API server
- Uvicorn: ASGI server for running FastAPI applications
## Example Usage
The `example.py` script demonstrates:
1. Creating an SQLite database
2. Defining and creating tables for the models
3. Creating sample data (customers, products, sales)
4. Performing operations on the data
5. Querying and displaying the data
To run the example manually (after activating the virtual environment):
```bash
# From the py directory
python example.py
# Or from the parent directory
cd py && python example.py

View File

@@ -0,0 +1,3 @@
"""
Python port of the business models from Rust.
"""

View File

@@ -0,0 +1,455 @@
#!/usr/bin/env python3
"""
FastAPI server providing CRUD operations for business models.
"""
import os
from datetime import datetime
from typing import List, Optional
from fastapi import Depends, FastAPI, HTTPException, Query
from fastapi.middleware.cors import CORSMiddleware
from sqlmodel import Session, SQLModel, create_engine, select
from models import (
Currency,
Customer,
Product,
ProductComponent,
ProductStatus,
ProductType,
Sale,
SaleItem,
SaleStatus,
)
# Create database
DATABASE_URL = os.environ.get("DATABASE_URL", "sqlite:///business.db")
engine = create_engine(DATABASE_URL, echo=False)
# Create tables
SQLModel.metadata.create_all(engine)
# Create FastAPI app
app = FastAPI(
title="Business API",
description="API for business models",
version="1.0.0",
docs_url="/docs",
redoc_url="/redoc",
openapi_url="/openapi.json",
)
# Add CORS middleware
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Dependency to get database session
def get_session():
with Session(engine) as session:
yield session
# Root endpoint
@app.get("/")
async def root():
return {"message": "Welcome to the Business API"}
# Currency endpoints
@app.post("/currencies/", response_model=Currency, tags=["Currencies"])
def create_currency(currency: Currency, session: Session = Depends(get_session)):
"""Create a new currency"""
session.add(currency)
session.commit()
session.refresh(currency)
return currency
@app.get("/currencies/", response_model=List[Currency], tags=["Currencies"])
def read_currencies(
skip: int = 0, limit: int = 100, session: Session = Depends(get_session)
):
"""Get all currencies"""
currencies = session.exec(select(Currency).offset(skip).limit(limit)).all()
return currencies
@app.get("/currencies/{currency_id}", response_model=Currency, tags=["Currencies"])
def read_currency(currency_id: int, session: Session = Depends(get_session)):
"""Get a currency by ID"""
currency = session.get(Currency, currency_id)
if not currency:
raise HTTPException(status_code=404, detail="Currency not found")
return currency
@app.put("/currencies/{currency_id}", response_model=Currency, tags=["Currencies"])
def update_currency(
currency_id: int, currency_data: Currency, session: Session = Depends(get_session)
):
"""Update a currency"""
currency = session.get(Currency, currency_id)
if not currency:
raise HTTPException(status_code=404, detail="Currency not found")
# Update currency attributes
currency_data_dict = currency_data.dict(exclude_unset=True)
for key, value in currency_data_dict.items():
setattr(currency, key, value)
session.add(currency)
session.commit()
session.refresh(currency)
return currency
@app.delete("/currencies/{currency_id}", tags=["Currencies"])
def delete_currency(currency_id: int, session: Session = Depends(get_session)):
"""Delete a currency"""
currency = session.get(Currency, currency_id)
if not currency:
raise HTTPException(status_code=404, detail="Currency not found")
session.delete(currency)
session.commit()
return {"message": "Currency deleted successfully"}
# Customer endpoints
@app.post("/customers/", response_model=Customer, tags=["Customers"])
def create_customer(customer: Customer, session: Session = Depends(get_session)):
"""Create a new customer"""
session.add(customer)
session.commit()
session.refresh(customer)
return customer
@app.get("/customers/", response_model=List[Customer], tags=["Customers"])
def read_customers(
skip: int = 0, limit: int = 100, session: Session = Depends(get_session)
):
"""Get all customers"""
customers = session.exec(select(Customer).offset(skip).limit(limit)).all()
return customers
@app.get("/customers/{customer_id}", response_model=Customer, tags=["Customers"])
def read_customer(customer_id: int, session: Session = Depends(get_session)):
"""Get a customer by ID"""
customer = session.get(Customer, customer_id)
if not customer:
raise HTTPException(status_code=404, detail="Customer not found")
return customer
@app.put("/customers/{customer_id}", response_model=Customer, tags=["Customers"])
def update_customer(
customer_id: int, customer_data: Customer, session: Session = Depends(get_session)
):
"""Update a customer"""
customer = session.get(Customer, customer_id)
if not customer:
raise HTTPException(status_code=404, detail="Customer not found")
# Update customer attributes
customer_data_dict = customer_data.dict(exclude_unset=True)
for key, value in customer_data_dict.items():
setattr(customer, key, value)
session.add(customer)
session.commit()
session.refresh(customer)
return customer
@app.delete("/customers/{customer_id}", tags=["Customers"])
def delete_customer(customer_id: int, session: Session = Depends(get_session)):
"""Delete a customer"""
customer = session.get(Customer, customer_id)
if not customer:
raise HTTPException(status_code=404, detail="Customer not found")
session.delete(customer)
session.commit()
return {"message": "Customer deleted successfully"}
# Product endpoints
@app.post("/products/", response_model=Product, tags=["Products"])
def create_product(product: Product, session: Session = Depends(get_session)):
"""Create a new product"""
session.add(product)
session.commit()
session.refresh(product)
return product
@app.get("/products/", response_model=List[Product], tags=["Products"])
def read_products(
skip: int = 0,
limit: int = 100,
category: Optional[str] = None,
status: Optional[ProductStatus] = None,
istemplate: Optional[bool] = None,
session: Session = Depends(get_session)
):
"""Get all products with optional filtering"""
query = select(Product)
if category:
query = query.where(Product.category == category)
if status:
query = query.where(Product.status == status)
if istemplate is not None:
query = query.where(Product.istemplate == istemplate)
products = session.exec(query.offset(skip).limit(limit)).all()
return products
@app.get("/products/{product_id}", response_model=Product, tags=["Products"])
def read_product(product_id: int, session: Session = Depends(get_session)):
"""Get a product by ID"""
product = session.get(Product, product_id)
if not product:
raise HTTPException(status_code=404, detail="Product not found")
return product
@app.put("/products/{product_id}", response_model=Product, tags=["Products"])
def update_product(
product_id: int, product_data: Product, session: Session = Depends(get_session)
):
"""Update a product"""
product = session.get(Product, product_id)
if not product:
raise HTTPException(status_code=404, detail="Product not found")
# Update product attributes
product_data_dict = product_data.dict(exclude_unset=True)
for key, value in product_data_dict.items():
setattr(product, key, value)
session.add(product)
session.commit()
session.refresh(product)
return product
@app.delete("/products/{product_id}", tags=["Products"])
def delete_product(product_id: int, session: Session = Depends(get_session)):
"""Delete a product"""
product = session.get(Product, product_id)
if not product:
raise HTTPException(status_code=404, detail="Product not found")
session.delete(product)
session.commit()
return {"message": "Product deleted successfully"}
# Product Component endpoints
@app.post("/products/{product_id}/components/", response_model=ProductComponent, tags=["Product Components"])
def create_product_component(
product_id: int, component: ProductComponent, session: Session = Depends(get_session)
):
"""Add a component to a product"""
product = session.get(Product, product_id)
if not product:
raise HTTPException(status_code=404, detail="Product not found")
component.product_id = product_id
session.add(component)
session.commit()
session.refresh(component)
return component
@app.get("/products/{product_id}/components/", response_model=List[ProductComponent], tags=["Product Components"])
def read_product_components(
product_id: int, session: Session = Depends(get_session)
):
"""Get all components for a product"""
product = session.get(Product, product_id)
if not product:
raise HTTPException(status_code=404, detail="Product not found")
return product.components
# Sale endpoints
@app.post("/sales/", response_model=Sale, tags=["Sales"])
def create_sale(sale: Sale, session: Session = Depends(get_session)):
"""Create a new sale"""
# Ensure customer exists if customer_id is provided
if sale.customer_id:
customer = session.get(Customer, sale.customer_id)
if not customer:
raise HTTPException(status_code=404, detail="Customer not found")
session.add(sale)
session.commit()
session.refresh(sale)
return sale
@app.get("/sales/", response_model=List[Sale], tags=["Sales"])
def read_sales(
skip: int = 0,
limit: int = 100,
status: Optional[SaleStatus] = None,
customer_id: Optional[int] = None,
session: Session = Depends(get_session)
):
"""Get all sales with optional filtering"""
query = select(Sale)
if status:
query = query.where(Sale.status == status)
if customer_id:
query = query.where(Sale.customer_id == customer_id)
sales = session.exec(query.offset(skip).limit(limit)).all()
return sales
@app.get("/sales/{sale_id}", response_model=Sale, tags=["Sales"])
def read_sale(sale_id: int, session: Session = Depends(get_session)):
"""Get a sale by ID"""
sale = session.get(Sale, sale_id)
if not sale:
raise HTTPException(status_code=404, detail="Sale not found")
return sale
@app.put("/sales/{sale_id}", response_model=Sale, tags=["Sales"])
def update_sale(
sale_id: int, sale_data: Sale, session: Session = Depends(get_session)
):
"""Update a sale"""
sale = session.get(Sale, sale_id)
if not sale:
raise HTTPException(status_code=404, detail="Sale not found")
# Update sale attributes
sale_data_dict = sale_data.dict(exclude_unset=True)
for key, value in sale_data_dict.items():
setattr(sale, key, value)
session.add(sale)
session.commit()
session.refresh(sale)
return sale
@app.delete("/sales/{sale_id}", tags=["Sales"])
def delete_sale(sale_id: int, session: Session = Depends(get_session)):
"""Delete a sale"""
sale = session.get(Sale, sale_id)
if not sale:
raise HTTPException(status_code=404, detail="Sale not found")
session.delete(sale)
session.commit()
return {"message": "Sale deleted successfully"}
# Sale Item endpoints
@app.post("/sales/{sale_id}/items/", response_model=SaleItem, tags=["Sale Items"])
def create_sale_item(
sale_id: int, item: SaleItem, session: Session = Depends(get_session)
):
"""Add an item to a sale"""
sale = session.get(Sale, sale_id)
if not sale:
raise HTTPException(status_code=404, detail="Sale not found")
item.sale_id = sale_id
session.add(item)
session.commit()
session.refresh(item)
# Update the sale's total amount
sale.add_item(item)
session.add(sale)
session.commit()
return item
@app.get("/sales/{sale_id}/items/", response_model=List[SaleItem], tags=["Sale Items"])
def read_sale_items(
sale_id: int, session: Session = Depends(get_session)
):
"""Get all items for a sale"""
sale = session.get(Sale, sale_id)
if not sale:
raise HTTPException(status_code=404, detail="Sale not found")
return sale.items
# Convenience endpoints
@app.put("/sales/{sale_id}/status/{status}", response_model=Sale, tags=["Convenience"])
def update_sale_status(
sale_id: int, status: SaleStatus, session: Session = Depends(get_session)
):
"""Update the status of a sale"""
sale = session.get(Sale, sale_id)
if not sale:
raise HTTPException(status_code=404, detail="Sale not found")
sale.update_status(status)
session.add(sale)
session.commit()
session.refresh(sale)
return sale
@app.get("/products/available/", response_model=List[Product], tags=["Convenience"])
def get_available_products(
istemplate: Optional[bool] = False,
session: Session = Depends(get_session)
):
"""Get all available products"""
query = select(Product).where(
Product.status == ProductStatus.AVAILABLE,
Product.purchase_till > datetime.utcnow(),
Product.istemplate == istemplate
)
products = session.exec(query).all()
return products
@app.get("/customers/{customer_id}/sales/", response_model=List[Sale], tags=["Convenience"])
def get_customer_sales(
customer_id: int,
status: Optional[SaleStatus] = None,
session: Session = Depends(get_session)
):
"""Get all sales for a customer"""
customer = session.get(Customer, customer_id)
if not customer:
raise HTTPException(status_code=404, detail="Customer not found")
query = select(Sale).where(Sale.customer_id == customer_id)
if status:
query = query.where(Sale.status == status)
sales = session.exec(query).all()
return sales
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More