...
This commit is contained in:
parent
4a65f8255a
commit
b37b9da8b5
1417
herodb/Cargo.lock
generated
1417
herodb/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -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"
|
||||
|
14
herodb/aiprompts/rhaiwrapping.md
Normal file
14
herodb/aiprompts/rhaiwrapping.md
Normal 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
|
38
herodb/examples/rhai_demo.rs
Normal file
38
herodb/examples/rhai_demo.rs
Normal 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
1
herodb/rhaibook
Symbolic link
@ -0,0 +1 @@
|
||||
../../rhaj/rhai_engine/rhaibook
|
1
herodb/rhaiexamples
Symbolic link
1
herodb/rhaiexamples
Symbolic link
@ -0,0 +1 @@
|
||||
../../rhaj/rhai_engine/rhaiexamples
|
5
herodb/src/bin/dummy.rs
Normal file
5
herodb/src/bin/dummy.rs
Normal file
@ -0,0 +1,5 @@
|
||||
//! Simple binary to test if binaries are working
|
||||
|
||||
fn main() {
|
||||
println!("Hello from dummy binary!");
|
||||
}
|
7
herodb/src/bin/rhai_examples.rs
Normal file
7
herodb/src/bin/rhai_examples.rs
Normal 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()
|
||||
}
|
35
herodb/src/circle/models/README.md
Normal file
35
herodb/src/circle/models/README.md
Normal 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.
|
||||
|
70
herodb/src/circle/models/circle.rs
Normal file
70
herodb/src/circle/models/circle.rs
Normal 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"
|
||||
}
|
||||
}
|
@ -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};
|
||||
|
@ -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};
|
||||
|
72
herodb/src/circle/models/name.rs
Normal file
72
herodb/src/circle/models/name.rs
Normal 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"
|
||||
}
|
||||
}
|
@ -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};
|
@ -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;
|
||||
|
@ -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)
|
||||
}
|
63
herodb/src/mcc/models/README.md
Normal file
63
herodb/src/mcc/models/README.md
Normal 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
|
35
herodb/src/mcc/models/calendar.rs
Normal file
35
herodb/src/mcc/models/calendar.rs
Normal 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"
|
||||
}
|
||||
}
|
73
herodb/src/mcc/models/contacts.rs
Normal file
73
herodb/src/mcc/models/contacts.rs
Normal 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"
|
||||
}
|
||||
}
|
94
herodb/src/mcc/models/event.rs
Normal file
94
herodb/src/mcc/models/event.rs
Normal 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"
|
||||
}
|
||||
}
|
13
herodb/src/mcc/models/lib.rs
Normal file
13
herodb/src/mcc/models/lib.rs
Normal 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};
|
85
herodb/src/mcc/models/mail.rs
Normal file
85
herodb/src/mcc/models/mail.rs
Normal 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"
|
||||
}
|
||||
}
|
13
herodb/src/mcc/models/mod.rs
Normal file
13
herodb/src/mcc/models/mod.rs
Normal 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};
|
377
herodb/src/rhaiengine/engine.rs
Normal file
377
herodb/src/rhaiengine/engine.rs
Normal 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
368
herodb/src/server/api.rs
Normal 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)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
289
herodb/src/server/extensions.rs
Normal file
289
herodb/src/server/extensions.rs
Normal 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
41
herodb/src/server/mod.rs
Normal 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
145
herodb/src/server/models.rs
Normal 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>),
|
||||
}
|
@ -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(())
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
13
herodb/src/zaz/rhai/mod.rs
Normal file
13
herodb/src/zaz/rhai/mod.rs
Normal 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()
|
||||
}
|
8
herodb/src/zaz/rhai_examples.rs
Normal file
8
herodb/src/zaz/rhai_examples.rs
Normal 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()
|
||||
}
|
Loading…
Reference in New Issue
Block a user