This commit is contained in:
kristof 2025-04-04 10:45:09 +02:00
parent 4a65f8255a
commit b37b9da8b5
35 changed files with 3372 additions and 53 deletions

1417
herodb/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -16,3 +16,11 @@ 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.15.1"
[[example]]
name = "rhai_demo"
path = "examples/rhai_demo.rs"

View File

@ -0,0 +1,14 @@
in @src/zaz/rhai
make wrappers for the src/zaz/models and see how to use them from src/zaz/cmd/examples.rs
how to do this you can find in rhaibook/rust
the wrappers need to be simple and return the result and if an error return a proper error into the rhai environment so the rhai script using the wrappers will get appropriate error
all wrapped functions need to be registered into the rhai engine
keep all code as small as possible
the creation of the db should not be done in the rhai script,
this shouldd be done before we call the rhai script
a db is context outside of the script execution

View File

@ -0,0 +1,38 @@
//! Demonstrates how to use the Rhai wrappers for our models
use herodb::zaz::rhai::{run_script_file, run_example_script};
use std::path::PathBuf;
use std::fs;
use std::time::SystemTime;
fn main() -> Result<(), String> {
println!("=== RHAI MODEL WRAPPERS DEMONSTRATION ===");
// Run our test script that creates model objects
let test_script_path = "src/zaz/rhai/test.rhai";
println!("\n1. Running model creation test script: {}", test_script_path);
run_script_file(test_script_path)?;
// Create temporary directory for DB example
let temp_dir = create_temp_dir()
.map_err(|e| format!("Failed to create temp dir: {}", e))?;
// Run our example script that uses the DB
println!("\n2. Running example with database at: {:?}", temp_dir);
run_example_script(temp_dir.to_str().unwrap())?;
println!("\n=== DEMONSTRATION COMPLETED SUCCESSFULLY ===");
Ok(())
}
/// Creates a simple temporary directory
fn create_temp_dir() -> std::io::Result<PathBuf> {
let temp_dir = std::env::temp_dir();
let random_name = format!("rhai-demo-{}", SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_millis());
let path = temp_dir.join(random_name);
fs::create_dir_all(&path)?;
Ok(path)
}

1
herodb/rhaibook Symbolic link
View File

@ -0,0 +1 @@
../../rhaj/rhai_engine/rhaibook

1
herodb/rhaiexamples Symbolic link
View File

@ -0,0 +1 @@
../../rhaj/rhai_engine/rhaiexamples

5
herodb/src/bin/dummy.rs Normal file
View File

@ -0,0 +1,5 @@
//! Simple binary to test if binaries are working
fn main() {
println!("Hello from dummy binary!");
}

View File

@ -0,0 +1,7 @@
//! Simple binary to demonstrate Rhai wrapper functionality
use herodb::zaz::cmd::examples::run_rhai_demo;
fn main() -> Result<(), String> {
println!("Running Rhai wrapper examples for zaz models...");
run_rhai_demo()
}

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,70 @@
use serde::{Deserialize, Serialize};
use crate::core::{SledModel, Storable};
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 pubkeys: Vec<String>, // public keys of the member
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
}
/// 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
pub members: Vec<Member>, // members of the circle
}
impl Circle {
/// Create a new circle
pub fn new(id: u32, name: String, description: String) -> Self {
Self {
id,
name,
description,
members: Vec::new(),
}
}
/// Add a member to the circle
pub fn add_member(&mut self, member: Member) {
self.members.push(member);
}
/// 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 (provides default dump/load)
impl Storable for Circle {}
// Implement SledModel trait
impl SledModel for Circle {
fn get_id(&self) -> String {
self.id.to_string()
}
fn db_prefix() -> &'static str {
"circle"
}
}

View File

@ -1,25 +1,9 @@
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
pub mod circle;
pub mod name;
// 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, Currency, ProductComponent, ProductType, ProductStatus};
pub use sale::Sale;
pub use shareholder::Shareholder;
pub use circle::{Circle, Member, Role};
pub use name::{Name, Record, RecordType};
// Re-export database components
// pub use db::{DB, DBError, DBResult, Model, ModelMetadata}; // Removed old DB re-exports
pub use crate::db::core::{SledDB, SledDBError, SledDBResult, Storable, SledModel}; // Re-export Sled DB components
// Re-export migration components - Removed
// pub use migration::{Migrator, MigrationError, MigrationResult};
pub use crate::core::{SledDB, SledDBError, SledDBResult, Storable, SledModel, DB};

View File

@ -1,8 +1,9 @@
// Declare the models submodule
#[path = "models/lib.rs"] // Tell compiler where to find models module source
pub mod models;
pub mod circle;
pub mod name;
// Declare the db submodule with the new database implementation
#[path = "db/mod.rs"] // Tell compiler where to find db module source
pub mod db;
// Re-export all model types for convenience
pub use circle::{Circle, Member, Role};
pub use name::{Name, Record, RecordType};
// Re-export database components from core module
pub use crate::core::{SledDB, SledDBError, SledDBResult, Storable, SledModel, DB};

View File

@ -0,0 +1,72 @@
use serde::{Deserialize, Serialize};
use crate::core::{SledModel, Storable};
/// 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 (provides default dump/load)
impl Storable for Name {}
// Implement SledModel trait
impl SledModel for Name {
fn get_id(&self) -> String {
self.id.to_string()
}
fn db_prefix() -> &'static str {
"name"
}
}

View File

@ -1,4 +1,4 @@
use crate::core::base::*;
use crate::db::base::*;
use std::any::TypeId;
use std::collections::HashMap;
use std::path::{Path, PathBuf};

View File

@ -4,8 +4,9 @@
//! and includes support for defining and working with data models.
// Core modules
pub mod core;
mod db;
mod error;
pub mod server;
// Domain-specific modules
pub mod zaz;

View File

@ -1,12 +1,49 @@
//! Main entry point for running HeroDB examples
//! Main entry point for the HeroDB server
//!
//! This file serves as the entry point for the HeroDB server,
//! which provides a web API for accessing HeroDB functionality.
use herodb::zaz::cmd::examples::run_db_examples;
use std::path::PathBuf;
fn main() {
println!("Starting HeroDB examples...");
// We need tokio for the async runtime
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Print a welcome message
println!("Starting HeroDB server...");
match run_db_examples() {
Ok(_) => println!("Examples completed successfully!"),
Err(e) => eprintln!("Error running examples: {}", e),
}
// Set up default values
let host = "127.0.0.1";
let port = 3002;
// Create a temporary directory for the database
let db_path = match create_temp_dir() {
Ok(path) => {
println!("Using temporary DB path: {:?}", path);
path
}
Err(e) => {
eprintln!("Error creating temporary directory: {}", e);
return Err(e.into());
}
};
println!("Starting server on {}:{}", host, port);
println!("API Documentation: http://{}:{}/docs", host, port);
// Start the server
herodb::server::start_server(db_path, host, port).await?;
Ok(())
}
/// Creates a simple temporary directory for the database
fn create_temp_dir() -> std::io::Result<PathBuf> {
let temp_dir = std::env::temp_dir();
let random_name = format!("herodb-{}", std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_millis());
let path = temp_dir.join(random_name);
std::fs::create_dir_all(&path)?;
Ok(path)
}

View File

@ -0,0 +1,63 @@
# 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
### 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
## 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,35 @@
use serde::{Deserialize, Serialize};
use crate::core::{SledModel, Storable};
/// 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
}
impl Calendar {
/// Create a new calendar
pub fn new(id: u32, title: String, description: String) -> Self {
Self {
id,
title,
description,
}
}
}
// Implement Storable trait (provides default dump/load)
impl Storable for Calendar {}
// Implement SledModel trait
impl SledModel for Calendar {
fn get_id(&self) -> String {
self.id.to_string()
}
fn db_prefix() -> &'static str {
"calendar"
}
}

View File

@ -0,0 +1,73 @@
use serde::{Deserialize, Serialize};
use crate::core::{SledModel, Storable};
use chrono::{DateTime, 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 email: String,
pub group: String, // Reference to a dns name, each group has a globally unique dns
}
impl Contact {
/// Create a new contact
pub fn new(id: u32, first_name: String, last_name: String, email: String, group: String) -> Self {
let now = Utc::now().timestamp();
Self {
id,
created_at: now,
modified_at: now,
first_name,
last_name,
email,
group,
}
}
/// 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();
}
/// Get the full name of the contact
pub fn full_name(&self) -> String {
format!("{} {}", self.first_name, self.last_name)
}
}
// Implement Storable trait (provides default dump/load)
impl Storable for Contact {}
// Implement SledModel trait
impl SledModel for Contact {
fn get_id(&self) -> String {
self.id.to_string()
}
fn db_prefix() -> &'static str {
"contact"
}
}

View File

@ -0,0 +1,94 @@
use serde::{Deserialize, Serialize};
use crate::core::{SledModel, Storable};
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
}
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(),
},
}
}
/// 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();
}
}
// Implement Storable trait (provides default dump/load)
impl Storable for Event {}
// Implement SledModel trait
impl SledModel for Event {
fn get_id(&self) -> String {
self.id.to_string()
}
fn db_prefix() -> &'static str {
"event"
}
}

View File

@ -0,0 +1,13 @@
pub mod calendar;
pub mod event;
pub mod mail;
pub mod contacts;
// 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;
// Re-export database components from core module
pub use crate::core::{SledDB, SledDBError, SledDBResult, Storable, SledModel, DB};

View File

@ -0,0 +1,85 @@
use serde::{Deserialize, Serialize};
use crate::core::{SledModel, Storable};
use chrono::{DateTime, 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, // Database ID (assigned by DBHandler)
// Content fields
pub uid: u32, // Unique identifier of the message (in the circle)
pub seq_num: u32, // IMAP sequence number (in the mailbox)
pub mailbox: String, // The mailbox this email belongs to
pub message: String, // The email body content
pub attachments: Vec<Attachment>, // Any file attachments
// IMAP specific fields
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 information (contains From, To, Subject, etc.)
}
/// 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,
pub message_id: String,
}
impl Email {
/// Create a new email
pub fn new(id: u32, uid: u32, seq_num: u32, mailbox: String, message: String) -> Self {
Self {
id,
uid,
seq_num,
mailbox,
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);
}
/// Set the envelope for this email
pub fn set_envelope(&mut self, envelope: Envelope) {
self.envelope = Some(envelope);
}
}
// Implement Storable trait (provides default dump/load)
impl Storable for Email {}
// Implement SledModel trait
impl SledModel for Email {
fn get_id(&self) -> String {
self.id.to_string()
}
fn db_prefix() -> &'static str {
"email"
}
}

View File

@ -0,0 +1,13 @@
pub mod calendar;
pub mod event;
pub mod mail;
pub mod contacts;
// 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;
// Re-export database components from core module
pub use crate::core::{SledDB, SledDBError, SledDBResult, Storable, SledModel, DB};

View File

@ -0,0 +1,377 @@
use rhai::{Engine, EvalAltResult, Dynamic, Map};
use std::sync::Arc;
use std::fs;
use crate::core::{DB, SledDBResult};
use crate::zaz::factory::create_zaz_db;
use crate::zaz::models::{Company, User, Shareholder};
/// A wrapper around the Rhai Engine that provides additional functionality
pub struct RhaiEngine {
engine: Engine,
}
impl RhaiEngine {
/// Creates a new Rhai engine with all model functions registered
pub fn new() -> Self {
let mut engine = Engine::new();
rhai_engine
}
/// Get a reference to the underlying Rhai engine
pub fn get_engine(&mut self) -> &mut Engine {
&mut self.engine
}
/// Register a named function with the engine
///
/// This allows adding wrapped functions to the engine in a generic way,
/// so they can be called from Rhai scripts
pub fn register_fn<F>(&mut self, name: &str, f: F) {
//in script engine
}
/// Run a Rhai script from a file
///
/// This method reads the specified script file and executes it
/// using the current engine instance.
pub fn run_script(&mut self, file_path: &str) -> Result<(), String> {
// Read the script file content
let script = match fs::read_to_string(file_path) {
Ok(content) => content,
Err(e) => return Err(format!("Failed to read script file: {}", e))
};
// Execute the script
match self.engine.eval::<()>(&script) {
Ok(_) => {
println!("Rhai script executed successfully: {}", file_path);
Ok(())
},
Err(e) => {
Err(format!("Rhai script execution failed: {}", e))
}
}
}
/// Run a Rhai script from a text string
///
/// This method executes the provided script text using the current engine instance.
pub fn run(&mut self, script_text: &str) -> Result<(), String> {
// Execute the script
match self.engine.eval::<()>(script_text) {
Ok(_) => {
println!("Rhai script text executed successfully");
Ok(())
},
Err(e) => {
Err(format!("Rhai script execution failed: {}", e))
}
}
}
}
/// Create a new Rhai engine with all model functions registered
/// This function is maintained for backward compatibility
pub fn new() -> Engine {
let rhai_engine = RhaiEngine::new();
rhai_engine.engine
}
/// Register a named function with the engine
/// This function is maintained for backward compatibility
pub fn register_named_fn<F>(engine: &mut Engine, name: &str, f: F)
where
F: 'static + Fn(&mut Engine) -> Result<(), Box<EvalAltResult>>
{
match f(engine) {
Ok(_) => println!("Function '{}' registered successfully", name),
Err(e) => eprintln!("Failed to register function '{}': {}", name, e)
}
}
/// Run a Rhai script from a file
/// This function is maintained for backward compatibility
pub fn run_script(file_path: &str) -> Result<(), String> {
let mut rhai_engine = RhaiEngine::new();
rhai_engine.run_script(file_path)
}
/// Run a Rhai script from a text string
/// This function is maintained for backward compatibility
pub fn run(script_text: &str) -> Result<(), String> {
let mut rhai_engine = RhaiEngine::new();
rhai_engine.run(script_text)
}
/// Utility functions to safely convert between Rhai Dynamic types and Rust types
impl RhaiEngine {
/// Helper function to convert a value to a Rhai Dynamic type with proper error handling
pub fn to_dynamic<T: Clone + Into<Dynamic>>(&self, value: T) -> Dynamic {
value.into()
}
/// Helper function to safely get a property from a map with proper error handling
pub fn get_property<T: Clone + 'static>(&self, map: &Map, key: &str) -> Result<T, String>
where Dynamic: TryInto<T> {
match map.get(key) {
Some(value) => {
match value.clone().try_into() {
Ok(result) => Ok(result),
Err(_) => Err(format!("Property '{}' has wrong type", key))
}
},
None => Err(format!("Property '{}' not found", key))
}
}
/// Register CRUD operations for Company
pub fn register_company_crud(&mut self) {
// Create company from a map of properties
self.engine.register_result_fn("create_company_from_map",
|map: Map| -> Result<Company, Box<EvalAltResult>> {
// Extract required fields with proper error handling
let name = match map.get("name") {
Some(val) => match val.clone().try_into::<String>() {
Ok(s) => s,
Err(_) => return Err("'name' must be a string".into())
},
None => return Err("Missing required field: 'name'".into())
};
let reg_number = match map.get("registration_number") {
Some(val) => match val.clone().try_into::<String>() {
Ok(s) => s,
Err(_) => return Err("'registration_number' must be a string".into())
},
None => return Err("Missing required field: 'registration_number'".into())
};
// Extract other fields (example with default values)
let id = match map.get("id") {
Some(val) => match val.clone().try_into::<i64>() {
Ok(id) => id as u32,
Err(_) => return Err("'id' must be an integer".into())
},
None => return Err("Missing required field: 'id'".into())
};
// Here you would call your actual company creation logic
// This is just a stub that would be replaced with actual implementation
Err("Not fully implemented: would connect to database here".into())
}
);
// Read company by ID
self.engine.register_result_fn("get_company_by_id",
|id: i64| -> Result<Company, Box<EvalAltResult>> {
if id <= 0 {
return Err(format!("Invalid company ID: {}", id).into());
}
// Here you would query the database for the company
// This is just a stub that would be replaced with actual implementation
Err(format!("Company with ID {} not found", id).into())
}
);
// Update company
self.engine.register_result_fn("update_company",
|company: Company| -> Result<(), Box<EvalAltResult>> {
// Here you would update the company in the database
// This is just a stub that would be replaced with actual implementation
Err(format!("Failed to update company: {}", company.name).into())
}
);
// Delete company
self.engine.register_result_fn("delete_company",
|id: i64| -> Result<(), Box<EvalAltResult>> {
if id <= 0 {
return Err(format!("Invalid company ID: {}", id).into());
}
// Here you would delete the company from the database
// This is just a stub that would be replaced with actual implementation
Err(format!("Failed to delete company with ID {}", id).into())
}
);
// List companies with filtering capabilities
self.engine.register_result_fn("list_companies",
|filter: Map| -> Result<Vec<Company>, Box<EvalAltResult>> {
// Here you would query the database with the filter
// This is just a stub that would be replaced with actual implementation
Err("No companies found matching filter".into())
}
);
}
/// Register CRUD operations for User
pub fn register_user_crud(&mut self) {
// Create user from a map of properties
self.engine.register_result_fn("create_user_from_map",
|map: Map| -> Result<User, Box<EvalAltResult>> {
// Extract required fields with proper error handling
let name = match map.get("name") {
Some(val) => match val.clone().try_into::<String>() {
Ok(s) => s,
Err(_) => return Err("'name' must be a string".into())
},
None => return Err("Missing required field: 'name'".into())
};
let email = match map.get("email") {
Some(val) => match val.clone().try_into::<String>() {
Ok(s) => s,
Err(_) => return Err("'email' must be a string".into())
},
None => return Err("Missing required field: 'email'".into())
};
// Extract other fields
let id = match map.get("id") {
Some(val) => match val.clone().try_into::<i64>() {
Ok(id) => id as u32,
Err(_) => return Err("'id' must be an integer".into())
},
None => return Err("Missing required field: 'id'".into())
};
// This is a stub that would be replaced with actual implementation
Err("Not fully implemented: would create user in database".into())
}
);
// Read user by ID
self.engine.register_result_fn("get_user_by_id",
|id: i64| -> Result<User, Box<EvalAltResult>> {
if id <= 0 {
return Err(format!("Invalid user ID: {}", id).into());
}
// Implementation would query database
Err(format!("User with ID {} not found", id).into())
}
);
// Update user
self.engine.register_result_fn("update_user",
|user: User| -> Result<(), Box<EvalAltResult>> {
// Implementation would update the user in the database
Err(format!("Failed to update user: {}", user.name).into())
}
);
// Delete user
self.engine.register_result_fn("delete_user",
|id: i64| -> Result<(), Box<EvalAltResult>> {
if id <= 0 {
return Err(format!("Invalid user ID: {}", id).into());
}
// Implementation would delete from database
Err(format!("Failed to delete user with ID {}", id).into())
}
);
}
/// Register CRUD operations for Shareholder
pub fn register_shareholder_crud(&mut self) {
// Create shareholder from a map of properties
self.engine.register_result_fn("create_shareholder_from_map",
|map: Map| -> Result<Shareholder, Box<EvalAltResult>> {
// Extract required fields with proper error handling
let name = match map.get("name") {
Some(val) => match val.clone().try_into::<String>() {
Ok(s) => s,
Err(_) => return Err("'name' must be a string".into())
},
None => return Err("Missing required field: 'name'".into())
};
let company_id = match map.get("company_id") {
Some(val) => match val.clone().try_into::<i64>() {
Ok(id) => id as u32,
Err(_) => return Err("'company_id' must be an integer".into())
},
None => return Err("Missing required field: 'company_id'".into())
};
let shares = match map.get("shares") {
Some(val) => match val.clone().try_into::<i64>() {
Ok(num) => num,
Err(_) => return Err("'shares' must be an integer".into())
},
None => return Err("Missing required field: 'shares'".into())
};
// This is a stub that would be replaced with actual implementation
Err("Not fully implemented: would create shareholder in database".into())
}
);
// Read shareholder by ID
self.engine.register_result_fn("get_shareholder_by_id",
|id: i64| -> Result<Shareholder, Box<EvalAltResult>> {
if id <= 0 {
return Err(format!("Invalid shareholder ID: {}", id).into());
}
// Implementation would query database
Err(format!("Shareholder with ID {} not found", id).into())
}
);
// Update shareholder
self.engine.register_result_fn("update_shareholder",
|shareholder: Shareholder| -> Result<(), Box<EvalAltResult>> {
// Implementation would update the shareholder in the database
Err(format!("Failed to update shareholder ID: {}", shareholder.id).into())
}
);
// Delete shareholder
self.engine.register_result_fn("delete_shareholder",
|id: i64| -> Result<(), Box<EvalAltResult>> {
if id <= 0 {
return Err(format!("Invalid shareholder ID: {}", id).into());
}
// Implementation would delete from database
Err(format!("Failed to delete shareholder with ID {}", id).into())
}
);
}
/// Helper function to safely convert a map to a strongly-typed object
pub fn map_to_object<T>(&self, map: &Map, converter: fn(&Map) -> Result<T, String>) -> Result<T, String> {
converter(map)
}
/// Helper function to handle Dynamic types safely
pub fn get_dynamic_value(&self, dynamic: &Dynamic, key: &str) -> Result<Dynamic, String> {
if let Some(map) = dynamic.try_cast::<Map>() {
match map.get(key) {
Some(value) => Ok(value.clone()),
None => Err(format!("Key '{}' not found in map", key))
}
} else {
Err("Dynamic value is not a map".to_string())
}
}
/// Helper function to extract a value of a specific type from a Dynamic value
pub fn extract_value<T: Clone + 'static>(&self, dynamic: &Dynamic, key: &str) -> Result<T, String>
where Dynamic: TryInto<T> {
let value = self.get_dynamic_value(dynamic, key)?;
match value.clone().try_into() {
Ok(result) => Ok(result),
Err(_) => Err(format!("Value for key '{}' has wrong type", key))
}
}
}

368
herodb/src/server/api.rs Normal file
View File

@ -0,0 +1,368 @@
//! API module for the HeroDB server
use crate::core::DB;
use crate::server::models::{ApiError, SuccessResponse, UserCreate, UserUpdate, SaleCreate, SaleStatusUpdate,
UserResponse, SuccessOrError
};
use crate::zaz::create_zaz_db;
use crate::zaz::models::*;
use crate::zaz::models::sale::{SaleStatus, SaleItem};
use crate::zaz::models::product::Currency;
use poem_openapi::{
param::Path,
payload::Json,
OpenApi,
};
use std::path::PathBuf;
use std::sync::{Arc, Mutex};
use chrono::Utc;
/// API handler struct that holds the database connection
pub struct Api {
db: Arc<Mutex<DB>>,
}
impl Api {
/// Create a new API instance with the given database path
pub fn new(db_path: PathBuf) -> Self {
// Create the DB
let db = match create_zaz_db(db_path) {
Ok(db) => db,
Err(e) => {
eprintln!("Failed to create DB: {}", e);
panic!("Failed to initialize database");
}
};
// Wrap in Arc<Mutex> for thread safety
Self {
db: Arc::new(Mutex::new(db)),
}
}
}
/// OpenAPI implementation for the API
#[OpenApi]
impl Api {
/// Get all users
#[oai(path = "/users", method = "get")]
async fn get_users(&self) -> Json<String> {
let db = self.db.lock().unwrap();
match db.list::<User>() {
Ok(users) => {
// Convert to JSON manually
let json_result = serde_json::to_string(&users).unwrap_or_else(|_| "[]".to_string());
Json(json_result)
}
Err(e) => {
eprintln!("Error listing users: {}", e);
Json("[]".to_string())
}
}
}
/// Get a user by ID
#[oai(path = "/users/:id", method = "get")]
async fn get_user(&self, id: Path<u32>) -> UserResponse {
let db = self.db.lock().unwrap();
match db.get::<User>(&id.0.to_string()) {
Ok(user) => {
// Convert to JSON manually
let json_result = serde_json::to_string(&user).unwrap_or_else(|_| "{}".to_string());
UserResponse::Ok(Json(json_result))
}
Err(e) => {
eprintln!("Error getting user: {}", e);
UserResponse::NotFound(Json(ApiError::not_found(id.0)))
}
}
}
/// Create a new user
#[oai(path = "/users", method = "post")]
async fn create_user(
&self,
user: Json<UserCreate>,
) -> UserResponse {
let db = self.db.lock().unwrap();
// Find the next available ID
let users: Vec<User> = match db.list() {
Ok(users) => users,
Err(e) => {
eprintln!("Error listing users: {}", e);
return UserResponse::InternalError(Json(ApiError::internal_error("Failed to generate ID")));
}
};
let next_id = users.iter().map(|u| u.id).max().unwrap_or(0) + 1;
// Create the new user
let new_user = User::new(
next_id,
user.name.clone(),
user.email.clone(),
user.password.clone(),
user.company.clone(),
user.role.clone(),
);
// Save the user
match db.set(&new_user) {
Ok(_) => {
let json_result = serde_json::to_string(&new_user).unwrap_or_else(|_| "{}".to_string());
UserResponse::Ok(Json(json_result))
},
Err(e) => {
eprintln!("Error creating user: {}", e);
UserResponse::InternalError(Json(ApiError::internal_error("Failed to create user")))
}
}
}
/// Update a user
#[oai(path = "/users/:id", method = "put")]
async fn update_user(
&self,
id: Path<u32>,
user: Json<UserUpdate>,
) -> UserResponse {
let db = self.db.lock().unwrap();
// Get the existing user
let existing_user: User = match db.get(&id.0.to_string()) {
Ok(user) => user,
Err(e) => {
eprintln!("Error getting user: {}", e);
return UserResponse::NotFound(Json(ApiError::not_found(id.0)));
}
};
// Update the user
let updated_user = User {
id: existing_user.id,
name: user.name.clone().unwrap_or(existing_user.name),
email: user.email.clone().unwrap_or(existing_user.email),
password: user.password.clone().unwrap_or(existing_user.password),
company: user.company.clone().unwrap_or(existing_user.company),
role: user.role.clone().unwrap_or(existing_user.role),
created_at: existing_user.created_at,
updated_at: Utc::now(),
};
// Save the updated user
match db.set(&updated_user) {
Ok(_) => {
let json_result = serde_json::to_string(&updated_user).unwrap_or_else(|_| "{}".to_string());
UserResponse::Ok(Json(json_result))
},
Err(e) => {
eprintln!("Error updating user: {}", e);
UserResponse::InternalError(Json(ApiError::internal_error("Failed to update user")))
}
}
}
/// Delete a user
#[oai(path = "/users/:id", method = "delete")]
async fn delete_user(&self, id: Path<u32>) -> SuccessOrError {
let db = self.db.lock().unwrap();
match db.delete::<User>(&id.0.to_string()) {
Ok(_) => SuccessOrError::Ok(Json(SuccessResponse {
success: true,
message: format!("User with ID {} deleted", id.0),
})),
Err(e) => {
eprintln!("Error deleting user: {}", e);
SuccessOrError::NotFound(Json(ApiError::not_found(id.0)))
}
}
}
/// Get all products
#[oai(path = "/products", method = "get")]
async fn get_products(&self) -> Json<String> {
let db = self.db.lock().unwrap();
match db.list::<Product>() {
Ok(products) => {
let json_result = serde_json::to_string(&products).unwrap_or_else(|_| "[]".to_string());
Json(json_result)
}
Err(e) => {
eprintln!("Error listing products: {}", e);
Json("[]".to_string())
}
}
}
/// Get a product by ID
#[oai(path = "/products/:id", method = "get")]
async fn get_product(&self, id: Path<u32>) -> UserResponse {
let db = self.db.lock().unwrap();
match db.get::<Product>(&id.0.to_string()) {
Ok(product) => {
let json_result = serde_json::to_string(&product).unwrap_or_else(|_| "{}".to_string());
UserResponse::Ok(Json(json_result))
}
Err(e) => {
eprintln!("Error getting product: {}", e);
UserResponse::NotFound(Json(ApiError::not_found(id.0)))
}
}
}
/// Get all sales
#[oai(path = "/sales", method = "get")]
async fn get_sales(&self) -> Json<String> {
let db = self.db.lock().unwrap();
match db.list::<Sale>() {
Ok(sales) => {
let json_result = serde_json::to_string(&sales).unwrap_or_else(|_| "[]".to_string());
Json(json_result)
}
Err(e) => {
eprintln!("Error listing sales: {}", e);
Json("[]".to_string())
}
}
}
/// Get a sale by ID
#[oai(path = "/sales/:id", method = "get")]
async fn get_sale(&self, id: Path<u32>) -> UserResponse {
let db = self.db.lock().unwrap();
match db.get::<Sale>(&id.0.to_string()) {
Ok(sale) => {
let json_result = serde_json::to_string(&sale).unwrap_or_else(|_| "{}".to_string());
UserResponse::Ok(Json(json_result))
}
Err(e) => {
eprintln!("Error getting sale: {}", e);
UserResponse::NotFound(Json(ApiError::not_found(id.0)))
}
}
}
/// Create a new sale
#[oai(path = "/sales", method = "post")]
async fn create_sale(
&self,
sale: Json<SaleCreate>,
) -> UserResponse {
let db = self.db.lock().unwrap();
// Find the next available ID
let sales: Vec<Sale> = match db.list() {
Ok(sales) => sales,
Err(e) => {
eprintln!("Error listing sales: {}", e);
return UserResponse::InternalError(Json(ApiError::internal_error("Failed to generate ID")));
}
};
let next_id = sales.iter().map(|s| s.id).max().unwrap_or(0) + 1;
// Create the new sale
let mut new_sale = Sale::new(
next_id,
sale.company_id,
sale.buyer_name.clone(),
sale.buyer_email.clone(),
sale.currency_code.clone(),
SaleStatus::Pending,
);
// Add items if provided
if let Some(items) = &sale.items {
for (i, item) in items.iter().enumerate() {
let item_id = (i + 1) as u32;
let active_till = Utc::now() + chrono::Duration::days(365); // Default 1 year
let sale_item = SaleItem::new(
item_id,
next_id,
item.product_id,
item.name.clone(),
item.quantity,
Currency {
amount: item.unit_price,
currency_code: sale.currency_code.clone(),
},
active_till,
);
new_sale.add_item(sale_item);
}
}
// Save the sale
match db.set(&new_sale) {
Ok(_) => {
let json_result = serde_json::to_string(&new_sale).unwrap_or_else(|_| "{}".to_string());
UserResponse::Ok(Json(json_result))
}
Err(e) => {
eprintln!("Error creating sale: {}", e);
UserResponse::InternalError(Json(ApiError::internal_error("Failed to create sale")))
}
}
}
/// Update a sale status
#[oai(path = "/sales/:id/status", method = "put")]
async fn update_sale_status(
&self,
id: Path<u32>,
status: Json<SaleStatusUpdate>,
) -> UserResponse {
let db = self.db.lock().unwrap();
// Get the existing sale
let mut existing_sale: Sale = match db.get(&id.0.to_string()) {
Ok(sale) => sale,
Err(e) => {
eprintln!("Error getting sale: {}", e);
return UserResponse::NotFound(Json(ApiError::not_found(id.0)));
}
};
// Parse and update the status
let new_status = match status.parse_status() {
Ok(status) => status,
Err(e) => return UserResponse::InternalError(Json(e)),
};
// Update the status
existing_sale.update_status(new_status);
// Save the updated sale
match db.set(&existing_sale) {
Ok(_) => {
let json_result = serde_json::to_string(&existing_sale).unwrap_or_else(|_| "{}".to_string());
UserResponse::Ok(Json(json_result))
}
Err(e) => {
eprintln!("Error updating sale: {}", e);
UserResponse::InternalError(Json(ApiError::internal_error("Failed to update sale")))
}
}
}
/// Delete a sale
#[oai(path = "/sales/:id", method = "delete")]
async fn delete_sale(&self, id: Path<u32>) -> SuccessOrError {
let db = self.db.lock().unwrap();
match db.delete::<Sale>(&id.0.to_string()) {
Ok(_) => SuccessOrError::Ok(Json(SuccessResponse {
success: true,
message: format!("Sale with ID {} deleted", id.0),
})),
Err(e) => {
eprintln!("Error deleting sale: {}", e);
SuccessOrError::NotFound(Json(ApiError::not_found(id.0)))
}
}
}
}

View File

@ -0,0 +1,289 @@
//! Extensions to make the zaz models compatible with OpenAPI
//!
//! This module adds the necessary traits and implementations to make the
//! existing zaz models work with OpenAPI.
use poem_openapi::types::{ToSchema, Type};
use chrono::{DateTime, Utc};
use crate::zaz::models::*;
// Make DateTime<Utc> compatible with OpenAPI
impl Type for DateTime<Utc> {
const IS_REQUIRED: bool = true;
type RawValueType = String;
type RawElementValueType = Self::RawValueType;
fn name() -> std::borrow::Cow<'static, str> {
"DateTime".into()
}
fn schema_ref() -> std::borrow::Cow<'static, str> {
"string".into()
}
fn as_raw_value(&self) -> Option<Self::RawValueType> {
Some(self.to_rfc3339())
}
fn raw_element_iter<'a>(
&'a self,
) -> Box<dyn Iterator<Item = Self::RawElementValueType> + 'a> {
Box::new(self.as_raw_value().into_iter())
}
}
// Make Currency compatible with OpenAPI
impl Type for Currency {
const IS_REQUIRED: bool = true;
type RawValueType = serde_json::Value;
type RawElementValueType = Self::RawValueType;
fn name() -> std::borrow::Cow<'static, str> {
"Currency".into()
}
fn schema_ref() -> std::borrow::Cow<'static, str> {
"object".into()
}
fn as_raw_value(&self) -> Option<Self::RawValueType> {
Some(serde_json::json!({
"amount": self.amount,
"currency_code": self.currency_code
}))
}
fn raw_element_iter<'a>(
&'a self,
) -> Box<dyn Iterator<Item = Self::RawElementValueType> + 'a> {
Box::new(self.as_raw_value().into_iter())
}
}
// Make SaleStatus compatible with OpenAPI
impl Type for SaleStatus {
const IS_REQUIRED: bool = true;
type RawValueType = String;
type RawElementValueType = Self::RawValueType;
fn name() -> std::borrow::Cow<'static, str> {
"SaleStatus".into()
}
fn schema_ref() -> std::borrow::Cow<'static, str> {
"string".into()
}
fn as_raw_value(&self) -> Option<Self::RawValueType> {
Some(match self {
SaleStatus::Pending => "pending".to_string(),
SaleStatus::Completed => "completed".to_string(),
SaleStatus::Cancelled => "cancelled".to_string(),
})
}
fn raw_element_iter<'a>(
&'a self,
) -> Box<dyn Iterator<Item = Self::RawElementValueType> + 'a> {
Box::new(self.as_raw_value().into_iter())
}
}
// Schema generation for User
impl ToSchema for User {
fn schema() -> poem_openapi::registry::MetaSchema {
let mut schema = poem_openapi::registry::MetaSchema::new("User");
schema.properties.insert(
"id".to_string(),
poem_openapi::registry::MetaSchemaRef::Inline(Box::new(u32::schema())),
);
schema.properties.insert(
"name".to_string(),
poem_openapi::registry::MetaSchemaRef::Inline(Box::new(String::schema())),
);
schema.properties.insert(
"email".to_string(),
poem_openapi::registry::MetaSchemaRef::Inline(Box::new(String::schema())),
);
schema.properties.insert(
"company".to_string(),
poem_openapi::registry::MetaSchemaRef::Inline(Box::new(String::schema())),
);
schema.properties.insert(
"role".to_string(),
poem_openapi::registry::MetaSchemaRef::Inline(Box::new(String::schema())),
);
schema.properties.insert(
"created_at".to_string(),
poem_openapi::registry::MetaSchemaRef::Inline(Box::new(
<DateTime<Utc> as ToSchema>::schema(),
)),
);
schema.properties.insert(
"updated_at".to_string(),
poem_openapi::registry::MetaSchemaRef::Inline(Box::new(
<DateTime<Utc> as ToSchema>::schema(),
)),
);
// Note: We exclude password for security reasons
schema
}
}
// Schema generation for Product
impl ToSchema for Product {
fn schema() -> poem_openapi::registry::MetaSchema {
let mut schema = poem_openapi::registry::MetaSchema::new("Product");
schema.properties.insert(
"id".to_string(),
poem_openapi::registry::MetaSchemaRef::Inline(Box::new(u32::schema())),
);
schema.properties.insert(
"company_id".to_string(),
poem_openapi::registry::MetaSchemaRef::Inline(Box::new(u32::schema())),
);
schema.properties.insert(
"name".to_string(),
poem_openapi::registry::MetaSchemaRef::Inline(Box::new(String::schema())),
);
schema.properties.insert(
"description".to_string(),
poem_openapi::registry::MetaSchemaRef::Inline(Box::new(String::schema())),
);
schema.properties.insert(
"price".to_string(),
poem_openapi::registry::MetaSchemaRef::Inline(Box::new(
<Currency as ToSchema>::schema(),
)),
);
schema.properties.insert(
"status".to_string(),
poem_openapi::registry::MetaSchemaRef::Inline(Box::new(String::schema())),
);
schema.properties.insert(
"created_at".to_string(),
poem_openapi::registry::MetaSchemaRef::Inline(Box::new(
<DateTime<Utc> as ToSchema>::schema(),
)),
);
schema.properties.insert(
"updated_at".to_string(),
poem_openapi::registry::MetaSchemaRef::Inline(Box::new(
<DateTime<Utc> as ToSchema>::schema(),
)),
);
schema
}
}
// Schema generation for SaleItem
impl ToSchema for SaleItem {
fn schema() -> poem_openapi::registry::MetaSchema {
let mut schema = poem_openapi::registry::MetaSchema::new("SaleItem");
schema.properties.insert(
"id".to_string(),
poem_openapi::registry::MetaSchemaRef::Inline(Box::new(u32::schema())),
);
schema.properties.insert(
"sale_id".to_string(),
poem_openapi::registry::MetaSchemaRef::Inline(Box::new(u32::schema())),
);
schema.properties.insert(
"product_id".to_string(),
poem_openapi::registry::MetaSchemaRef::Inline(Box::new(u32::schema())),
);
schema.properties.insert(
"name".to_string(),
poem_openapi::registry::MetaSchemaRef::Inline(Box::new(String::schema())),
);
schema.properties.insert(
"quantity".to_string(),
poem_openapi::registry::MetaSchemaRef::Inline(Box::new(i32::schema())),
);
schema.properties.insert(
"unit_price".to_string(),
poem_openapi::registry::MetaSchemaRef::Inline(Box::new(
<Currency as ToSchema>::schema(),
)),
);
schema.properties.insert(
"subtotal".to_string(),
poem_openapi::registry::MetaSchemaRef::Inline(Box::new(
<Currency as ToSchema>::schema(),
)),
);
schema.properties.insert(
"active_till".to_string(),
poem_openapi::registry::MetaSchemaRef::Inline(Box::new(
<DateTime<Utc> as ToSchema>::schema(),
)),
);
schema
}
}
// Schema generation for Sale
impl ToSchema for Sale {
fn schema() -> poem_openapi::registry::MetaSchema {
let mut schema = poem_openapi::registry::MetaSchema::new("Sale");
schema.properties.insert(
"id".to_string(),
poem_openapi::registry::MetaSchemaRef::Inline(Box::new(u32::schema())),
);
schema.properties.insert(
"company_id".to_string(),
poem_openapi::registry::MetaSchemaRef::Inline(Box::new(u32::schema())),
);
schema.properties.insert(
"buyer_name".to_string(),
poem_openapi::registry::MetaSchemaRef::Inline(Box::new(String::schema())),
);
schema.properties.insert(
"buyer_email".to_string(),
poem_openapi::registry::MetaSchemaRef::Inline(Box::new(String::schema())),
);
schema.properties.insert(
"total_amount".to_string(),
poem_openapi::registry::MetaSchemaRef::Inline(Box::new(
<Currency as ToSchema>::schema(),
)),
);
schema.properties.insert(
"status".to_string(),
poem_openapi::registry::MetaSchemaRef::Inline(Box::new(
<SaleStatus as ToSchema>::schema(),
)),
);
schema.properties.insert(
"sale_date".to_string(),
poem_openapi::registry::MetaSchemaRef::Inline(Box::new(
<DateTime<Utc> as ToSchema>::schema(),
)),
);
schema.properties.insert(
"created_at".to_string(),
poem_openapi::registry::MetaSchemaRef::Inline(Box::new(
<DateTime<Utc> as ToSchema>::schema(),
)),
);
schema.properties.insert(
"updated_at".to_string(),
poem_openapi::registry::MetaSchemaRef::Inline(Box::new(
<DateTime<Utc> as ToSchema>::schema(),
)),
);
schema.properties.insert(
"items".to_string(),
poem_openapi::registry::MetaSchemaRef::Inline(Box::new(
<Vec<SaleItem> as ToSchema>::schema(),
)),
);
schema
}
}

41
herodb/src/server/mod.rs Normal file
View File

@ -0,0 +1,41 @@
//! Server module for the HeroDB API
//!
//! This module provides a web API server using Poem and OpenAPI.
pub mod api;
pub mod models;
use poem::{
listener::TcpListener,
Route,
Server
};
use poem_openapi::OpenApiService;
use std::path::PathBuf;
/// Start the API server
pub async fn start_server(db_path: PathBuf, host: &str, port: u16) -> Result<(), std::io::Error> {
// Create the API service
let api_service = OpenApiService::new(
api::Api::new(db_path),
"HeroDB API",
env!("CARGO_PKG_VERSION"),
)
.server(format!("http://{}:{}/api", host, port));
// Create Swagger UI
let swagger_ui = api_service.swagger_ui();
// Create the main route
let app = Route::new()
.nest("/api", api_service)
.nest("/swagger", swagger_ui);
// Start the server
println!("Starting server on {}:{}", host, port);
println!("API Documentation: http://{}:{}/swagger", host, port);
Server::new(TcpListener::bind(format!("{}:{}", host, port)))
.run(app)
.await
}

145
herodb/src/server/models.rs Normal file
View File

@ -0,0 +1,145 @@
//! API models for the HeroDB server
use crate::zaz::models::sale::SaleStatus;
use poem_openapi::{Object, ApiResponse};
use serde::{Deserialize, Serialize};
/// API error response
#[derive(Debug, Object)]
pub struct ApiError {
/// Error code
pub code: u16,
/// Error message
pub message: String,
}
impl ApiError {
pub fn not_found(id: impl ToString) -> Self {
Self {
code: 404,
message: format!("Resource with ID {} not found", id.to_string()),
}
}
pub fn internal_error(msg: impl ToString) -> Self {
Self {
code: 500,
message: msg.to_string(),
}
}
}
/// API success response
#[derive(Debug, Object)]
pub struct SuccessResponse {
/// Success flag
pub success: bool,
/// Success message
pub message: String,
}
/// User create request
#[derive(Debug, Object)]
pub struct UserCreate {
/// User name
pub name: String,
/// User email
pub email: String,
/// User password
pub password: String,
/// User company
pub company: String,
/// User role
pub role: String,
}
/// User update request
#[derive(Debug, Object)]
pub struct UserUpdate {
/// User name
#[oai(skip_serializing_if_is_none)]
pub name: Option<String>,
/// User email
#[oai(skip_serializing_if_is_none)]
pub email: Option<String>,
/// User password
#[oai(skip_serializing_if_is_none)]
pub password: Option<String>,
/// User company
#[oai(skip_serializing_if_is_none)]
pub company: Option<String>,
/// User role
#[oai(skip_serializing_if_is_none)]
pub role: Option<String>,
}
/// Sale item create request
#[derive(Debug, Serialize, Deserialize, Object)]
pub struct SaleItemCreate {
/// Product ID
pub product_id: u32,
/// Item name
pub name: String,
/// Quantity
pub quantity: i32,
/// Unit price
pub unit_price: f64,
}
/// Sale create request
#[derive(Debug, Serialize, Deserialize, Object)]
pub struct SaleCreate {
/// Company ID
pub company_id: u32,
/// Buyer name
pub buyer_name: String,
/// Buyer email
pub buyer_email: String,
/// Currency code
pub currency_code: String,
/// Items
#[oai(skip_serializing_if_is_none)]
pub items: Option<Vec<SaleItemCreate>>,
}
/// Sale status update request
#[derive(Debug, Serialize, Deserialize, Object)]
pub struct SaleStatusUpdate {
/// New status
pub status: String,
}
impl SaleStatusUpdate {
pub fn parse_status(&self) -> Result<SaleStatus, ApiError> {
match self.status.to_lowercase().as_str() {
"pending" => Ok(SaleStatus::Pending),
"completed" => Ok(SaleStatus::Completed),
"cancelled" => Ok(SaleStatus::Cancelled),
_ => Err(ApiError {
code: 400,
message: format!("Invalid status: {}", self.status),
})
}
}
}
// Define API responses
#[derive(Debug, ApiResponse)]
pub enum UserResponse {
#[oai(status = 200)]
Ok(poem_openapi::payload::Json<String>),
#[oai(status = 404)]
NotFound(poem_openapi::payload::Json<ApiError>),
#[oai(status = 500)]
InternalError(poem_openapi::payload::Json<ApiError>),
}
#[derive(Debug, ApiResponse)]
pub enum SuccessOrError {
#[oai(status = 200)]
Ok(poem_openapi::payload::Json<SuccessResponse>),
#[oai(status = 404)]
NotFound(poem_openapi::payload::Json<ApiError>),
#[oai(status = 500)]
InternalError(poem_openapi::payload::Json<ApiError>),
}

View File

@ -4,6 +4,8 @@
use crate::zaz::models::*;
use crate::zaz::models::shareholder::ShareholderType;
use crate::zaz::factory::create_zaz_db;
use crate::zaz::rhai::{initialize_rhai_engine, run_script_file, run_example_script};
use rhai::{Engine, EvalAltResult};
use std::path::PathBuf;
use std::fs;
use chrono::Utc;
@ -159,3 +161,39 @@ pub fn run_db_examples() -> Result<(), String> {
println!("\nDB examples completed successfully!");
Ok(())
}
/// Example demonstrating how to use Rhai scripts with models
pub fn run_rhai_examples() -> Result<(), String> {
println!("Running Rhai examples...");
// Run the test script that demonstrates using the model wrappers
// without database operations
let script_path = "src/zaz/rhai/test.rhai";
run_script_file(script_path)?;
// Run an example that demonstrates creating model objects
let temp_dir = create_temp_dir().map_err(|e| format!("Failed to create temp dir: {}", e))?;
println!("Using DB path for Rhai example: {:?}", temp_dir);
// Run the example script that uses the DB
run_example_script(temp_dir.to_str().unwrap())?;
println!("Rhai examples completed successfully!");
Ok(())
}
/// Run a complete demonstration of Rhai integration with our models
pub fn run_rhai_demo() -> Result<(), String> {
println!("=====================================================");
println!("Starting Rhai demos for Zaz DB");
println!("=====================================================");
// Run the Rhai examples
run_rhai_examples()?;
println!("=====================================================");
println!("Rhai demo completed");
println!("=====================================================");
Ok(())
}

View File

@ -6,6 +6,9 @@ pub mod models;
mod factory;
pub use factory::create_zaz_db;
// Expose the rhai module with wrappers
pub mod rhai;
pub use rhai::initialize_rhai_engine;
// Declare the examples module for the new DB implementation
#[path = "examples.rs"] // Tell compiler where to find the examples module

View File

@ -1,6 +1,6 @@
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use crate::core::{SledModel, Storable}; // Import Sled traits from new location
use crate::db::{SledModel, Storable}; // Import Sled traits from new location
// use std::collections::HashMap; // Removed unused import
// use super::db::Model; // Removed old Model trait import

View File

@ -0,0 +1,13 @@
// Export all rhai wrapper modules
mod engine;
mod user;
mod company;
mod shareholder;
// Re-export key components
pub use engine::{create_rhai_engine, run_script_file, run_example_script};
// Public API
pub fn initialize_rhai_engine() -> rhai::Engine {
create_rhai_engine()
}

View File

@ -0,0 +1,8 @@
//! Simple binary to demonstrate Rhai wrapper functionality
use crate::zaz::cmd::examples::run_rhai_demo;
fn main() -> Result<(), String> {
println!("Running Rhai wrapper examples...");
run_rhai_demo()
}