move repos into monorepo

This commit is contained in:
Timur Gordon
2025-11-13 20:44:00 +01:00
commit 4b23e5eb7f
204 changed files with 33737 additions and 0 deletions

View File

@@ -0,0 +1,23 @@
[package]
name = "hero-job-client"
version.workspace = true
edition.workspace = true
description = "Job client for Hero - Redis-based job management"
license = "MIT OR Apache-2.0"
[lib]
name = "hero_job_client"
path = "lib.rs"
[dependencies]
# Core dependencies
redis.workspace = true
tokio.workspace = true
chrono.workspace = true
thiserror.workspace = true
async-trait.workspace = true
serde.workspace = true
serde_json.workspace = true
# Hero dependencies
hero-job = { path = "../../models/job" }

473
lib/clients/job/lib.rs Normal file
View File

@@ -0,0 +1,473 @@
//! Job client implementation for managing jobs in Redis
use chrono::Utc;
use redis::AsyncCommands;
use hero_job::{Job, JobStatus, JobError};
use thiserror::Error;
/// Client-specific error types
#[derive(Error, Debug)]
pub enum ClientError {
#[error("Redis error: {0}")]
Redis(#[from] redis::RedisError),
#[error("Job error: {0}")]
Job(#[from] JobError),
#[error("Invalid status: {0}")]
InvalidStatus(String),
#[error("Timeout waiting for job completion")]
Timeout,
}
/// Client for managing jobs in Redis
#[derive(Debug, Clone)]
pub struct Client {
redis_client: redis::Client,
namespace: String,
}
pub struct ClientBuilder {
/// Redis URL for connection
redis_url: String,
/// Namespace for queue keys
namespace: String,
}
impl ClientBuilder {
/// Create a new client builder
pub fn new() -> Self {
Self {
redis_url: "redis://localhost:6379".to_string(),
namespace: "".to_string(),
}
}
/// Set the Redis URL
pub fn redis_url<S: Into<String>>(mut self, url: S) -> Self {
self.redis_url = url.into();
self
}
/// Set the namespace for queue keys
pub fn namespace<S: Into<String>>(mut self, namespace: S) -> Self {
self.namespace = namespace.into();
self
}
/// Build the client
pub async fn build(self) -> Result<Client, ClientError> {
// Create Redis client
let redis_client = redis::Client::open(self.redis_url.as_str())
.map_err(|e| ClientError::Redis(e))?;
Ok(Client {
redis_client,
namespace: self.namespace,
})
}
}
impl Default for Client {
fn default() -> Self {
// Note: Default implementation creates an empty client
// Use Client::builder() for proper initialization
Self {
redis_client: redis::Client::open("redis://localhost:6379").unwrap(),
namespace: "".to_string(),
}
}
}
impl Client {
/// Create a new client builder
pub fn builder() -> ClientBuilder {
ClientBuilder::new()
}
/// List all job IDs from Redis
pub async fn list_jobs(&self) -> Result<Vec<String>, ClientError> {
let mut conn = self.redis_client
.get_multiplexed_async_connection()
.await
.map_err(|e| ClientError::Redis(e))?;
let keys: Vec<String> = conn.keys(format!("{}:*", &self.jobs_key())).await
.map_err(|e| ClientError::Redis(e))?;
let job_ids: Vec<String> = keys
.into_iter()
.filter_map(|key| {
if key.starts_with(&format!("{}:", self.jobs_key())) {
key.strip_prefix(&format!("{}:", self.jobs_key()))
.map(|s| s.to_string())
} else {
None
}
})
.collect();
Ok(job_ids)
}
fn jobs_key(&self) -> String {
if self.namespace.is_empty() {
format!("job")
} else {
format!("{}:job", self.namespace)
}
}
pub fn job_key(&self, job_id: &str) -> String {
if self.namespace.is_empty() {
format!("job:{}", job_id)
} else {
format!("{}:job:{}", self.namespace, job_id)
}
}
pub fn job_reply_key(&self, job_id: &str) -> String {
if self.namespace.is_empty() {
format!("reply:{}", job_id)
} else {
format!("{}:reply:{}", self.namespace, job_id)
}
}
pub fn runner_key(&self, runner_name: &str) -> String {
if self.namespace.is_empty() {
format!("runner:{}", runner_name)
} else {
format!("{}:runner:{}", self.namespace, runner_name)
}
}
/// Set job error in Redis
pub async fn set_error(&self,
job_id: &str,
error: &str,
) -> Result<(), ClientError> {
let job_key = self.job_key(job_id);
let now = Utc::now();
let mut conn = self.redis_client
.get_multiplexed_async_connection()
.await
.map_err(|e| ClientError::Redis(e))?;
let _: () = conn.hset_multiple(&job_key, &[
("error", error),
("status", JobStatus::Error.as_str()),
("updated_at", &now.to_rfc3339()),
]).await
.map_err(|e| ClientError::Redis(e))?;
Ok(())
}
/// Set job status in Redis
pub async fn set_job_status(&self,
job_id: &str,
status: JobStatus,
) -> Result<(), ClientError> {
let job_key = self.job_key(job_id);
let now = Utc::now();
let mut conn = self.redis_client
.get_multiplexed_async_connection()
.await
.map_err(|e| ClientError::Redis(e))?;
let _: () = conn.hset_multiple(&job_key, &[
("status", status.as_str()),
("updated_at", &now.to_rfc3339()),
]).await
.map_err(|e| ClientError::Redis(e))?;
Ok(())
}
/// Get job status from Redis
pub async fn get_status(
&self,
job_id: &str,
) -> Result<JobStatus, ClientError> {
let mut conn = self.redis_client
.get_multiplexed_async_connection()
.await
.map_err(|e| ClientError::Redis(e))?;
let status_str: Option<String> = conn.hget(&self.job_key(job_id), "status").await
.map_err(|e| ClientError::Redis(e))?;
match status_str {
Some(s) => JobStatus::from_str(&s).ok_or_else(|| ClientError::InvalidStatus(s)),
None => Err(ClientError::Job(JobError::NotFound(job_id.to_string()))),
}
}
/// Delete job from Redis
pub async fn delete_from_redis(
&self,
job_id: &str,
) -> Result<(), ClientError> {
let mut conn = self.redis_client
.get_multiplexed_async_connection()
.await
.map_err(|e| ClientError::Redis(e))?;
let job_key = self.job_key(job_id);
let _: () = conn.del(&job_key).await
.map_err(|e| ClientError::Redis(e))?;
Ok(())
}
/// Store this job in Redis with the specified status
pub async fn store_job_in_redis_with_status(&self, job: &Job, status: JobStatus) -> Result<(), ClientError> {
let mut conn = self.redis_client
.get_multiplexed_async_connection()
.await
.map_err(|e| ClientError::Redis(e))?;
let job_key = self.job_key(&job.id);
// Serialize the job data
let job_data = serde_json::to_string(job)
.map_err(|e| JobError::Serialization(e))?;
// Store job data in Redis hash
let _: () = conn.hset_multiple(&job_key, &[
("data", job_data),
("status", status.as_str().to_string()),
("created_at", job.created_at.to_rfc3339()),
("updated_at", job.updated_at.to_rfc3339()),
]).await
.map_err(|e| ClientError::Redis(e))?;
// Set TTL for the job (24 hours)
let _: () = conn.expire(&job_key, 86400).await
.map_err(|e| ClientError::Redis(e))?;
Ok(())
}
/// Store this job in Redis (defaults to Dispatched status for backwards compatibility)
pub async fn store_job_in_redis(&self, job: &Job) -> Result<(), ClientError> {
self.store_job_in_redis_with_status(job, JobStatus::Dispatched).await
}
/// Load a job from Redis by ID
pub async fn load_job_from_redis(
&self,
job_id: &str,
) -> Result<Job, ClientError> {
let job_key = self.job_key(job_id);
let mut conn = self.redis_client
.get_multiplexed_async_connection()
.await
.map_err(|e| ClientError::Redis(e))?;
// Get job data from Redis
let job_data: Option<String> = conn.hget(&job_key, "data").await
.map_err(|e| ClientError::Redis(e))?;
match job_data {
Some(data) => {
let job: Job = serde_json::from_str(&data)
.map_err(|e| JobError::Serialization(e))?;
Ok(job)
}
None => Err(ClientError::Job(JobError::NotFound(job_id.to_string()))),
}
}
/// Delete a job by ID
pub async fn delete_job(&mut self, job_id: &str) -> Result<(), ClientError> {
let mut conn = self.redis_client.get_multiplexed_async_connection().await
.map_err(|e| ClientError::Redis(e))?;
let job_key = self.job_key(job_id);
let deleted_count: i32 = conn.del(&job_key).await
.map_err(|e| ClientError::Redis(e))?;
if deleted_count == 0 {
return Err(ClientError::Job(JobError::NotFound(job_id.to_string())));
}
Ok(())
}
/// Set job result in Redis
pub async fn set_result(
&self,
job_id: &str,
result: &str,
) -> Result<(), ClientError> {
let job_key = self.job_key(&job_id);
let now = Utc::now();
let mut conn = self.redis_client
.get_multiplexed_async_connection()
.await
.map_err(|e| ClientError::Redis(e))?;
let _: () = conn.hset_multiple(&job_key, &[
("result", result),
("status", JobStatus::Finished.as_str()),
("updated_at", &now.to_rfc3339()),
]).await
.map_err(|e| ClientError::Redis(e))?;
Ok(())
}
/// Get job result from Redis
pub async fn get_result(
&self,
job_id: &str,
) -> Result<Option<String>, ClientError> {
let job_key = self.job_key(job_id);
let mut conn = self.redis_client
.get_multiplexed_async_connection()
.await
.map_err(|e| ClientError::Redis(e))?;
let result: Option<String> = conn.hget(&job_key, "result").await
.map_err(|e| ClientError::Redis(e))?;
Ok(result)
}
/// Get job result from Redis
pub async fn get_error(
&self,
job_id: &str,
) -> Result<Option<String>, ClientError> {
let job_key = self.job_key(job_id);
let mut conn = self.redis_client
.get_multiplexed_async_connection()
.await
.map_err(|e| ClientError::Redis(e))?;
let result: Option<String> = conn.hget(&job_key, "error").await
.map_err(|e| ClientError::Redis(e))?;
Ok(result)
}
/// Get a job ID from the work queue (blocking pop)
pub async fn get_job_id(&self, queue_key: &str) -> Result<Option<String>, ClientError> {
let mut conn = self.redis_client
.get_multiplexed_async_connection()
.await
.map_err(|e| ClientError::Redis(e))?;
// Use BRPOP with a short timeout to avoid blocking indefinitely
let result: Option<(String, String)> = conn.brpop(queue_key, 1.0).await
.map_err(|e| ClientError::Redis(e))?;
Ok(result.map(|(_, job_id)| job_id))
}
/// Get a job by ID (alias for load_job_from_redis)
pub async fn get_job(&self, job_id: &str) -> Result<Job, ClientError> {
self.load_job_from_redis(job_id).await
}
/// Run a job by dispatching it to a runner's queue (fire-and-forget)
pub async fn job_run(&self, job_id: &str, runner_name: &str) -> Result<(), ClientError> {
let mut conn = self.redis_client
.get_multiplexed_async_connection()
.await
.map_err(|e| ClientError::Redis(e))?;
let queue_key = self.runner_key(runner_name);
// Push job ID to the runner's queue (LPUSH for FIFO with BRPOP)
let _: () = conn.lpush(&queue_key, job_id).await
.map_err(|e| ClientError::Redis(e))?;
Ok(())
}
/// Run a job and wait for completion
///
/// This is a convenience method that:
/// 1. Stores the job in Redis
/// 2. Dispatches it to the runner's queue
/// 3. Waits for the job to complete (polls status)
/// 4. Returns the result or error
///
/// # Arguments
/// * `job` - The job to run
/// * `runner_name` - The name of the runner to dispatch to
/// * `timeout_secs` - Maximum time to wait for job completion (in seconds)
///
/// # Returns
/// * `Ok(String)` - The job result if successful
/// * `Err(JobError)` - If the job fails, times out, or encounters an error
pub async fn job_run_wait(
&self,
job: &Job,
runner_name: &str,
timeout_secs: u64,
) -> Result<String, ClientError> {
use tokio::time::{Duration, timeout};
// Store the job in Redis
self.store_job_in_redis(job).await?;
// Dispatch to runner queue
self.job_run(&job.id, runner_name).await?;
// Wait for job to complete with timeout
let result = timeout(
Duration::from_secs(timeout_secs),
self.wait_for_job_completion(&job.id)
).await;
match result {
Ok(Ok(job_result)) => Ok(job_result),
Ok(Err(e)) => Err(e),
Err(_) => Err(ClientError::Timeout),
}
}
/// Wait for a job to complete by polling its status
///
/// This polls the job status every 500ms until it reaches a terminal state
/// (Finished or Error), then returns the result or error.
async fn wait_for_job_completion(&self, job_id: &str) -> Result<String, ClientError> {
use tokio::time::{sleep, Duration};
loop {
// Check job status
let status = self.get_status(job_id).await?;
match status {
JobStatus::Finished => {
// Job completed successfully, get the result
let result = self.get_result(job_id).await?;
return result.ok_or_else(|| {
ClientError::Job(JobError::InvalidData(format!("Job {} finished but has no result", job_id)))
});
}
JobStatus::Error => {
// Job failed, get the error message
let mut conn = self.redis_client
.get_multiplexed_async_connection()
.await
.map_err(|e| ClientError::Redis(e))?;
let error_msg: Option<String> = conn
.hget(&self.job_key(job_id), "error")
.await
.map_err(|e| ClientError::Redis(e))?;
return Err(ClientError::Job(JobError::InvalidData(
error_msg.unwrap_or_else(|| format!("Job {} failed with unknown error", job_id))
)));
}
JobStatus::Stopping => {
return Err(ClientError::Job(JobError::InvalidData(format!("Job {} was stopped", job_id))));
}
// Job is still running (Dispatched, WaitingForPrerequisites, Started)
_ => {
// Wait before polling again
sleep(Duration::from_millis(500)).await;
}
}
}
}
}

View File

@@ -0,0 +1,31 @@
[package]
name = "osiris-client"
version.workspace = true
edition.workspace = true
description = "Osiris client library"
license = "MIT OR Apache-2.0"
[dependencies]
# Core dependencies
serde.workspace = true
serde_json.workspace = true
anyhow.workspace = true
thiserror.workspace = true
chrono.workspace = true
# HTTP client
reqwest = { version = "0.12", default-features = false, features = ["json"] }
# Hero dependencies
hero-supervisor-openrpc-client = { path = "../supervisor" }
hero-job = { path = "../../models/job" }
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
uuid.workspace = true
[target.'cfg(target_arch = "wasm32")'.dependencies]
uuid = { workspace = true, features = ["js"] }
getrandom.workspace = true
[dev-dependencies]
tokio.workspace = true

View File

@@ -0,0 +1,170 @@
//! Complete Osiris Client Example
//!
//! This example demonstrates the full CQRS pattern with Osiris:
//! - Commands (writes) via Rhai scripts through Supervisor
//! - Queries (reads) via REST API from Osiris server
//!
//! Prerequisites:
//! - Redis running on localhost:6379
//! - Supervisor running on localhost:3030
//! - Osiris server running on localhost:8080
//! - Osiris runner connected to Redis
use osiris_client::OsirisClient;
use hero_supervisor_openrpc_client::SupervisorClient;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("🚀 Osiris Client - Complete Example\n");
println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
// Configuration
let admin_secret = "807470fd1e1ccc3fb997a1d4177cceb31a68cb355a4412c8fd6e66e517e902be";
let supervisor_url = "http://localhost:3030";
let osiris_url = "http://localhost:8080";
let runner_name = "osiris-queue";
// ========== Part 1: Setup Runner ==========
println!("📋 Part 1: Runner Setup\n");
let supervisor = SupervisorClient::builder()
.url(supervisor_url)
.secret(admin_secret)
.build()?;
// Register the runner
println!("1. Registering runner '{}'...", runner_name);
match supervisor.register_runner(runner_name).await {
Ok(result) => println!(" ✅ Runner registered: {}\n", result),
Err(e) => println!(" ⚠️ Registration failed (may already exist): {:?}\n", e),
}
// List all runners
println!("2. Listing all runners...");
match supervisor.list_runners().await {
Ok(runners) => {
println!(" ✅ Found {} runner(s):", runners.len());
for runner in runners {
println!(" - {}", runner);
}
println!();
}
Err(e) => println!(" ❌ Failed: {:?}\n", e),
}
// ========== Part 2: Initialize Osiris Client ==========
println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
println!("📋 Part 2: Osiris Client (CQRS Pattern)\n");
let client = OsirisClient::builder()
.osiris_url(osiris_url)
.supervisor_url(supervisor_url)
.supervisor_secret(admin_secret)
.runner_name(runner_name)
.build()?;
println!("✅ Osiris client initialized");
println!(" - Osiris URL: {}", osiris_url);
println!(" - Supervisor URL: {}", supervisor_url);
println!(" - Runner: {}\n", runner_name);
// ========== Part 3: Execute Simple Script ==========
println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
println!("📋 Part 3: Execute Rhai Script\n");
let script = r#"
print("Hello from Osiris!");
let result = 40 + 2;
print("The answer is: " + result);
result
"#;
println!("Executing script...");
match client.execute_script(script).await {
Ok(response) => {
println!(" ✅ Script executed successfully!");
println!(" Job ID: {}", response.job_id);
println!(" Status: {}\n", response.status);
}
Err(e) => {
println!(" ❌ Script execution failed: {}", e);
println!(" Make sure the runner is connected to Redis!\n");
}
}
// ========== Part 4: CQRS Operations (if runner is connected) ==========
println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
println!("📋 Part 4: CQRS Operations (Commands + Queries)\n");
// Create an API Key (Command via Rhai)
println!("1. Creating API Key (Command via Rhai)...");
let api_key = format!("test-key-{}", chrono::Utc::now().timestamp());
match client.create_api_key(
api_key.clone(),
"Test Key".to_string(),
"admin".to_string()
).await {
Ok(response) => {
println!(" ✅ API Key created!");
println!(" Job ID: {}", response.job_id);
println!(" Status: {}\n", response.status);
}
Err(e) => println!(" ⚠️ Failed: {}\n", e),
}
// Query the API Key (Query via REST)
println!("2. Querying API Key (Query via REST)...");
match client.get_api_key(&api_key).await {
Ok(key) => {
println!(" ✅ API Key retrieved!");
println!(" Key: {:?}\n", key);
}
Err(e) => println!(" ⚠️ Not found yet: {}\n", e),
}
// List all API Keys (Query via REST)
println!("3. Listing all API Keys (Query via REST)...");
match client.list_api_keys().await {
Ok(keys) => {
println!(" ✅ Found {} API key(s)", keys.len());
for key in keys.iter().take(3) {
println!(" - {:?}", key);
}
if keys.len() > 3 {
println!(" ... and {} more", keys.len() - 3);
}
println!();
}
Err(e) => println!(" ⚠️ Failed: {}\n", e),
}
// ========== Part 5: Cleanup ==========
println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
println!("📋 Part 5: Cleanup\n");
println!("Deleting test API Key (Command via Rhai)...");
match client.delete_api_key(api_key.clone()).await {
Ok(response) => {
println!(" ✅ API Key deleted!");
println!(" Job ID: {}", response.job_id);
println!(" Status: {}\n", response.status);
}
Err(e) => println!(" ⚠️ Failed: {}\n", e),
}
// ========== Summary ==========
println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
println!("✅ Example Complete!\n");
println!("Summary:");
println!(" ✅ Runner registration working");
println!(" ✅ Osiris client initialized");
println!(" ✅ CQRS pattern demonstrated");
println!(" - Commands via Rhai scripts");
println!(" - Queries via REST API");
println!("\nNext steps:");
println!(" - Explore other CQRS methods (runners, jobs, etc.)");
println!(" - Use template-based script generation");
println!(" - Build your own Osiris-backed applications!");
Ok(())
}

View File

@@ -0,0 +1,100 @@
//! Communication methods (queries and commands)
use serde::{Deserialize, Serialize};
use crate::{OsirisClient, OsirisClientError};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Verification {
pub id: String,
pub email: String,
pub code: String,
pub transport: String,
pub status: VerificationStatus,
pub created_at: i64,
pub expires_at: i64,
pub verified_at: Option<i64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum VerificationStatus {
Pending,
Verified,
Expired,
Failed,
}
// ========== Request/Response Models ==========
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SendVerificationRequest {
pub email: String,
pub verification_url: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SendVerificationResponse {
pub verification_id: String,
pub email: String,
pub expires_at: i64,
}
// ========== Client Methods ==========
impl OsirisClient {
// ========== Query Methods ==========
/// Get verification by ID (query)
pub async fn get_verification(&self, verification_id: &str) -> Result<Verification, OsirisClientError> {
self.get("verification", verification_id).await
}
/// Get verification by email (query)
pub async fn get_verification_by_email(&self, email: &str) -> Result<Vec<Verification>, OsirisClientError> {
self.query("verification", &format!("email={}", email)).await
}
/// Get verification status - alias for get_verification (query)
pub async fn get_verification_status(&self, verification_id: &str) -> Result<Verification, OsirisClientError> {
self.get_verification(verification_id).await
}
// ========== Command Methods ==========
/// Send verification email (command)
pub async fn send_verification_email(
&self,
request: SendVerificationRequest,
) -> Result<SendVerificationResponse, OsirisClientError> {
let email = &request.email;
let verification_url = request.verification_url.as_deref().unwrap_or("");
// Generate verification code
let verification_id = format!("ver_{}", uuid::Uuid::new_v4());
let code = format!("{:06}", (uuid::Uuid::new_v4().as_u128() % 1_000_000));
let script = format!(r#"
// Send email verification
let email = "{}";
let code = "{}";
let verification_url = "{}";
let verification_id = "{}";
// TODO: Implement actual email sending logic
print("Sending verification email to: " + email);
print("Verification code: " + code);
print("Verification URL: " + verification_url);
// Return verification details
verification_id
"#, email, code, verification_url, verification_id);
let _response = self.execute_script(&script).await?;
Ok(SendVerificationResponse {
verification_id,
email: request.email,
expires_at: chrono::Utc::now().timestamp() + 3600, // 1 hour
})
}
}

View File

@@ -0,0 +1,102 @@
//! KYC methods (queries and commands)
use serde::{Deserialize, Serialize};
use crate::{OsirisClient, OsirisClientError};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KycSession {
pub id: String,
pub resident_id: String,
pub status: KycSessionStatus,
pub kyc_url: Option<String>,
pub created_at: i64,
pub updated_at: i64,
pub expires_at: i64,
pub verified_at: Option<i64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum KycSessionStatus {
Pending,
InProgress,
Completed,
Failed,
Expired,
}
// ========== Request/Response Models ==========
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KycVerificationRequest {
pub resident_id: String,
pub callback_url: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KycVerificationResponse {
pub session_id: String,
pub kyc_url: String,
pub expires_at: i64,
}
// ========== Client Methods ==========
impl OsirisClient {
// ========== Query Methods ==========
/// Get KYC session by ID
pub async fn get_kyc_session(&self, session_id: &str) -> Result<KycSession, OsirisClientError> {
self.get("kyc_session", session_id).await
}
/// List all KYC sessions for a resident
pub async fn list_kyc_sessions_by_resident(&self, resident_id: &str) -> Result<Vec<KycSession>, OsirisClientError> {
self.query("kyc_session", &format!("resident_id={}", resident_id)).await
}
// ========== Command Methods ==========
/// Start KYC verification (command)
pub async fn start_kyc_verification(
&self,
request: KycVerificationRequest,
) -> Result<KycVerificationResponse, OsirisClientError> {
let resident_id = &request.resident_id;
let callback_url = request.callback_url.as_deref().unwrap_or("");
// Generate session ID
let session_id = format!("kyc_{}", uuid::Uuid::new_v4());
let script = format!(r#"
// Start KYC verification
let resident_id = "{}";
let callback_url = "{}";
let session_id = "{}";
// TODO: Implement actual KYC provider integration
print("Starting KYC verification for resident: " + resident_id);
print("Session ID: " + session_id);
print("Callback URL: " + callback_url);
// Return session details
session_id
"#, resident_id, callback_url, session_id);
let _response = self.execute_script(&script).await?;
Ok(KycVerificationResponse {
session_id,
kyc_url: "https://kyc.example.com/verify".to_string(),
expires_at: chrono::Utc::now().timestamp() + 86400,
})
}
/// Check KYC status (query)
pub async fn check_kyc_status(
&self,
session_id: String,
) -> Result<KycSession, OsirisClientError> {
self.get_kyc_session(&session_id).await
}
}

View File

@@ -0,0 +1,439 @@
//! Osiris Client - Unified CQRS Client
//!
//! This client provides both:
//! - Commands (writes) via Rhai scripts to Hero Supervisor
//! - Queries (reads) via REST API to Osiris server
//!
//! Follows CQRS pattern with a single unified interface.
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use thiserror::Error;
pub mod kyc;
pub mod payment;
pub mod communication;
pub use kyc::*;
pub use payment::*;
pub use communication::*;
#[derive(Debug, Error)]
pub enum OsirisClientError {
#[error("HTTP request failed: {0}")]
RequestFailed(#[from] reqwest::Error),
#[error("Resource not found: {0}")]
NotFound(String),
#[error("Deserialization failed: {0}")]
DeserializationFailed(String),
#[error("Configuration error: {0}")]
ConfigError(String),
#[error("Command execution failed: {0}")]
CommandFailed(String),
}
/// Osiris client with CQRS support
#[derive(Clone)]
pub struct OsirisClient {
// Query side (Osiris REST API)
osiris_url: String,
// Command side (Supervisor + Rhai)
supervisor_client: Option<hero_supervisor_openrpc_client::SupervisorClient>,
runner_name: String,
timeout: u64,
// HTTP client
client: reqwest::Client,
}
/// Builder for OsirisClient
#[derive(Clone, Debug, Default)]
pub struct OsirisClientBuilder {
osiris_url: Option<String>,
supervisor_url: Option<String>,
runner_name: Option<String>,
supervisor_secret: Option<String>,
timeout: u64,
}
impl OsirisClientBuilder {
/// Create a new builder
pub fn new() -> Self {
Self {
osiris_url: None,
supervisor_url: None,
runner_name: None,
supervisor_secret: None,
timeout: 30,
}
}
/// Set the Osiris server URL (for queries)
pub fn osiris_url(mut self, url: impl Into<String>) -> Self {
self.osiris_url = Some(url.into());
self
}
/// Set the Supervisor URL (for commands)
pub fn supervisor_url(mut self, url: impl Into<String>) -> Self {
self.supervisor_url = Some(url.into());
self
}
/// Set the runner name (default: "osiris")
pub fn runner_name(mut self, name: impl Into<String>) -> Self {
self.runner_name = Some(name.into());
self
}
/// Set the supervisor authentication secret
pub fn supervisor_secret(mut self, secret: impl Into<String>) -> Self {
self.supervisor_secret = Some(secret.into());
self
}
/// Set the timeout in seconds (default: 30)
pub fn timeout(mut self, timeout: u64) -> Self {
self.timeout = timeout;
self
}
/// Build the OsirisClient
pub fn build(self) -> Result<OsirisClient, OsirisClientError> {
let osiris_url = self.osiris_url
.ok_or_else(|| OsirisClientError::ConfigError("osiris_url is required".to_string()))?;
// Build supervisor client if URL and secret are provided
let supervisor_client = if let (Some(url), Some(secret)) = (self.supervisor_url, self.supervisor_secret) {
Some(
hero_supervisor_openrpc_client::SupervisorClient::builder()
.url(url)
.secret(secret)
.build()
.map_err(|e| OsirisClientError::ConfigError(format!("Failed to create supervisor client: {:?}", e)))?
)
} else {
None
};
Ok(OsirisClient {
osiris_url,
supervisor_client,
runner_name: self.runner_name.unwrap_or_else(|| "osiris".to_string()),
timeout: self.timeout,
client: reqwest::Client::new(),
})
}
}
impl OsirisClient {
/// Create a new Osiris client (query-only)
pub fn new(osiris_url: impl Into<String>) -> Self {
Self {
osiris_url: osiris_url.into(),
supervisor_client: None,
runner_name: "osiris".to_string(),
timeout: 30,
client: reqwest::Client::new(),
}
}
/// Create a builder for full CQRS configuration
pub fn builder() -> OsirisClientBuilder {
OsirisClientBuilder::new()
}
/// Generic GET request for any struct by ID
pub async fn get<T>(&self, struct_name: &str, id: &str) -> Result<T, OsirisClientError>
where
T: for<'de> Deserialize<'de>,
{
let url = format!("{}/api/{}/{}", self.osiris_url, struct_name, id);
let response = self.client
.get(&url)
.send()
.await?;
if response.status() == 404 {
return Err(OsirisClientError::NotFound(format!("{}/{}", struct_name, id)));
}
let data = response
.json::<T>()
.await
.map_err(|e| OsirisClientError::DeserializationFailed(e.to_string()))?;
Ok(data)
}
/// Generic LIST request for all instances of a struct
pub async fn list<T>(&self, struct_name: &str) -> Result<Vec<T>, OsirisClientError>
where
T: for<'de> Deserialize<'de>,
{
let url = format!("{}/api/{}", self.osiris_url, struct_name);
let response = self.client
.get(&url)
.send()
.await?;
let data = response
.json::<Vec<T>>()
.await
.map_err(|e| OsirisClientError::DeserializationFailed(e.to_string()))?;
Ok(data)
}
/// Generic QUERY request with filters
pub async fn query<T>(&self, struct_name: &str, query: &str) -> Result<Vec<T>, OsirisClientError>
where
T: for<'de> Deserialize<'de>,
{
let url = format!("{}/api/{}?{}", self.osiris_url, struct_name, query);
let response = self.client
.get(&url)
.send()
.await?;
let data = response
.json::<Vec<T>>()
.await
.map_err(|e| OsirisClientError::DeserializationFailed(e.to_string()))?;
Ok(data)
}
// ========== Command Methods (Supervisor + Rhai) ==========
// Commands are write operations that execute Rhai scripts via the supervisor
// to modify state in Osiris
/// Execute a Rhai script via the Supervisor
pub async fn execute_script(&self, script: &str) -> Result<RunJobResponse, OsirisClientError> {
let supervisor_client = self.supervisor_client.as_ref()
.ok_or_else(|| OsirisClientError::ConfigError("supervisor_client not configured for commands".to_string()))?;
// Use JobBuilder from supervisor client (which re-exports from hero-job)
use hero_supervisor_openrpc_client::JobBuilder;
let job = JobBuilder::new()
.caller_id("osiris-client")
.context_id("command-execution")
.runner(&self.runner_name)
.payload(script)
.executor("rhai")
.timeout(self.timeout)
.build()
.map_err(|e| OsirisClientError::CommandFailed(format!("Failed to build job: {}", e)))?;
// Use job_run method which returns JobRunResponse
// Secret is sent via Authorization header (configured during client creation)
let result = supervisor_client.job_run(job, Some(self.timeout))
.await
.map_err(|e| OsirisClientError::CommandFailed(format!("{:?}", e)))?;
// Convert JobRunResponse to our RunJobResponse
Ok(RunJobResponse {
job_id: result.job_id,
status: result.status,
})
}
/// Execute a Rhai script template with variable substitution
pub async fn execute_template(&self, template: &str, variables: &HashMap<String, String>) -> Result<RunJobResponse, OsirisClientError> {
let script = substitute_variables(template, variables);
self.execute_script(&script).await
}
// ========== Supervisor-specific CQRS Methods ==========
/// Create an API key (Command - via Rhai)
pub async fn create_api_key(&self, key: String, name: String, scope: String) -> Result<RunJobResponse, OsirisClientError> {
let script = format!(
r#"
let api_key = new_api_key("{}", "{}", "{}", "{}");
save_api_key(api_key);
"#,
self.get_namespace(),
key,
name,
scope
);
self.execute_script(&script).await
}
/// Get an API key by key value (Query - via REST)
pub async fn get_api_key(&self, key: &str) -> Result<Option<serde_json::Value>, OsirisClientError> {
// Query by indexed field
let results: Vec<serde_json::Value> = self.query("ApiKey", &format!("key={}", key)).await?;
Ok(results.into_iter().next())
}
/// List all API keys (Query - via REST)
pub async fn list_api_keys(&self) -> Result<Vec<serde_json::Value>, OsirisClientError> {
self.list("ApiKey").await
}
/// Delete an API key (Command - via Rhai)
pub async fn delete_api_key(&self, key: String) -> Result<RunJobResponse, OsirisClientError> {
let script = format!(
r#"
delete_api_key("{}");
"#,
key
);
self.execute_script(&script).await
}
/// Create a runner (Command - via Rhai)
pub async fn create_runner(&self, runner_id: String, name: String, queue: String, registered_by: String) -> Result<RunJobResponse, OsirisClientError> {
let script = format!(
r#"
let runner = new_runner("{}", "{}", "{}", "{}", "{}");
save_runner(runner);
"#,
self.get_namespace(),
runner_id,
name,
queue,
registered_by
);
self.execute_script(&script).await
}
/// Get a runner by ID (Query - via REST)
pub async fn get_runner(&self, runner_id: &str) -> Result<Option<serde_json::Value>, OsirisClientError> {
let results: Vec<serde_json::Value> = self.query("Runner", &format!("runner_id={}", runner_id)).await?;
Ok(results.into_iter().next())
}
/// List all runners (Query - via REST)
pub async fn list_runners(&self) -> Result<Vec<serde_json::Value>, OsirisClientError> {
self.list("Runner").await
}
/// Delete a runner (Command - via Rhai)
pub async fn delete_runner(&self, runner_id: String) -> Result<RunJobResponse, OsirisClientError> {
let script = format!(
r#"
delete_runner("{}");
"#,
runner_id
);
self.execute_script(&script).await
}
/// Create job metadata (Command - via Rhai)
pub async fn create_job_metadata(&self, job_id: String, runner: String, created_by: String, payload: String) -> Result<RunJobResponse, OsirisClientError> {
let script = format!(
r#"
let job = new_job_metadata("{}", "{}", "{}", "{}", "{}");
save_job_metadata(job);
"#,
self.get_namespace(),
job_id,
runner,
created_by,
payload
);
self.execute_script(&script).await
}
/// Get job metadata by ID (Query - via REST)
pub async fn get_job_metadata(&self, job_id: &str) -> Result<Option<serde_json::Value>, OsirisClientError> {
let results: Vec<serde_json::Value> = self.query("JobMetadata", &format!("job_id={}", job_id)).await?;
Ok(results.into_iter().next())
}
/// List all job metadata (Query - via REST)
pub async fn list_job_metadata(&self) -> Result<Vec<serde_json::Value>, OsirisClientError> {
self.list("JobMetadata").await
}
/// List jobs by runner (Query - via REST)
pub async fn list_jobs_by_runner(&self, runner: &str) -> Result<Vec<serde_json::Value>, OsirisClientError> {
self.query("JobMetadata", &format!("runner={}", runner)).await
}
/// List jobs by creator (Query - via REST)
pub async fn list_jobs_by_creator(&self, creator: &str) -> Result<Vec<serde_json::Value>, OsirisClientError> {
self.query("JobMetadata", &format!("created_by={}", creator)).await
}
// Helper method to get namespace
fn get_namespace(&self) -> &str {
"supervisor"
}
}
// ========== Helper Structures ==========
#[derive(Serialize)]
struct RunJobRequest {
runner_name: String,
script: String,
timeout: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
env: Option<HashMap<String, String>>,
}
#[derive(Deserialize, Debug, Clone)]
pub struct RunJobResponse {
pub job_id: String,
pub status: String,
}
/// Helper function to substitute variables in a Rhai script template
pub fn substitute_variables(template: &str, variables: &HashMap<String, String>) -> String {
let mut result = template.to_string();
for (key, value) in variables {
let placeholder = format!("{{{{ {} }}}}", key);
result = result.replace(&placeholder, value);
}
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_client_creation() {
let client = OsirisClient::new("http://localhost:8080");
assert_eq!(client.osiris_url, "http://localhost:8080");
}
#[test]
fn test_builder() {
let client = OsirisClient::builder()
.osiris_url("http://localhost:8081")
.supervisor_url("http://localhost:3030")
.supervisor_secret("test_secret")
.runner_name("osiris")
.build()
.unwrap();
assert_eq!(client.osiris_url, "http://localhost:8081");
assert_eq!(client.supervisor_url, Some("http://localhost:3030".to_string()));
assert_eq!(client.runner_name, "osiris");
}
#[test]
fn test_substitute_variables() {
let template = "let x = {{ value }}; let y = {{ name }};";
let mut vars = HashMap::new();
vars.insert("value".to_string(), "42".to_string());
vars.insert("name".to_string(), "\"test\"".to_string());
let result = substitute_variables(template, &vars);
assert_eq!(result, "let x = 42; let y = \"test\";");
}
}

View File

@@ -0,0 +1,39 @@
//! Payment query methods
use serde::{Deserialize, Serialize};
use crate::{OsirisClient, OsirisClientError};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Payment {
pub id: String,
pub amount: f64,
pub currency: String,
pub status: PaymentStatus,
pub description: String,
pub payment_url: Option<String>,
pub created_at: i64,
pub updated_at: i64,
pub completed_at: Option<i64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum PaymentStatus {
Pending,
Processing,
Completed,
Failed,
Cancelled,
}
impl OsirisClient {
/// Get payment by ID
pub async fn get_payment(&self, payment_id: &str) -> Result<Payment, OsirisClientError> {
self.get("payment", payment_id).await
}
/// List all payments
pub async fn list_payments(&self) -> Result<Vec<Payment>, OsirisClientError> {
self.list("payment").await
}
}

View File

@@ -0,0 +1,37 @@
// KYC verification script template
// Variables: {{resident_id}}, {{callback_url}}
print("=== Starting KYC Verification ===");
print("Resident ID: {{resident_id}}");
// Get freezone context
let freezone_pubkey = "04e58314c13ea3f9caed882001a5090797b12563d5f9bbd7f16efe020e060c780b446862311501e2e9653416527d2634ff8a8050ff3a085baccd7ddcb94185ff56";
let freezone_ctx = get_context([freezone_pubkey]);
// Get KYC client from context
let kyc_client = freezone_ctx.get("kyc_client");
if kyc_client == () {
print("ERROR: KYC client not configured");
return #{
success: false,
error: "KYC client not configured"
};
}
// Create KYC session
let session = kyc_client.create_session(
"{{resident_id}}",
"{{callback_url}}"
);
print("✓ KYC session created");
print(" Session ID: " + session.session_id);
print(" KYC URL: " + session.kyc_url);
// Return response
#{
success: true,
session_id: session.session_id,
kyc_url: session.kyc_url,
expires_at: session.expires_at
}

2
lib/clients/supervisor/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
pkg
target

View File

@@ -0,0 +1,59 @@
[package]
name = "hero-supervisor-openrpc-client-wasm"
version = "0.1.0"
edition = "2021"
description = "WASM-compatible OpenRPC client for Hero Supervisor"
license = "MIT OR Apache-2.0"
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
# WASM bindings
wasm-bindgen = "0.2"
wasm-bindgen-futures = "0.4"
js-sys = "0.3"
# Web APIs
web-sys = { version = "0.3", features = [
"console",
"Request",
"RequestInit",
"RequestMode",
"Response",
"Window",
"Headers",
"AbortController",
"AbortSignal",
] }
# Serialization
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
serde-wasm-bindgen = "0.6"
# Error handling
thiserror = "1.0"
# UUID for job IDs
uuid = { version = "1.0", features = ["v4", "serde", "js"] }
# Time handling
chrono = { version = "0.4", features = ["serde", "wasmbind"] }
# Collections
indexmap = "2.0"
# Logging for WASM
log = "0.4"
console_log = "1.0"
# Async utilities
futures = "0.3"
[dependencies.getrandom]
version = "0.2"
features = ["js"]
[dev-dependencies]
wasm-bindgen-test = "0.3"

View File

@@ -0,0 +1,77 @@
[package]
name = "hero-supervisor-openrpc-client"
version.workspace = true
edition.workspace = true
description = "OpenRPC client for Hero Supervisor"
license = "MIT OR Apache-2.0"
[lib]
crate-type = ["cdylib", "rlib"]
[features]
default = []
[dependencies]
# Common dependencies for both native and WASM
serde.workspace = true
serde_json.workspace = true
thiserror.workspace = true
log.workspace = true
uuid.workspace = true
indexmap.workspace = true
hero-job = { path = "../../models/job" }
# Native JSON-RPC client (not WASM compatible)
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
jsonrpsee = { workspace = true, features = ["http-client", "macros"] }
tokio.workspace = true
# hero-job-client removed - now part of supervisor
env_logger.workspace = true
http.workspace = true
# WASM-specific dependencies
[target.'cfg(target_arch = "wasm32")'.dependencies]
wasm-bindgen.workspace = true
wasm-bindgen-futures.workspace = true
js-sys.workspace = true
serde-wasm-bindgen.workspace = true
web-sys = { workspace = true, features = [
"console",
"Request",
"RequestInit",
"RequestMode",
"Response",
"Headers",
"Window",
] }
console_log.workspace = true
getrandom.workspace = true
# Crypto for signing
secp256k1.workspace = true
sha2.workspace = true
hex.workspace = true
[target.'cfg(target_arch = "wasm32")'.dev-dependencies]
wasm-bindgen-test = "0.3"
# UUID for job IDs (native)
[target.'cfg(not(target_arch = "wasm32"))'.dependencies.uuid]
workspace = true
# Time handling (native)
[target.'cfg(not(target_arch = "wasm32"))'.dependencies.chrono]
workspace = true
# WASM-compatible dependencies
[target.'cfg(target_arch = "wasm32")'.dependencies.chrono]
workspace = true
features = ["wasmbind"]
[target.'cfg(target_arch = "wasm32")'.dependencies.uuid]
workspace = true
features = ["js"]
[dev-dependencies]
# Testing utilities
tokio-test = "0.4"

View File

@@ -0,0 +1,180 @@
# Hero Supervisor OpenRPC Client
A Rust client library for interacting with the Hero Supervisor OpenRPC server. This crate provides a simple, async interface for managing actors and jobs remotely.
## Features
- **Async API**: Built on `tokio` and `jsonrpsee` for high-performance async operations
- **Type Safety**: Full Rust type safety with serde serialization/deserialization
- **Job Builder**: Fluent API for creating jobs with validation
- **Comprehensive Coverage**: All supervisor operations available via client
- **Error Handling**: Detailed error types with proper error propagation
## Installation
Add this to your `Cargo.toml`:
```toml
[dependencies]
hero-supervisor-openrpc-client = "0.1.0"
tokio = { version = "1.0", features = ["full"] }
```
## Quick Start
```rust
use hero_supervisor_openrpc_client::{SupervisorClient, JobBuilder};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Create a client with admin secret
let client = SupervisorClient::new("http://127.0.0.1:3030", "your-admin-secret")?;
// Register a runner (runner must be started externally)
client.register_runner("admin-secret", "my_runner").await?;
// Create and run a job
let job = JobBuilder::new()
.caller_id("my_client")
.context_id("example_context")
.payload("echo 'Hello from Hero Supervisor!'")
.executor("bash")
.runner("my_runner")
.timeout(60)
.build()?;
client.queue_job_to_runner("my_actor", job).await?;
// Check runner status
let status = client.get_runner_status("my_actor").await?;
println!("Runner status: {:?}", status);
// List all runners
let runners = client.list_runners().await?;
println!("Active runners: {:?}", runners);
Ok(())
}
```
## API Reference
### Client Creation
```rust
let client = SupervisorClient::new("http://127.0.0.1:3030")?;
```
### Runner Management
```rust
// Register a runner
client.register_runner("admin-secret", "my_runner").await?;
// Remove a runner
client.remove_runner("admin-secret", "my_runner").await?;
// List all runners
let runners = client.list_runners().await?;
// Start/stop runners
client.start_runner("actor_id").await?;
client.stop_runner("actor_id", false).await?; // force = false
// Get runner status
let status = client.get_runner_status("actor_id").await?;
// Get runner logs
let logs = client.get_runner_logs("actor_id", Some(100), false).await?;
```
### Job Management
```rust
// Create a job using the builder
let job = JobBuilder::new()
.caller_id("client_id")
.context_id("context_id")
.payload("script_content")
.job_type(JobType::OSIS)
.runner("target_actor")
.timeout(Duration::from_secs(300))
.env_var("KEY", "value")
.build()?;
// Queue the job
client.queue_job_to_runner("actor_id", job).await?;
```
### Bulk Operations
```rust
// Start all runners
let results = client.start_all().await?;
// Stop all runners
let results = client.stop_all(false).await?; // force = false
// Get status of all runners
let statuses = client.get_all_runner_status().await?;
```
## Types
### RunnerType
- `SALRunner` - System abstraction layer operations
- `OSISRunner` - Operating system interface operations
- `VRunner` - Virtualization operations
- `PyRunner` - Python-based actors
### JobType
- `SAL` - SAL job type
- `OSIS` - OSIS job type
- `V` - V job type
- `Python` - Python job type
### Runner Management
Runners are expected to be started and managed externally. The supervisor only tracks which runners are registered and queues jobs to them via Redis.
### ProcessStatus
- `Running` - Process is active
- `Stopped` - Process is stopped
- `Failed` - Process failed
- `Unknown` - Status unknown
## Error Handling
The client uses the `ClientError` enum for error handling:
```rust
use hero_supervisor_openrpc_client::ClientError;
match client.start_runner("actor_id").await {
Ok(()) => println!("Runner started successfully"),
Err(ClientError::JsonRpc(e)) => println!("JSON-RPC error: {}", e),
Err(ClientError::Server { message }) => println!("Server error: {}", message),
Err(e) => println!("Other error: {}", e),
}
```
## Examples
See the `examples/` directory for complete usage examples:
- `basic_client.rs` - Basic client usage
- `job_management.rs` - Job creation and management
- `runner_lifecycle.rs` - Complete runner lifecycle management
## Requirements
- Rust 1.70+
- Hero Supervisor server running with OpenRPC feature enabled
- Network access to the supervisor server
## License
Licensed under either of Apache License, Version 2.0 or MIT license at your option.

View File

@@ -0,0 +1,29 @@
#!/bin/bash
# Build script for WASM-compatible OpenRPC client
set -e
echo "Building WASM OpenRPC client..."
# Check if wasm-pack is installed
if ! command -v wasm-pack &> /dev/null; then
echo "wasm-pack is not installed. Installing..."
curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
fi
# Build the WASM package
echo "Building WASM package..."
wasm-pack build --target web --out-dir pkg-wasm
echo "WASM build complete! Package available in pkg-wasm/"
echo ""
echo "To use in a web project:"
echo "1. Copy the pkg-wasm directory to your web project"
echo "2. Import the module in your JavaScript:"
echo " import init, { WasmSupervisorClient, create_client, create_job } from './pkg-wasm/hero_supervisor_openrpc_client_wasm.js';"
echo "3. Initialize the WASM module:"
echo " await init();"
echo "4. Create and use the client:"
echo " const client = create_client('http://localhost:3030');"
echo " const runners = await client.list_runners();"

View File

@@ -0,0 +1,202 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Hero Supervisor WASM OpenRPC Client Example</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
.container {
background: #f5f5f5;
padding: 20px;
border-radius: 8px;
margin: 10px 0;
}
button {
background: #007cba;
color: white;
border: none;
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
margin: 5px;
}
button:hover {
background: #005a87;
}
button:disabled {
background: #ccc;
cursor: not-allowed;
}
input, textarea {
width: 100%;
padding: 8px;
margin: 5px 0;
border: 1px solid #ddd;
border-radius: 4px;
}
.output {
background: #fff;
border: 1px solid #ddd;
padding: 10px;
margin: 10px 0;
border-radius: 4px;
white-space: pre-wrap;
font-family: monospace;
max-height: 200px;
overflow-y: auto;
}
.error {
color: #d32f2f;
}
.success {
color: #2e7d32;
}
</style>
</head>
<body>
<h1>Hero Supervisor WASM OpenRPC Client</h1>
<div class="container">
<h2>Connection</h2>
<input type="text" id="serverUrl" placeholder="Server URL" value="http://localhost:3030">
<button onclick="testConnection()">Test Connection</button>
<div id="connectionOutput" class="output"></div>
</div>
<div class="container">
<h2>Runner Management</h2>
<button onclick="listRunners()">List Runners</button>
<div id="runnersOutput" class="output"></div>
<h3>Register Runner</h3>
<input type="text" id="registerSecret" placeholder="Secret" value="admin123">
<input type="text" id="runnerName" placeholder="Runner Name" value="wasm_runner">
<input type="text" id="runnerQueue" placeholder="Queue Name" value="wasm_queue">
<button onclick="registerRunner()">Register Runner</button>
<div id="registerOutput" class="output"></div>
</div>
<div class="container">
<h2>Job Execution</h2>
<input type="text" id="jobSecret" placeholder="Secret" value="admin123">
<input type="text" id="jobId" placeholder="Job ID" value="">
<input type="text" id="jobRunnerName" placeholder="Runner Name" value="wasm_runner">
<textarea id="jobPayload" placeholder="Job Payload" rows="3">echo "Hello from WASM client!"</textarea>
<button onclick="runJob()">Run Job</button>
<div id="jobOutput" class="output"></div>
</div>
<script type="module">
import init, {
WasmSupervisorClient,
WasmJob,
create_client,
create_job
} from './pkg-wasm/hero_supervisor_openrpc_client_wasm.js';
let client = null;
// Initialize WASM module
async function initWasm() {
try {
await init();
console.log('WASM module initialized');
document.getElementById('connectionOutput').textContent = 'WASM module loaded successfully';
document.getElementById('connectionOutput').className = 'output success';
} catch (error) {
console.error('Failed to initialize WASM:', error);
document.getElementById('connectionOutput').textContent = `Failed to initialize WASM: ${error}`;
document.getElementById('connectionOutput').className = 'output error';
}
}
// Test connection to supervisor
window.testConnection = async function() {
try {
const serverUrl = document.getElementById('serverUrl').value;
client = create_client(serverUrl);
const result = await client.discover();
document.getElementById('connectionOutput').textContent = `Connection successful!\n${JSON.stringify(result, null, 2)}`;
document.getElementById('connectionOutput').className = 'output success';
} catch (error) {
document.getElementById('connectionOutput').textContent = `Connection failed: ${error}`;
document.getElementById('connectionOutput').className = 'output error';
}
};
// List all runners
window.listRunners = async function() {
try {
if (!client) {
throw new Error('Client not initialized. Test connection first.');
}
const runners = await client.list_runners();
document.getElementById('runnersOutput').textContent = `Runners:\n${JSON.stringify(runners, null, 2)}`;
document.getElementById('runnersOutput').className = 'output success';
} catch (error) {
document.getElementById('runnersOutput').textContent = `Failed to list runners: ${error}`;
document.getElementById('runnersOutput').className = 'output error';
}
};
// Register a new runner
window.registerRunner = async function() {
try {
if (!client) {
throw new Error('Client not initialized. Test connection first.');
}
const secret = document.getElementById('registerSecret').value;
const name = document.getElementById('runnerName').value;
const queue = document.getElementById('runnerQueue').value;
await client.register_runner(secret, name, queue);
document.getElementById('registerOutput').textContent = `Runner '${name}' registered successfully!`;
document.getElementById('registerOutput').className = 'output success';
} catch (error) {
document.getElementById('registerOutput').textContent = `Failed to register runner: ${error}`;
document.getElementById('registerOutput').className = 'output error';
}
};
// Run a job
window.runJob = async function() {
try {
if (!client) {
throw new Error('Client not initialized. Test connection first.');
}
const secret = document.getElementById('jobSecret').value;
let jobId = document.getElementById('jobId').value;
const runnerName = document.getElementById('jobRunnerName').value;
const payload = document.getElementById('jobPayload').value;
// Generate job ID if not provided
if (!jobId) {
jobId = 'job_' + Math.random().toString(36).substr(2, 9);
document.getElementById('jobId').value = jobId;
}
const job = create_job(jobId, payload, "SAL", runnerName);
const result = await client.run_job(secret, job);
document.getElementById('jobOutput').textContent = `Job executed successfully!\nJob ID: ${jobId}\nResult: ${result}`;
document.getElementById('jobOutput').className = 'output success';
} catch (error) {
document.getElementById('jobOutput').textContent = `Failed to run job: ${error}`;
document.getElementById('jobOutput').className = 'output error';
}
};
// Initialize on page load
initWasm();
</script>
</body>
</html>

View File

@@ -0,0 +1,102 @@
//! Builder pattern for WasmSupervisorClient to ensure proper configuration
//!
//! This module provides a type-safe builder that guarantees a client cannot be
//! created without a secret, preventing authentication issues.
use crate::wasm::WasmSupervisorClient;
/// Builder for WasmSupervisorClient that enforces secret requirement
#[derive(Clone)]
pub struct WasmSupervisorClientBuilder {
server_url: Option<String>,
secret: Option<String>,
}
impl WasmSupervisorClientBuilder {
/// Create a new builder
pub fn new() -> Self {
Self {
server_url: None,
secret: None,
}
}
/// Set the server URL
pub fn server_url(mut self, url: impl Into<String>) -> Self {
self.server_url = Some(url.into());
self
}
/// Set the authentication secret (required)
pub fn secret(mut self, secret: impl Into<String>) -> Self {
self.secret = Some(secret.into());
self
}
/// Build the client
///
/// Returns Err if server_url or secret is not set
pub fn build(self) -> Result<WasmSupervisorClient, String> {
let server_url = self.server_url.ok_or("Server URL is required")?;
let secret = self.secret.ok_or("Secret is required for authenticated client")?;
if secret.is_empty() {
return Err("Secret cannot be empty".to_string());
}
Ok(WasmSupervisorClient::new(server_url, secret))
}
}
impl Default for WasmSupervisorClientBuilder {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_builder_requires_all_fields() {
let builder = WasmSupervisorClientBuilder::new();
assert!(builder.build().is_err());
let builder = WasmSupervisorClientBuilder::new()
.server_url("http://localhost:3030");
assert!(builder.build().is_err());
let builder = WasmSupervisorClientBuilder::new()
.secret("test-secret");
assert!(builder.build().is_err());
}
#[test]
fn test_builder_success() {
let builder = WasmSupervisorClientBuilder::new()
.server_url("http://localhost:3030")
.secret("test-secret");
assert!(builder.build().is_ok());
}
#[test]
fn test_build_error_messages() {
let result = WasmSupervisorClientBuilder::new().build();
assert!(result.is_err());
assert_eq!(result.unwrap_err(), "Server URL is required");
let result = WasmSupervisorClientBuilder::new()
.server_url("http://localhost:3030")
.build();
assert!(result.is_err());
assert_eq!(result.unwrap_err(), "Secret is required for authenticated client");
let result = WasmSupervisorClientBuilder::new()
.server_url("http://localhost:3030")
.secret("")
.build();
assert!(result.is_err());
assert_eq!(result.unwrap_err(), "Secret cannot be empty");
}
}

View File

@@ -0,0 +1,695 @@
use serde::{Deserialize, Serialize};
use thiserror::Error;
use serde_json;
// Import types from the main supervisor crate
// WASM-compatible client module
#[cfg(target_arch = "wasm32")]
pub mod wasm;
// Builder module for type-safe client construction
#[cfg(target_arch = "wasm32")]
pub mod builder;
// Re-export WASM types for convenience
#[cfg(target_arch = "wasm32")]
pub use wasm::{WasmSupervisorClient, WasmJobType, WasmRunnerType, create_job_canonical_repr, sign_job_canonical};
// Re-export builder for convenience
#[cfg(target_arch = "wasm32")]
pub use builder::WasmSupervisorClientBuilder;
// Native client dependencies
#[cfg(not(target_arch = "wasm32"))]
use jsonrpsee::{
core::client::ClientT,
http_client::{HttpClient, HttpClientBuilder},
rpc_params,
};
#[cfg(not(target_arch = "wasm32"))]
use http::{HeaderMap, HeaderName, HeaderValue};
#[cfg(not(target_arch = "wasm32"))]
use std::path::PathBuf;
/// Client for communicating with Hero Supervisor OpenRPC server
/// Requires authentication secret for all operations
#[cfg(not(target_arch = "wasm32"))]
#[derive(Clone)]
pub struct SupervisorClient {
client: HttpClient,
server_url: String,
secret: String,
}
/// Error types for client operations
#[cfg(not(target_arch = "wasm32"))]
#[derive(Error, Debug)]
pub enum ClientError {
#[error("JSON-RPC error: {0}")]
JsonRpc(#[from] jsonrpsee::core::ClientError),
#[error("Serialization error: {0}")]
Serialization(#[from] serde_json::Error),
#[error("HTTP client error: {0}")]
Http(String),
#[error("Server error: {message}")]
Server { message: String },
}
/// Error types for WASM client operations
#[cfg(target_arch = "wasm32")]
#[derive(Error, Debug)]
pub enum ClientError {
#[error("JSON-RPC error: {0}")]
JsonRpc(String),
#[error("Serialization error: {0}")]
Serialization(#[from] serde_json::Error),
#[error("HTTP client error: {0}")]
Http(String),
#[error("Server error: {message}")]
Server { message: String },
#[error("JavaScript error: {0}")]
JavaScript(String),
#[error("Network error: {0}")]
Network(String),
}
// Implement From for jsonrpsee ClientError for WASM
#[cfg(target_arch = "wasm32")]
impl From<wasm_bindgen::JsValue> for ClientError {
fn from(js_val: wasm_bindgen::JsValue) -> Self {
ClientError::JavaScript(format!("{:?}", js_val))
}
}
/// Result type for client operations
pub type ClientResult<T> = Result<T, ClientError>;
/// Request parameters for generating API keys (auto-generates key value)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GenerateApiKeyParams {
pub name: String,
pub scope: String, // "admin", "registrar", or "user"
}
/// Configuration for a runner
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RunnerConfig {
/// Name of the runner
pub name: String,
/// Command to run the runner (full command line)
pub command: String,
/// Optional environment variables
#[serde(skip_serializing_if = "Option::is_none")]
pub env: Option<std::collections::HashMap<String, String>>,
}
/// Job result response
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum JobResult {
Success { success: String },
Error { error: String },
}
/// Job status response
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JobStatusResponse {
pub job_id: String,
pub status: String,
pub created_at: String,
pub started_at: Option<String>,
pub completed_at: Option<String>,
}
/// Response from job.run (blocking execution)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JobRunResponse {
pub job_id: String,
pub status: String,
pub result: Option<String>,
}
/// Response from job.start (non-blocking execution)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JobStartResponse {
pub job_id: String,
pub status: String,
}
// Re-export Job types from hero-job crate (both native and WASM)
pub use hero_job::{Job, JobStatus, JobError, JobBuilder, JobSignature};
// Note: Job client is now part of hero-supervisor crate
// Re-exports removed - use hero_supervisor::job_client directly if needed
/// Process status wrapper for OpenRPC serialization (matches server response)
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum ProcessStatusWrapper {
Running,
Stopped,
Starting,
Stopping,
Error(String),
}
/// Log information wrapper for OpenRPC serialization (matches server response)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LogInfoWrapper {
pub timestamp: String,
pub level: String,
pub message: String,
}
/// Supervisor information response containing secret counts and server details
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SupervisorInfo {
pub server_url: String,
}
/// API Key information
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ApiKey {
pub key: String,
pub name: String,
pub scope: String,
pub created_at: String,
}
/// Auth verification response
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AuthVerifyResponse {
pub scope: String,
pub name: Option<String>,
pub created_at: Option<String>,
}
/// Simple ProcessStatus type for native builds to avoid service manager dependency
#[cfg(not(target_arch = "wasm32"))]
pub type ProcessStatus = ProcessStatusWrapper;
// Types duplicated from supervisor-core to avoid cyclic dependency
// These match the types in hero-supervisor but are defined here independently
/// Runner status information (duplicated to avoid cyclic dependency)
#[cfg(not(target_arch = "wasm32"))]
pub type RunnerStatus = ProcessStatusWrapper;
/// Log information (duplicated to avoid cyclic dependency)
#[cfg(not(target_arch = "wasm32"))]
pub type LogInfo = LogInfoWrapper;
/// Type aliases for WASM compatibility
#[cfg(target_arch = "wasm32")]
pub type ProcessStatus = ProcessStatusWrapper;
#[cfg(target_arch = "wasm32")]
pub type RunnerStatus = ProcessStatusWrapper;
#[cfg(target_arch = "wasm32")]
pub type LogInfo = LogInfoWrapper;
/// Builder for SupervisorClient
#[cfg(not(target_arch = "wasm32"))]
#[derive(Debug, Clone)]
pub struct SupervisorClientBuilder {
url: Option<String>,
secret: Option<String>,
timeout: Option<std::time::Duration>,
}
#[cfg(not(target_arch = "wasm32"))]
impl SupervisorClientBuilder {
/// Create a new builder
pub fn new() -> Self {
Self {
url: None,
secret: None,
timeout: Some(std::time::Duration::from_secs(30)),
}
}
/// Set the server URL
pub fn url(mut self, url: impl Into<String>) -> Self {
self.url = Some(url.into());
self
}
/// Set the authentication secret
pub fn secret(mut self, secret: impl Into<String>) -> Self {
self.secret = Some(secret.into());
self
}
/// Set the request timeout (default: 30 seconds)
pub fn timeout(mut self, timeout: std::time::Duration) -> Self {
self.timeout = Some(timeout);
self
}
/// Build the SupervisorClient
pub fn build(self) -> ClientResult<SupervisorClient> {
let server_url = self.url
.ok_or_else(|| ClientError::Http("URL is required".to_string()))?;
let secret = self.secret
.ok_or_else(|| ClientError::Http("Secret is required".to_string()))?;
// Create headers with Authorization bearer token
let mut headers = HeaderMap::new();
let auth_value = format!("Bearer {}", secret);
headers.insert(
HeaderName::from_static("authorization"),
HeaderValue::from_str(&auth_value)
.map_err(|e| ClientError::Http(format!("Invalid auth header: {}", e)))?
);
let client = HttpClientBuilder::default()
.request_timeout(self.timeout.unwrap_or(std::time::Duration::from_secs(30)))
.set_headers(headers)
.build(&server_url)
.map_err(|e| ClientError::Http(e.to_string()))?;
Ok(SupervisorClient {
client,
server_url,
secret,
})
}
}
#[cfg(not(target_arch = "wasm32"))]
impl Default for SupervisorClientBuilder {
fn default() -> Self {
Self::new()
}
}
#[cfg(not(target_arch = "wasm32"))]
impl SupervisorClient {
/// Create a builder for SupervisorClient
pub fn builder() -> SupervisorClientBuilder {
SupervisorClientBuilder::new()
}
/// Get the server URL
pub fn server_url(&self) -> &str {
&self.server_url
}
/// Test connection using OpenRPC discovery method
/// This calls the standard `rpc.discover` method that should be available on any OpenRPC server
pub async fn discover(&self) -> ClientResult<serde_json::Value> {
let result: serde_json::Value = self
.client
.request("rpc.discover", rpc_params![])
.await.map_err(|e| ClientError::JsonRpc(e))?;
Ok(result)
}
/// Register a new runner to the supervisor
/// The runner name is also used as the queue name
/// Authentication via Authorization header (set during client creation)
pub async fn runner_create(
&self,
name: &str,
) -> ClientResult<()> {
let _: () = self
.client
.request("runner.create", rpc_params![name])
.await.map_err(|e| ClientError::JsonRpc(e))?;
Ok(())
}
/// Create a new job without queuing it to a runner
/// Authentication via Authorization header (set during client creation)
pub async fn job_create(
&self,
job: Job,
) -> ClientResult<String> {
let job_id: String = self
.client
.request("job.create", rpc_params![job])
.await.map_err(|e| ClientError::JsonRpc(e))?;
Ok(job_id)
}
/// List all jobs
pub async fn job_list(&self) -> ClientResult<Vec<Job>> {
let jobs: Vec<Job> = self
.client
.request("job.list", rpc_params![])
.await.map_err(|e| ClientError::JsonRpc(e))?;
Ok(jobs)
}
/// Run a job on the appropriate runner and wait for the result (blocking)
/// This method queues the job and waits for completion before returning
/// The secret is sent via Authorization header (set during client creation)
pub async fn job_run(
&self,
job: Job,
timeout: Option<u64>,
) -> ClientResult<JobRunResponse> {
let mut params = serde_json::json!({
"job": job
});
if let Some(t) = timeout {
params["timeout"] = serde_json::json!(t);
}
let result: JobRunResponse = self
.client
.request("job.run", rpc_params![params])
.await.map_err(|e| ClientError::JsonRpc(e))?;
Ok(result)
}
/// Start a job without waiting for the result (non-blocking)
/// This method queues the job and returns immediately with the job_id
/// Authentication via Authorization header (set during client creation)
pub async fn job_start(
&self,
job: Job,
) -> ClientResult<JobStartResponse> {
let params = serde_json::json!({
"job": job
});
let result: JobStartResponse = self
.client
.request("job.start", rpc_params![params])
.await.map_err(|e| ClientError::JsonRpc(e))?;
Ok(result)
}
/// Get the current status of a job
pub async fn job_status(&self, job_id: &str) -> ClientResult<JobStatus> {
let status: JobStatus = self
.client
.request("job.status", rpc_params![job_id])
.await.map_err(|e| ClientError::JsonRpc(e))?;
Ok(status)
}
/// Get the result of a completed job (blocks until result is available)
pub async fn job_result(&self, job_id: &str) -> ClientResult<JobResult> {
let result: JobResult = self
.client
.request("job.result", rpc_params![job_id])
.await.map_err(|e| ClientError::JsonRpc(e))?;
Ok(result)
}
/// Remove a runner from the supervisor
/// Authentication via Authorization header (set during client creation)
pub async fn runner_remove(&self, runner_id: &str) -> ClientResult<()> {
let _: () = self
.client
.request("runner.remove", rpc_params![runner_id])
.await.map_err(|e| ClientError::JsonRpc(e))?;
Ok(())
}
/// List all runner IDs
pub async fn runner_list(&self) -> ClientResult<Vec<String>> {
let runners: Vec<String> = self
.client
.request("runner.list", rpc_params![])
.await.map_err(|e| ClientError::JsonRpc(e))?;
Ok(runners)
}
/// Start a specific runner
/// Authentication via Authorization header (set during client creation)
pub async fn start_runner(&self, actor_id: &str) -> ClientResult<()> {
let _: () = self
.client
.request("runner.start", rpc_params![actor_id])
.await.map_err(|e| ClientError::JsonRpc(e))?;
Ok(())
}
/// Add a runner to the supervisor
/// Authentication via Authorization header (set during client creation)
pub async fn add_runner(&self, config: RunnerConfig) -> ClientResult<()> {
let params = serde_json::json!({
"config": config
});
let _: () = self
.client
.request("runner.add", rpc_params![params])
.await.map_err(|e| ClientError::JsonRpc(e))?;
Ok(())
}
/// Get status of a specific runner
/// Authentication via Authorization header (set during client creation)
pub async fn get_runner_status(&self, actor_id: &str) -> ClientResult<RunnerStatus> {
let status: RunnerStatus = self
.client
.request("runner.status", rpc_params![actor_id])
.await.map_err(|e| ClientError::JsonRpc(e))?;
Ok(status)
}
/// Get logs for a specific runner
pub async fn get_runner_logs(
&self,
actor_id: &str,
lines: Option<usize>,
follow: bool,
) -> ClientResult<Vec<LogInfo>> {
let logs: Vec<LogInfo> = self
.client
.request("get_runner_logs", rpc_params![actor_id, lines, follow])
.await.map_err(|e| ClientError::JsonRpc(e))?;
Ok(logs)
}
/// Queue a job to a specific runner
pub async fn queue_job_to_runner(&self, runner: &str, job: Job) -> ClientResult<()> {
let params = serde_json::json!({
"runner": runner,
"job": job
});
let _: () = self
.client
.request("queue_job_to_runner", rpc_params![params])
.await.map_err(|e| ClientError::JsonRpc(e))?;
Ok(())
}
/// Queue a job and wait for completion
pub async fn queue_and_wait(&self, runner: &str, job: Job, timeout_secs: u64) -> ClientResult<Option<String>> {
let params = serde_json::json!({
"runner": runner,
"job": job,
"timeout_secs": timeout_secs
});
let result: Option<String> = self
.client
.request("queue_and_wait", rpc_params![params])
.await.map_err(|e| ClientError::JsonRpc(e))?;
Ok(result)
}
/// Run a job on a specific runner
pub async fn run_job(&self, job: Job) -> ClientResult<JobResult> {
let params = serde_json::json!({
"job": job
});
let result: JobResult = self
.client
.request("job.run", rpc_params![params])
.await.map_err(|e| ClientError::JsonRpc(e))?;
Ok(result)
}
/// Get job result by job ID
pub async fn get_job_result(&self, job_id: &str) -> ClientResult<Option<String>> {
let result: Option<String> = self
.client
.request("get_job_result", rpc_params![job_id])
.await.map_err(|e| ClientError::JsonRpc(e))?;
Ok(result)
}
/// Get status of all runners
pub async fn get_all_runner_status(&self) -> ClientResult<Vec<(String, RunnerStatus)>> {
let statuses: Vec<(String, RunnerStatus)> = self
.client
.request("get_all_runner_status", rpc_params![])
.await.map_err(|e| ClientError::JsonRpc(e))?;
Ok(statuses)
}
/// Start all runners
pub async fn start_all(&self) -> ClientResult<Vec<(String, bool)>> {
let results: Vec<(String, bool)> = self
.client
.request("start_all", rpc_params![])
.await.map_err(|e| ClientError::JsonRpc(e))?;
Ok(results)
}
/// Stop all runners
pub async fn stop_all(&self, force: bool) -> ClientResult<Vec<(String, bool)>> {
let results: Vec<(String, bool)> = self
.client
.request("stop_all", rpc_params![force])
.await.map_err(|e| ClientError::JsonRpc(e))?;
Ok(results)
}
/// Get status of all runners (alternative method)
pub async fn get_all_status(&self) -> ClientResult<Vec<(String, RunnerStatus)>> {
let statuses: Vec<(String, RunnerStatus)> = self
.client
.request("get_all_status", rpc_params![])
.await.map_err(|e| ClientError::JsonRpc(e))?;
Ok(statuses)
}
/// Add a secret to the supervisor
pub async fn add_secret(
&self,
secret_type: &str,
secret_value: &str,
) -> ClientResult<()> {
let params = serde_json::json!({
"secret_type": secret_type,
"secret_value": secret_value
});
let _: () = self
.client
.request("add_secret", rpc_params![params])
.await.map_err(|e| ClientError::JsonRpc(e))?;
Ok(())
}
/// Remove a secret from the supervisor
pub async fn remove_secret(
&self,
secret_type: &str,
secret_value: &str,
) -> ClientResult<()> {
let params = serde_json::json!({
"secret_type": secret_type,
"secret_value": secret_value
});
let _: () = self
.client
.request("remove_secret", rpc_params![params])
.await.map_err(|e| ClientError::JsonRpc(e))?;
Ok(())
}
/// List secrets (returns supervisor info including secret counts)
pub async fn list_secrets(&self) -> ClientResult<SupervisorInfo> {
let params = serde_json::json!({});
let info: SupervisorInfo = self
.client
.request("list_secrets", rpc_params![params])
.await.map_err(|e| ClientError::JsonRpc(e))?;
Ok(info)
}
/// Stop a running job
pub async fn job_stop(&self, job_id: &str) -> ClientResult<()> {
let _: () = self.client
.request("job.stop", rpc_params![job_id])
.await.map_err(|e| ClientError::JsonRpc(e))?;
Ok(())
}
/// Delete a job from the system
pub async fn job_delete(&self, job_id: &str) -> ClientResult<()> {
let _: () = self.client
.request("job.delete", rpc_params![job_id])
.await.map_err(|e| ClientError::JsonRpc(e))?;
Ok(())
}
/// Get supervisor information including secret counts
pub async fn get_supervisor_info(&self) -> ClientResult<SupervisorInfo> {
let info: SupervisorInfo = self
.client
.request("supervisor.info", rpc_params![])
.await.map_err(|e| ClientError::JsonRpc(e))?;
Ok(info)
}
/// Get a job by ID
pub async fn job_get(&self, job_id: &str) -> ClientResult<Job> {
let job: Job = self
.client
.request("job.get", rpc_params![job_id])
.await.map_err(|e| ClientError::JsonRpc(e))?;
Ok(job)
}
// ========== Auth/API Key Methods ==========
/// Verify the current API key
pub async fn auth_verify(&self) -> ClientResult<AuthVerifyResponse> {
let response: AuthVerifyResponse = self
.client
.request("auth.verify", rpc_params![])
.await.map_err(|e| ClientError::JsonRpc(e))?;
Ok(response)
}
/// Create a new API key (admin only)
pub async fn key_create(&self, key: ApiKey) -> ClientResult<()> {
let _: () = self
.client
.request("key.create", rpc_params![key])
.await.map_err(|e| ClientError::JsonRpc(e))?;
Ok(())
}
/// Generate a new API key with auto-generated key value (admin only)
pub async fn key_generate(&self, params: GenerateApiKeyParams) -> ClientResult<ApiKey> {
let api_key: ApiKey = self
.client
.request("key.generate", rpc_params![params])
.await.map_err(|e| ClientError::JsonRpc(e))?;
Ok(api_key)
}
/// Remove an API key (admin only)
pub async fn key_delete(&self, key_id: String) -> ClientResult<()> {
let _: () = self
.client
.request("key.delete", rpc_params![key_id])
.await.map_err(|e| ClientError::JsonRpc(e))?;
Ok(())
}
/// List all API keys (admin only)
pub async fn key_list(&self) -> ClientResult<Vec<ApiKey>> {
let keys: Vec<ApiKey> = self
.client
.request("key.list", rpc_params![])
.await.map_err(|e| ClientError::JsonRpc(e))?;
Ok(keys)
}
}

View File

@@ -0,0 +1,859 @@
//! WASM-compatible OpenRPC client for Hero Supervisor
//!
//! This module provides a WASM-compatible client library for interacting with the Hero Supervisor
//! OpenRPC server using browser-native fetch APIs.
use wasm_bindgen::prelude::*;
use wasm_bindgen_futures::JsFuture;
use web_sys::{Headers, Request, RequestInit, RequestMode, Response};
use serde_json::json;
use secp256k1::{Message, PublicKey, Secp256k1, SecretKey, ecdsa::Signature};
use sha2::{Sha256, Digest};
use serde::{Deserialize, Serialize};
use thiserror::Error;
use uuid::Uuid;
/// WASM-compatible client for communicating with Hero Supervisor OpenRPC server
/// Requires authentication secret for all operations
#[wasm_bindgen]
#[derive(Clone)]
pub struct WasmSupervisorClient {
server_url: String,
secret: String,
}
/// Error types for WASM client operations
#[derive(Error, Debug)]
pub enum WasmClientError {
#[error("Network error: {0}")]
Network(String),
#[error("Serialization error: {0}")]
Serialization(#[from] serde_json::Error),
#[error("JavaScript error: {0}")]
JavaScript(String),
#[error("Server error: {message}")]
Server { message: String },
#[error("Invalid response format")]
InvalidResponse,
}
/// Result type for WASM client operations
pub type WasmClientResult<T> = Result<T, WasmClientError>;
/// Auth verification response
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AuthVerifyResponse {
pub valid: bool,
pub name: String,
pub scope: String,
}
/// JSON-RPC request structure
#[derive(Serialize)]
struct JsonRpcRequest {
jsonrpc: String,
method: String,
params: serde_json::Value,
id: u32,
}
/// JSON-RPC response structure
#[derive(Deserialize)]
struct JsonRpcResponse {
jsonrpc: String,
#[serde(skip_serializing_if = "Option::is_none")]
result: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
error: Option<JsonRpcError>,
id: u32,
}
/// JSON-RPC error structure
#[derive(Deserialize)]
struct JsonRpcError {
code: i32,
message: String,
#[serde(skip_serializing_if = "Option::is_none")]
data: Option<serde_json::Value>,
}
/// Types of runners supported by the supervisor
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[wasm_bindgen]
pub enum WasmRunnerType {
SALRunner,
OSISRunner,
VRunner,
}
/// Job type enumeration that maps to runner types
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[wasm_bindgen]
pub enum WasmJobType {
SAL,
OSIS,
V,
}
/// Job status enumeration
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum JobStatus {
Pending,
Running,
Finished,
Error,
}
/// Job error type
#[derive(Debug, Clone, thiserror::Error)]
pub enum JobError {
#[error("Validation error: {0}")]
Validation(String),
#[error("Execution error: {0}")]
Execution(String),
#[error("Timeout error")]
Timeout,
}
// Re-export JobBuilder from hero-job for convenience
pub use hero_job::JobBuilder;
#[wasm_bindgen]
impl WasmSupervisorClient {
/// Create a new WASM supervisor client with authentication secret
#[wasm_bindgen(constructor)]
pub fn new(server_url: String, secret: String) -> Self {
console_log::init_with_level(log::Level::Info).ok();
Self {
server_url,
secret,
}
}
/// Alias for new() to maintain backward compatibility
#[wasm_bindgen]
pub fn with_secret(server_url: String, secret: String) -> Self {
Self::new(server_url, secret)
}
/// Get the server URL
#[wasm_bindgen(getter)]
pub fn server_url(&self) -> String {
self.server_url.clone()
}
/// Test connection using OpenRPC discovery method
pub async fn discover(&self) -> Result<JsValue, JsValue> {
let result = self.call_method("rpc.discover", serde_json::Value::Null).await;
match result {
Ok(value) => Ok(wasm_bindgen::JsValue::from_str(&value.to_string())),
Err(e) => Err(JsValue::from_str(&e.to_string())),
}
}
/// Verify an API key and return its metadata as JSON
/// The key is sent via Authorization header (Bearer token)
pub async fn auth_verify(&self, key: String) -> Result<JsValue, JsValue> {
// Create a temporary client with the key to verify
let temp_client = WasmSupervisorClient::with_secret(self.server_url.clone(), key);
// Send empty object as params - the key is in the Authorization header
let params = serde_json::json!({});
match temp_client.call_method("auth.verify", params).await {
Ok(result) => {
// Parse to AuthVerifyResponse to validate, then convert to JsValue
let auth_response: AuthVerifyResponse = serde_json::from_value(result)
.map_err(|e| JsValue::from_str(&format!("Failed to parse auth response: {}", e)))?;
// Convert to JsValue
serde_wasm_bindgen::to_value(&auth_response)
.map_err(|e| JsValue::from_str(&format!("Failed to convert to JsValue: {}", e)))
}
Err(e) => Err(JsValue::from_str(&format!("Failed to verify auth: {}", e))),
}
}
/// Verify the client's stored API key
/// Uses the secret that was set when creating the client
pub async fn auth_verify_self(&self) -> Result<JsValue, JsValue> {
self.auth_verify(self.secret.clone()).await
}
/// Create a new API key (admin only)
/// Returns the created API key with its key string
pub async fn auth_create_key(&self, name: String, scope: String) -> Result<JsValue, JsValue> {
let params = serde_json::json!({
"name": name,
"scope": scope
});
match self.call_method("auth.create_key", params).await {
Ok(result) => Ok(serde_wasm_bindgen::to_value(&result)
.map_err(|e| JsValue::from_str(&format!("Failed to convert result: {}", e)))?),
Err(e) => Err(JsValue::from_str(&format!("Failed to create key: {}", e))),
}
}
/// List all API keys (admin only)
pub async fn auth_list_keys(&self) -> Result<JsValue, JsValue> {
match self.call_method("auth.list_keys", serde_json::Value::Null).await {
Ok(result) => Ok(serde_wasm_bindgen::to_value(&result)
.map_err(|e| JsValue::from_str(&format!("Failed to convert result: {}", e)))?),
Err(e) => Err(JsValue::from_str(&format!("Failed to list keys: {}", e))),
}
}
/// Remove an API key (admin only)
pub async fn auth_remove_key(&self, key: String) -> Result<bool, JsValue> {
let params = serde_json::json!({
"key": key
});
match self.call_method("auth.remove_key", params).await {
Ok(result) => {
if let Some(success) = result.as_bool() {
Ok(success)
} else {
Err(JsValue::from_str("Invalid response format: expected boolean"))
}
},
Err(e) => Err(JsValue::from_str(&format!("Failed to remove key: {}", e))),
}
}
/// Register a new runner to the supervisor
/// The queue name is automatically set to match the runner name
/// Authentication uses the secret from Authorization header (set during client creation)
pub async fn register_runner(&self, name: String) -> Result<String, JsValue> {
// Secret is sent via Authorization header, not in params
let params = serde_json::json!({
"name": name
});
match self.call_method("register_runner", params).await {
Ok(result) => {
// Extract the runner name from the result
if let Some(runner) = result.as_str() {
Ok(runner.to_string())
} else {
Err(JsValue::from_str("Invalid response format: expected runner name"))
}
},
Err(e) => Err(JsValue::from_str(&format!("Failed to register runner: {}", e))),
}
}
/// Create a job (fire-and-forget, non-blocking) - DEPRECATED: Use create_job with API key auth
#[wasm_bindgen]
pub async fn create_job_with_secret(&self, secret: String, job: hero_job::Job) -> Result<String, JsValue> {
// Backend expects RunJobParams struct with secret and job fields - wrap in array like register_runner
let params = serde_json::json!([{
"secret": secret,
"job": {
"id": job.id,
"caller_id": job.caller_id,
"context_id": job.context_id,
"payload": job.payload,
"runner": job.runner,
"executor": job.executor,
"timeout": job.timeout,
"env_vars": serde_json::from_str::<serde_json::Value>(&serde_json::to_string(&job.env_vars).unwrap_or_else(|_| "{}".to_string())).unwrap_or(serde_json::json!({})),
"created_at": job.created_at,
"updated_at": job.updated_at
}
}]);
match self.call_method("create_job", params).await {
Ok(result) => {
if let Some(job_id) = result.as_str() {
Ok(job_id.to_string())
} else {
Ok(result.to_string())
}
}
Err(e) => Err(JsValue::from_str(&format!("Failed to create job: {:?}", e)))
}
}
/// Run a job on a specific runner (blocking, returns result)
#[wasm_bindgen]
pub async fn run_job(&self, secret: String, job: hero_job::Job) -> Result<String, JsValue> {
// Backend expects RunJobParams struct with secret and job fields - wrap in array like register_runner
let params = serde_json::json!([{
"secret": secret,
"job": {
"id": job.id,
"caller_id": job.caller_id,
"context_id": job.context_id,
"payload": job.payload,
"runner": job.runner,
"executor": job.executor,
"timeout": job.timeout,
"env_vars": serde_json::from_str::<serde_json::Value>(&serde_json::to_string(&job.env_vars).unwrap_or_else(|_| "{}".to_string())).unwrap_or(serde_json::json!({})),
"created_at": job.created_at,
"updated_at": job.updated_at
}
}]);
match self.call_method("job.run", params).await {
Ok(result) => {
if let Some(result_str) = result.as_str() {
Ok(result_str.to_string())
} else {
Ok(result.to_string())
}
},
Err(e) => Err(JsValue::from_str(&e.to_string())),
}
}
/// List all runner IDs
pub async fn list_runners(&self) -> Result<Vec<String>, JsValue> {
match self.call_method("list_runners", serde_json::Value::Null).await {
Ok(result) => {
if let Ok(runners) = serde_json::from_value::<Vec<String>>(result) {
Ok(runners)
} else {
Err(JsValue::from_str("Failed to parse runners list"))
}
},
Err(e) => Err(JsValue::from_str(&format!("Failed to list runners: {}", e)))
}
}
/// Get status of all runners
pub async fn get_all_runner_status(&self) -> Result<JsValue, JsValue> {
match self.call_method("get_all_runner_status", serde_json::Value::Null).await {
Ok(result) => {
// Convert serde_json::Value to JsValue
Ok(serde_wasm_bindgen::to_value(&result)
.map_err(|e| JsValue::from_str(&format!("Failed to convert result: {}", e)))?)
},
Err(e) => Err(JsValue::from_str(&format!("Failed to get runner statuses: {}", e)))
}
}
/// Create a job from a JsValue (full Job object)
pub async fn create_job(&self, job: JsValue) -> Result<String, JsValue> {
// Convert JsValue to serde_json::Value
let job_value: serde_json::Value = serde_wasm_bindgen::from_value(job)
.map_err(|e| JsValue::from_str(&format!("Failed to parse job: {}", e)))?;
// Wrap in RunJobParams structure and pass as positional parameter
let params = serde_json::json!([{
"job": job_value
}]);
match self.call_method("jobs.create", params).await {
Ok(result) => {
if let Some(job_id) = result.as_str() {
Ok(job_id.to_string())
} else {
Err(JsValue::from_str("Invalid response format: expected job ID"))
}
},
Err(e) => Err(JsValue::from_str(&format!("Failed to create job: {}", e))),
}
}
/// Create a job with basic parameters (simplified version)
pub async fn create_simple_job(
&self,
runner: String,
caller_id: String,
context_id: String,
payload: String,
executor: String,
) -> Result<String, JsValue> {
// Generate a unique job ID
let job_id = format!("job-{}", uuid::Uuid::new_v4());
let job = serde_json::json!({
"id": job_id,
"runner": runner,
"caller_id": caller_id,
"context_id": context_id,
"payload": payload,
"executor": executor,
"timeout": 30,
"env": {}
});
let params = serde_json::json!({
"job": job
});
match self.call_method("jobs.create", params).await {
Ok(result) => {
if let Some(job_id) = result.as_str() {
Ok(job_id.to_string())
} else {
Err(JsValue::from_str("Invalid response format: expected job ID"))
}
},
Err(e) => Err(JsValue::from_str(&format!("Failed to create job: {}", e))),
}
}
/// List all jobs
pub async fn list_jobs(&self) -> Result<JsValue, JsValue> {
match self.call_method("jobs.list", serde_json::Value::Null).await {
Ok(result) => {
// Convert serde_json::Value to JsValue
serde_wasm_bindgen::to_value(&result)
.map_err(|e| JsValue::from_str(&format!("Failed to convert jobs list: {}", e)))
},
Err(e) => Err(JsValue::from_str(&e.to_string())),
}
}
/// Get a job by job ID
pub async fn get_job(&self, job_id: &str) -> Result<hero_job::Job, JsValue> {
let params = serde_json::json!([job_id]);
match self.call_method("get_job", params).await {
Ok(result) => {
// Convert the Job result to hero_job::Job
if let Ok(job_value) = serde_json::from_value::<serde_json::Value>(result) {
// Extract fields from the job
let id = job_value.get("id").and_then(|v| v.as_str()).unwrap_or("").to_string();
let caller_id = job_value.get("caller_id").and_then(|v| v.as_str()).unwrap_or("").to_string();
let context_id = job_value.get("context_id").and_then(|v| v.as_str()).unwrap_or("").to_string();
let payload = job_value.get("payload").and_then(|v| v.as_str()).unwrap_or("").to_string();
let runner = job_value.get("runner").and_then(|v| v.as_str()).unwrap_or("").to_string();
let executor = job_value.get("executor").and_then(|v| v.as_str()).unwrap_or("").to_string();
let timeout_secs = job_value.get("timeout").and_then(|v| v.get("secs")).and_then(|v| v.as_u64()).unwrap_or(30);
let env_vars = job_value.get("env_vars").map(|v| v.to_string()).unwrap_or_else(|| "{}".to_string());
let created_at = job_value.get("created_at").and_then(|v| v.as_str()).unwrap_or("").to_string();
let updated_at = job_value.get("updated_at").and_then(|v| v.as_str()).unwrap_or("").to_string();
Ok(hero_job::Job {
id,
caller_id,
context_id,
payload,
runner,
executor,
timeout: timeout_secs,
env_vars: serde_json::from_str(&env_vars).unwrap_or_default(),
created_at: chrono::DateTime::parse_from_rfc3339(&created_at)
.map(|dt| dt.with_timezone(&chrono::Utc))
.unwrap_or_else(|_| chrono::Utc::now()),
updated_at: chrono::DateTime::parse_from_rfc3339(&updated_at)
.map(|dt| dt.with_timezone(&chrono::Utc))
.unwrap_or_else(|_| chrono::Utc::now()),
signatures: Vec::new(),
})
} else {
Err(JsValue::from_str("Invalid response format for get_job"))
}
},
Err(e) => Err(JsValue::from_str(&e.to_string())),
}
}
/// Ping a runner by dispatching a ping job to its queue
#[wasm_bindgen]
pub async fn ping_runner(&self, runner_id: &str) -> Result<String, JsValue> {
let params = serde_json::json!([runner_id]);
match self.call_method("ping_runner", params).await {
Ok(result) => {
if let Some(job_id) = result.as_str() {
Ok(job_id.to_string())
} else {
Ok(result.to_string())
}
}
Err(e) => Err(JsValue::from_str(&format!("Failed to ping runner: {:?}", e)))
}
}
/// Stop a job by ID
#[wasm_bindgen]
pub async fn stop_job(&self, job_id: &str) -> Result<(), JsValue> {
let params = serde_json::json!([job_id]);
match self.call_method("stop_job", params).await {
Ok(_) => Ok(()),
Err(e) => Err(JsValue::from_str(&format!("Failed to stop job: {:?}", e)))
}
}
/// Delete a job by ID
#[wasm_bindgen]
pub async fn delete_job(&self, job_id: &str) -> Result<(), JsValue> {
let params = serde_json::json!([{
"job_id": job_id
}]);
match self.call_method("job.delete", params).await {
Ok(_) => Ok(()),
Err(e) => Err(JsValue::from_str(&format!("Failed to delete job: {:?}", e)))
}
}
/// Get logs for a specific job
#[wasm_bindgen]
pub async fn get_job_logs(&self, job_id: &str, lines: Option<usize>) -> Result<JsValue, JsValue> {
let params = if let Some(n) = lines {
serde_json::json!([job_id, n])
} else {
serde_json::json!([job_id, serde_json::Value::Null])
};
match self.call_method("get_job_logs", params).await {
Ok(result) => {
// Convert Vec<String> to JsValue
Ok(serde_wasm_bindgen::to_value(&result)
.map_err(|e| JsValue::from_str(&format!("Failed to convert logs: {}", e)))?)
},
Err(e) => Err(JsValue::from_str(&format!("Failed to get job logs: {:?}", e)))
}
}
/// Remove a runner from the supervisor
pub async fn remove_runner(&self, actor_id: &str) -> Result<(), JsValue> {
let params = serde_json::json!([actor_id]);
match self.call_method("remove_runner", params).await {
Ok(_) => Ok(()),
Err(e) => Err(JsValue::from_str(&e.to_string())),
}
}
/// Start a specific runner
pub async fn start_runner(&self, actor_id: &str) -> Result<(), JsValue> {
let params = serde_json::json!([actor_id]);
match self.call_method("start_runner", params).await {
Ok(_) => Ok(()),
Err(e) => Err(JsValue::from_str(&e.to_string())),
}
}
/// Stop a specific runner
pub async fn stop_runner(&self, actor_id: &str, force: bool) -> Result<(), JsValue> {
let params = serde_json::json!([actor_id, force]);
self.call_method("stop_runner", params)
.await
.map_err(|e| JsValue::from_str(&e.to_string()))?;
Ok(())
}
/// Get a specific runner by ID
pub async fn get_runner(&self, actor_id: &str) -> Result<JsValue, JsValue> {
let params = serde_json::json!([actor_id]);
let result = self.call_method("get_runner", params)
.await
.map_err(|e| JsValue::from_str(&e.to_string()))?;
// Convert the serde_json::Value to a JsValue via string serialization
let json_string = serde_json::to_string(&result)
.map_err(|e| JsValue::from_str(&e.to_string()))?;
Ok(js_sys::JSON::parse(&json_string)
.map_err(|e| JsValue::from_str("Failed to parse JSON"))?)
}
/// Add a secret to the supervisor
pub async fn add_secret(&self, admin_secret: &str, secret_type: &str, secret_value: &str) -> Result<(), JsValue> {
let params = serde_json::json!([{
"admin_secret": admin_secret,
"secret_type": secret_type,
"secret_value": secret_value
}]);
match self.call_method("add_secret", params).await {
Ok(_) => Ok(()),
Err(e) => Err(JsValue::from_str(&e.to_string())),
}
}
/// Remove a secret from the supervisor
pub async fn remove_secret(&self, admin_secret: &str, secret_type: &str, secret_value: &str) -> Result<(), JsValue> {
let params = serde_json::json!([{
"admin_secret": admin_secret,
"secret_type": secret_type,
"secret_value": secret_value
}]);
match self.call_method("remove_secret", params).await {
Ok(_) => Ok(()),
Err(e) => Err(JsValue::from_str(&e.to_string())),
}
}
/// List secrets (returns supervisor info including secret counts)
pub async fn list_secrets(&self, admin_secret: &str) -> Result<JsValue, JsValue> {
let params = serde_json::json!([{
"admin_secret": admin_secret
}]);
match self.call_method("list_secrets", params).await {
Ok(result) => {
// Convert serde_json::Value to JsValue
let result_str = serde_json::to_string(&result)
.map_err(|e| JsValue::from_str(&e.to_string()))?;
Ok(js_sys::JSON::parse(&result_str)
.map_err(|e| JsValue::from_str(&format!("JSON parse error: {:?}", e)))?)
},
Err(e) => Err(JsValue::from_str(&e.to_string())),
}
}
/// Get supervisor information including secret counts
pub async fn get_supervisor_info(&self, admin_secret: &str) -> Result<JsValue, JsValue> {
let params = serde_json::json!({
"admin_secret": admin_secret
});
match self.call_method("get_supervisor_info", params).await {
Ok(result) => {
let result_str = serde_json::to_string(&result)
.map_err(|e| JsValue::from_str(&format!("Serialization error: {:?}", e)))?;
Ok(js_sys::JSON::parse(&result_str)
.map_err(|e| JsValue::from_str(&format!("JSON parse error: {:?}", e)))?)
},
Err(e) => Err(JsValue::from_str(&format!("Failed to get supervisor info: {:?}", e))),
}
}
/// List admin secrets (returns actual secret values)
pub async fn list_admin_secrets(&self, admin_secret: &str) -> Result<Vec<String>, JsValue> {
let params = serde_json::json!({
"admin_secret": admin_secret
});
match self.call_method("list_admin_secrets", params).await {
Ok(result) => {
let secrets: Vec<String> = serde_json::from_value(result)
.map_err(|e| JsValue::from_str(&format!("Failed to parse admin secrets: {:?}", e)))?;
Ok(secrets)
},
Err(e) => Err(JsValue::from_str(&format!("Failed to list admin secrets: {:?}", e))),
}
}
/// List user secrets (returns actual secret values)
pub async fn list_user_secrets(&self, admin_secret: &str) -> Result<Vec<String>, JsValue> {
let params = serde_json::json!({
"admin_secret": admin_secret
});
match self.call_method("list_user_secrets", params).await {
Ok(result) => {
let secrets: Vec<String> = serde_json::from_value(result)
.map_err(|e| JsValue::from_str(&format!("Failed to parse user secrets: {:?}", e)))?;
Ok(secrets)
},
Err(e) => Err(JsValue::from_str(&format!("Failed to list user secrets: {:?}", e))),
}
}
/// List register secrets (returns actual secret values)
pub async fn list_register_secrets(&self, admin_secret: &str) -> Result<Vec<String>, JsValue> {
let params = serde_json::json!({
"admin_secret": admin_secret
});
match self.call_method("list_register_secrets", params).await {
Ok(result) => {
let secrets: Vec<String> = serde_json::from_value(result)
.map_err(|e| JsValue::from_str(&format!("Failed to parse register secrets: {:?}", e)))?;
Ok(secrets)
},
Err(e) => Err(JsValue::from_str(&format!("Failed to list register secrets: {:?}", e))),
}
}
/// Start a previously created job by queuing it to its assigned runner
pub async fn start_job(&self, job_id: &str) -> Result<(), JsValue> {
let params = serde_json::json!([{
"job_id": job_id
}]);
match self.call_method("job.start", params).await {
Ok(_) => Ok(()),
Err(e) => Err(JsValue::from_str(&e.to_string())),
}
}
/// Get the status of a job
pub async fn get_job_status(&self, job_id: &str) -> Result<JsValue, JsValue> {
let params = serde_json::json!([job_id]);
match self.call_method("job.status", params).await {
Ok(result) => serde_wasm_bindgen::to_value(&result)
.map_err(|e| JsValue::from_str(&format!("Serialization error: {:?}", e))),
Err(e) => Err(JsValue::from_str(&e.to_string())),
}
}
/// Get the result of a completed job
pub async fn get_job_result(&self, job_id: &str) -> Result<JsValue, JsValue> {
let params = serde_json::json!([job_id]);
match self.call_method("job.result", params).await {
Ok(result) => serde_wasm_bindgen::to_value(&result)
.map_err(|e| JsValue::from_str(&format!("Serialization error: {:?}", e))),
Err(e) => Err(JsValue::from_str(&e.to_string())),
}
}
/// Internal method to make JSON-RPC calls
async fn call_method(&self, method: &str, params: serde_json::Value) -> WasmClientResult<serde_json::Value> {
let request = JsonRpcRequest {
jsonrpc: "2.0".to_string(),
method: method.to_string(),
params,
id: 1,
};
let body = serde_json::to_string(&request)?;
// Create headers
let headers = Headers::new().map_err(|e| WasmClientError::JavaScript(format!("{:?}", e)))?;
headers.set("Content-Type", "application/json")
.map_err(|e| WasmClientError::JavaScript(format!("{:?}", e)))?;
// Add Authorization header with secret
let auth_value = format!("Bearer {}", self.secret);
headers.set("Authorization", &auth_value)
.map_err(|e| WasmClientError::JavaScript(format!("{:?}", e)))?;
// Create request init
let opts = RequestInit::new();
opts.set_method("POST");
opts.set_headers(&headers);
opts.set_body(&JsValue::from_str(&body));
opts.set_mode(RequestMode::Cors);
// Create request
let request = Request::new_with_str_and_init(&self.server_url, &opts)
.map_err(|e| WasmClientError::JavaScript(format!("{:?}", e)))?;
// Get window and fetch
let window = web_sys::window().ok_or_else(|| WasmClientError::JavaScript("No window object".to_string()))?;
let resp_value = JsFuture::from(window.fetch_with_request(&request)).await
.map_err(|e| WasmClientError::Network(format!("{:?}", e)))?;
// Convert to Response
let resp: Response = resp_value.dyn_into()
.map_err(|e| WasmClientError::JavaScript(format!("{:?}", e)))?;
// Check if response is ok
if !resp.ok() {
return Err(WasmClientError::Network(format!("HTTP {}: {}", resp.status(), resp.status_text())));
}
// Get response text
let text_promise = resp.text()
.map_err(|e| WasmClientError::JavaScript(format!("{:?}", e)))?;
let text_value = JsFuture::from(text_promise).await
.map_err(|e| WasmClientError::Network(format!("{:?}", e)))?;
let text = text_value.as_string()
.ok_or_else(|| WasmClientError::InvalidResponse)?;
// Parse JSON-RPC response
let response: JsonRpcResponse = serde_json::from_str(&text)?;
if let Some(error) = response.error {
return Err(WasmClientError::Server {
message: format!("{}: {}", error.code, error.message),
});
}
// For void methods, null result is valid
Ok(response.result.unwrap_or(serde_json::Value::Null))
}
}
/// Initialize the WASM client library (call manually if needed)
pub fn init() {
console_log::init_with_level(log::Level::Info).ok();
log::info!("Hero Supervisor WASM OpenRPC Client initialized");
}
/// Utility function to create a client from JavaScript
#[wasm_bindgen]
pub fn create_client(server_url: String, secret: String) -> WasmSupervisorClient {
WasmSupervisorClient::new(server_url, secret)
}
/// Sign a job's canonical representation with a private key
/// Returns a tuple of (public_key_hex, signature_hex)
#[wasm_bindgen]
pub fn sign_job_canonical(
canonical_repr: String,
private_key_hex: String,
) -> Result<JsValue, JsValue> {
// Decode private key from hex
let secret_bytes = hex::decode(&private_key_hex)
.map_err(|e| JsValue::from_str(&format!("Invalid private key hex: {}", e)))?;
let secret_key = SecretKey::from_slice(&secret_bytes)
.map_err(|e| JsValue::from_str(&format!("Invalid private key: {}", e)))?;
// Get the public key (uncompressed format)
let secp = Secp256k1::new();
let public_key = PublicKey::from_secret_key(&secp, &secret_key);
let public_key_hex = hex::encode(public_key.serialize_uncompressed());
// Hash the canonical representation
let mut hasher = Sha256::new();
hasher.update(canonical_repr.as_bytes());
let hash = hasher.finalize();
// Create message from hash
let message = Message::from_digest_slice(&hash)
.map_err(|e| JsValue::from_str(&format!("Invalid message: {}", e)))?;
// Sign the message
let signature = secp.sign_ecdsa(&message, &secret_key);
let signature_hex = hex::encode(signature.serialize_compact());
// Return as JS object
let result = serde_json::json!({
"public_key": public_key_hex,
"signature": signature_hex
});
serde_wasm_bindgen::to_value(&result)
.map_err(|e| JsValue::from_str(&format!("Failed to serialize result: {}", e)))
}
/// Create canonical representation of a job for signing
/// This matches the format used in runner_rust Job::canonical_representation
#[wasm_bindgen]
pub fn create_job_canonical_repr(
id: String,
caller_id: String,
context_id: String,
payload: String,
runner: String,
executor: String,
timeout: u64,
env_vars_json: String,
) -> Result<String, JsValue> {
// Parse env_vars from JSON
let env_vars: std::collections::HashMap<String, String> = serde_json::from_str(&env_vars_json)
.map_err(|e| JsValue::from_str(&format!("Invalid env_vars JSON: {}", e)))?;
// Sort env_vars keys for deterministic ordering
let mut env_vars_sorted: Vec<_> = env_vars.iter().collect();
env_vars_sorted.sort_by_key(|&(k, _)| k);
// Create canonical representation (matches Job::canonical_representation in runner_rust)
let canonical = format!(
"{}:{}:{}:{}:{}:{}:{}:{:?}",
id,
caller_id,
context_id,
payload,
runner,
executor,
timeout,
env_vars_sorted
);
Ok(canonical)
}

24
lib/models/job/Cargo.toml Normal file
View File

@@ -0,0 +1,24 @@
[package]
name = "hero-job"
version.workspace = true
edition.workspace = true
description = "Job types and models for Hero"
license = "MIT OR Apache-2.0"
[lib]
name = "hero_job"
path = "lib.rs"
[dependencies]
serde.workspace = true
serde_json.workspace = true
thiserror.workspace = true
chrono.workspace = true
uuid.workspace = true
log.workspace = true
hex.workspace = true
sha2.workspace = true
secp256k1.workspace = true
[target.'cfg(target_arch = "wasm32")'.dependencies]
wasm-bindgen.workspace = true

338
lib/models/job/lib.rs Normal file
View File

@@ -0,0 +1,338 @@
use chrono::{Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use thiserror::Error;
use uuid::Uuid;
use log::{error};
#[cfg(target_arch = "wasm32")]
use wasm_bindgen::prelude::*;
/// Signature for a job - contains the signatory's public key and their signature
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JobSignature {
/// Public key of the signatory (hex-encoded secp256k1 public key)
pub public_key: String,
/// Signature (hex-encoded secp256k1 signature)
pub signature: String,
}
/// Job status enumeration
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum JobStatus {
Created,
Dispatched,
WaitingForPrerequisites,
Started,
Error,
Stopping,
Finished,
}
/// Job result response for RPC calls
#[derive(Debug, Serialize, Clone)]
#[serde(untagged)]
pub enum JobResult {
Success { success: String },
Error { error: String },
}
impl JobStatus {
pub fn as_str(&self) -> &'static str {
match self {
JobStatus::Created => "created",
JobStatus::Dispatched => "dispatched",
JobStatus::WaitingForPrerequisites => "waiting_for_prerequisites",
JobStatus::Started => "started",
JobStatus::Error => "error",
JobStatus::Stopping => "stopping",
JobStatus::Finished => "finished",
}
}
pub fn from_str(s: &str) -> Option<Self> {
match s {
"created" => Some(JobStatus::Created),
"dispatched" => Some(JobStatus::Dispatched),
"waiting_for_prerequisites" => Some(JobStatus::WaitingForPrerequisites),
"started" => Some(JobStatus::Started),
"error" => Some(JobStatus::Error),
"stopping" => Some(JobStatus::Stopping),
"finished" => Some(JobStatus::Finished),
_ => None,
}
}
}
/// Job structure representing a unit of work to be executed
#[cfg_attr(target_arch = "wasm32", wasm_bindgen(getter_with_clone))]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Job {
pub id: String,
pub caller_id: String,
pub context_id: String,
pub payload: String,
pub runner: String, // name of the runner to execute this job
pub executor: String, // name of the executor the runner will use to execute this job
pub timeout: u64, // timeout in seconds
#[cfg_attr(target_arch = "wasm32", wasm_bindgen(skip))]
pub env_vars: HashMap<String, String>, // environment variables for script execution
#[cfg_attr(target_arch = "wasm32", wasm_bindgen(skip))]
pub created_at: chrono::DateTime<chrono::Utc>,
#[cfg_attr(target_arch = "wasm32", wasm_bindgen(skip))]
pub updated_at: chrono::DateTime<chrono::Utc>,
/// Signatures from authorized signatories (public keys are included in each signature)
#[cfg_attr(target_arch = "wasm32", wasm_bindgen(skip))]
pub signatures: Vec<JobSignature>,
}
/// Error types for job operations
#[derive(Error, Debug)]
pub enum JobError {
#[error("Serialization error: {0}")]
Serialization(#[from] serde_json::Error),
#[error("Job not found: {0}")]
NotFound(String),
#[error("Invalid data: {0}")]
InvalidData(String),
#[error("Validation error: {0}")]
Validation(String),
#[error("Signature verification failed: {0}")]
SignatureVerification(String),
}
impl Job {
/// Create a new job with the given parameters
pub fn new(
caller_id: String,
context_id: String,
payload: String,
runner: String,
executor: String,
) -> Self {
let now = Utc::now();
Self {
id: Uuid::new_v4().to_string(),
caller_id,
context_id,
payload,
runner,
executor,
timeout: 300, // 5 minutes default
env_vars: HashMap::new(),
created_at: now,
updated_at: now,
signatures: Vec::new(),
}
}
/// Get the canonical representation of the job for signing
/// This creates a deterministic string representation that can be hashed and signed
/// Note: Signatures are excluded from the canonical representation
pub fn canonical_representation(&self) -> String {
// Create a deterministic representation excluding signatures
// Sort env_vars keys for deterministic ordering
let mut env_vars_sorted: Vec<_> = self.env_vars.iter().collect();
env_vars_sorted.sort_by_key(|&(k, _)| k);
format!(
"{}:{}:{}:{}:{}:{}:{}:{:?}",
self.id,
self.caller_id,
self.context_id,
self.payload,
self.runner,
self.executor,
self.timeout,
env_vars_sorted
)
}
/// Get list of signatory public keys from signatures
pub fn signatories(&self) -> Vec<String> {
self.signatures.iter()
.map(|sig| sig.public_key.clone())
.collect()
}
/// Verify that all signatures are valid
/// Returns Ok(()) if verification passes, Err otherwise
/// Empty signatures list is allowed - loop simply won't execute
pub fn verify_signatures(&self) -> Result<(), JobError> {
use secp256k1::{Message, PublicKey, Secp256k1, ecdsa::Signature};
use sha2::{Sha256, Digest};
// Get the canonical representation and hash it
let canonical = self.canonical_representation();
let mut hasher = Sha256::new();
hasher.update(canonical.as_bytes());
let hash = hasher.finalize();
let secp = Secp256k1::verification_only();
let message = Message::from_digest_slice(&hash)
.map_err(|e| JobError::SignatureVerification(format!("Invalid message: {}", e)))?;
// Verify each signature (if any)
for sig_data in &self.signatures {
// Decode public key
let pubkey_bytes = hex::decode(&sig_data.public_key)
.map_err(|e| JobError::SignatureVerification(format!("Invalid public key hex: {}", e)))?;
let pubkey = PublicKey::from_slice(&pubkey_bytes)
.map_err(|e| JobError::SignatureVerification(format!("Invalid public key: {}", e)))?;
// Decode signature
let sig_bytes = hex::decode(&sig_data.signature)
.map_err(|e| JobError::SignatureVerification(format!("Invalid signature hex: {}", e)))?;
let signature = Signature::from_compact(&sig_bytes)
.map_err(|e| JobError::SignatureVerification(format!("Invalid signature: {}", e)))?;
// Verify signature
secp.verify_ecdsa(&message, &signature, &pubkey)
.map_err(|e| JobError::SignatureVerification(format!("Signature verification failed: {}", e)))?;
}
Ok(())
}
}
/// Builder for constructing job execution requests.
pub struct JobBuilder {
caller_id: String,
context_id: String,
payload: String,
runner: String,
executor: String,
timeout: u64, // timeout in seconds
env_vars: HashMap<String, String>,
signatures: Vec<JobSignature>,
}
impl JobBuilder {
pub fn new() -> Self {
Self {
caller_id: "".to_string(),
context_id: "".to_string(),
payload: "".to_string(),
runner: "".to_string(),
executor: "".to_string(),
timeout: 300, // 5 minutes default
env_vars: HashMap::new(),
signatures: Vec::new(),
}
}
/// Set the caller ID for this job
pub fn caller_id(mut self, caller_id: &str) -> Self {
self.caller_id = caller_id.to_string();
self
}
/// Set the context ID for this job
pub fn context_id(mut self, context_id: &str) -> Self {
self.context_id = context_id.to_string();
self
}
/// Set the payload (script content) for this job
pub fn payload(mut self, payload: &str) -> Self {
self.payload = payload.to_string();
self
}
/// Set the runner name for this job
pub fn runner(mut self, runner: &str) -> Self {
self.runner = runner.to_string();
self
}
/// Set the executor for this job
pub fn executor(mut self, executor: &str) -> Self {
self.executor = executor.to_string();
self
}
/// Set the timeout for job execution (in seconds)
pub fn timeout(mut self, timeout: u64) -> Self {
self.timeout = timeout;
self
}
/// Set a single environment variable
pub fn env_var(mut self, key: &str, value: &str) -> Self {
self.env_vars.insert(key.to_string(), value.to_string());
self
}
/// Set multiple environment variables from a HashMap
pub fn env_vars(mut self, env_vars: HashMap<String, String>) -> Self {
self.env_vars = env_vars;
self
}
/// Clear all environment variables
pub fn clear_env_vars(mut self) -> Self {
self.env_vars.clear();
self
}
/// Add a signature (public key and signature)
pub fn signature(mut self, public_key: &str, signature: &str) -> Self {
self.signatures.push(JobSignature {
public_key: public_key.to_string(),
signature: signature.to_string(),
});
self
}
/// Set multiple signatures
pub fn signatures(mut self, signatures: Vec<JobSignature>) -> Self {
self.signatures = signatures;
self
}
/// Clear all signatures
pub fn clear_signatures(mut self) -> Self {
self.signatures.clear();
self
}
/// Build the job
pub fn build(self) -> Result<Job, JobError> {
if self.caller_id.is_empty() {
return Err(JobError::InvalidData("caller_id is required".to_string()));
}
if self.context_id.is_empty() {
return Err(JobError::InvalidData("context_id is required".to_string()));
}
if self.payload.is_empty() {
return Err(JobError::InvalidData("payload is required".to_string()));
}
if self.runner.is_empty() {
return Err(JobError::InvalidData("runner is required".to_string()));
}
if self.executor.is_empty() {
return Err(JobError::InvalidData("executor is required".to_string()));
}
let mut job = Job::new(
self.caller_id,
self.context_id,
self.payload,
self.runner,
self.executor,
);
job.timeout = self.timeout;
job.env_vars = self.env_vars;
job.signatures = self.signatures;
Ok(job)
}
}
impl Default for JobBuilder {
fn default() -> Self {
Self::new()
}
}

View File

@@ -0,0 +1,46 @@
[package]
name = "osiris-core"
version.workspace = true
edition = "2021"
description = "Osiris core - Object storage and indexing system"
license = "MIT OR Apache-2.0"
[lib]
name = "osiris"
path = "lib.rs"
[dependencies]
# Core dependencies
anyhow.workspace = true
redis.workspace = true
serde.workspace = true
serde_json.workspace = true
tokio.workspace = true
uuid.workspace = true
toml.workspace = true
thiserror.workspace = true
clap.workspace = true
env_logger.workspace = true
log.workspace = true
# Time handling
time = { version = "0.3", features = ["serde", "formatting", "parsing", "macros"] }
# Tracing
tracing.workspace = true
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
# Email
lettre = "0.11"
# HTTP client
reqwest = { version = "0.11", features = ["json"] }
# Rhai scripting
rhai = { version = "1.21.0", features = ["std", "sync", "serde"] }
# Osiris derive macros
osiris-derive = { path = "../derive" }
[dev-dependencies]
tempfile = "3.8"

View File

@@ -0,0 +1,60 @@
pub mod model;
pub use model::{Config, HeroDbConfig, NamespaceConfig};
use crate::error::{Error, Result};
use std::collections::HashMap;
use std::fs;
use std::path::PathBuf;
/// Load configuration from file
pub fn load_config(path: Option<PathBuf>) -> Result<Config> {
let config_path = path.unwrap_or_else(default_config_path);
if !config_path.exists() {
return Err(Error::Config(format!(
"Configuration file not found: {}",
config_path.display()
)));
}
let content = fs::read_to_string(&config_path)?;
let config: Config = toml::from_str(&content)
.map_err(|e| Error::Config(format!("Failed to parse config: {}", e)))?;
Ok(config)
}
/// Save configuration to file
pub fn save_config(config: &Config, path: Option<PathBuf>) -> Result<()> {
let config_path = path.unwrap_or_else(default_config_path);
// Create parent directory if it doesn't exist
if let Some(parent) = config_path.parent() {
fs::create_dir_all(parent)?;
}
let content = toml::to_string_pretty(config)
.map_err(|e| Error::Config(format!("Failed to serialize config: {}", e)))?;
fs::write(&config_path, content)?;
Ok(())
}
/// Get the default configuration file path
pub fn default_config_path() -> PathBuf {
let home = std::env::var("HOME").unwrap_or_else(|_| ".".to_string());
PathBuf::from(home)
.join(".config")
.join("osiris")
.join("config.toml")
}
/// Create a default configuration
pub fn create_default_config(herodb_url: String) -> Config {
Config {
herodb: HeroDbConfig { url: herodb_url },
namespaces: HashMap::new(),
}
}

View File

@@ -0,0 +1,55 @@
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
/// OSIRIS configuration
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Config {
/// HeroDB connection configuration
pub herodb: HeroDbConfig,
/// Namespace configurations
#[serde(default)]
pub namespaces: HashMap<String, NamespaceConfig>,
}
/// HeroDB connection configuration
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct HeroDbConfig {
/// HeroDB URL (e.g., "redis://localhost:6379")
pub url: String,
}
/// Namespace configuration
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct NamespaceConfig {
/// HeroDB database ID for this namespace
pub db_id: u16,
}
impl Config {
/// Get namespace configuration by name
pub fn get_namespace(&self, name: &str) -> Option<&NamespaceConfig> {
self.namespaces.get(name)
}
/// Add or update a namespace
pub fn set_namespace(&mut self, name: String, config: NamespaceConfig) {
self.namespaces.insert(name, config);
}
/// Remove a namespace
pub fn remove_namespace(&mut self, name: &str) -> Option<NamespaceConfig> {
self.namespaces.remove(name)
}
/// Get the next available database ID
pub fn next_db_id(&self) -> u16 {
let max_id = self
.namespaces
.values()
.map(|ns| ns.db_id)
.max()
.unwrap_or(0);
max_id + 1
}
}

413
lib/osiris/core/context.rs Normal file
View File

@@ -0,0 +1,413 @@
/// OSIRIS Context
///
/// A complete context with HeroDB storage and participant-based access.
/// Each context is isolated with its own HeroDB connection.
///
/// Combines:
/// - HeroDB storage (via GenericStore)
/// - Participant list (public keys)
/// - Generic CRUD operations for any data
use crate::objects::Note;
use crate::objects::heroledger::{
user::User,
group::Group,
money::Account,
dnsrecord::DNSZone,
};
use crate::store::{GenericStore, HeroDbClient};
use rhai::{CustomType, EvalAltResult, TypeBuilder};
use std::sync::Arc;
/// Convert serde_json::Value to rhai::Dynamic
fn json_to_rhai(value: serde_json::Value) -> Result<rhai::Dynamic, String> {
match value {
serde_json::Value::Null => Ok(rhai::Dynamic::UNIT),
serde_json::Value::Bool(b) => Ok(rhai::Dynamic::from(b)),
serde_json::Value::Number(n) => {
if let Some(i) = n.as_i64() {
Ok(rhai::Dynamic::from(i))
} else if let Some(f) = n.as_f64() {
Ok(rhai::Dynamic::from(f))
} else {
Err("Invalid number".to_string())
}
}
serde_json::Value::String(s) => Ok(rhai::Dynamic::from(s)),
serde_json::Value::Array(arr) => {
let rhai_arr: Result<Vec<rhai::Dynamic>, String> = arr
.into_iter()
.map(json_to_rhai)
.collect();
Ok(rhai::Dynamic::from(rhai_arr?))
}
serde_json::Value::Object(obj) => {
let mut rhai_map = rhai::Map::new();
for (k, v) in obj {
rhai_map.insert(k.into(), json_to_rhai(v)?);
}
Ok(rhai::Dynamic::from(rhai_map))
}
}
}
// ============================================================================
// OsirisContext - Main Context Type
// ============================================================================
/// OSIRIS Context - combines storage with participant-based access
///
/// This is the main context object that provides:
/// - HeroDB storage via GenericStore
/// - Participant list (public keys)
/// - Generic CRUD operations
#[derive(Clone, Debug)]
pub struct OsirisContext {
pub(crate) participants: Vec<String>, // Public keys of all participants in this context
pub(crate) store: Arc<GenericStore>,
}
// Keep OsirisInstance as an alias for backward compatibility
pub type OsirisInstance = OsirisContext;
impl OsirisContext {
/// Create a builder for OsirisContext
pub fn builder() -> OsirisContextBuilder {
OsirisContextBuilder::new()
}
/// Create a new OSIRIS context with minimal config (for backwards compatibility)
pub fn new(name: impl ToString, herodb_url: &str, db_id: u16) -> Result<Self, Box<dyn std::error::Error>> {
OsirisContextBuilder::new()
.name(name)
.herodb_url(herodb_url)
.db_id(db_id)
.build()
}
/// Get the context participants (public keys)
pub fn participants(&self) -> Vec<String> {
self.participants.clone()
}
/// Get the context ID (sorted, comma-separated participant keys)
pub fn context_id(&self) -> String {
let mut sorted = self.participants.clone();
sorted.sort();
sorted.join(",")
}
// ============================================================================
// Generic CRUD Operations
// ============================================================================
// These methods work with any Rhai Dynamic object and store in HeroDB
/// Generic save - saves any Rhai object to HeroDB
///
/// Usage in Rhai:
/// ```rhai
/// let resident = digital_resident()
/// .email("test@example.com")
/// .first_name("John");
/// let id = ctx.save("residents", "resident_123", resident);
/// ```
pub fn save(&self, collection: String, id: String, data: rhai::Dynamic) -> Result<String, Box<EvalAltResult>> {
let store = self.store.clone();
let id_clone = id.clone();
let collection_clone = collection.clone();
// Serialize Rhai object to JSON
let json_content = format!("{:?}", data); // Simple serialization for now
// Save as Note
tokio::task::block_in_place(|| {
tokio::runtime::Handle::current().block_on(async move {
let mut note = Note::new(collection_clone);
// Parse string ID to u32, default to 0 if parsing fails
note.base_data.id = id_clone.parse::<u32>().unwrap_or(0);
note.content = Some(json_content);
store.put(&note).await
.map_err(|e| format!("Failed to save: {}", e))?;
Ok(id_clone)
})
}).map_err(|e: String| e.into())
}
/// Generic get - retrieves data from HeroDB and returns as Rhai object
///
/// Usage in Rhai:
/// ```rhai
/// let resident = ctx.get("residents", "resident_123");
/// print(resident); // Can use the data directly
/// ```
pub fn get(&self, collection: String, id: String) -> Result<rhai::Dynamic, Box<EvalAltResult>> {
let store = self.store.clone();
tokio::task::block_in_place(|| {
tokio::runtime::Handle::current().block_on(async move {
// Get raw JSON from HeroDB (generic)
let json_data = store.get_raw(&collection, &id).await
.map_err(|e| format!("Failed to get from HeroDB: {}", e))?;
// Parse JSON to Rhai Map
let parsed: serde_json::Value = serde_json::from_str(&json_data)
.map_err(|e| format!("Failed to parse JSON: {}", e))?;
// Convert serde_json::Value to rhai::Dynamic
json_to_rhai(parsed)
})
}).map_err(|e: String| e.into())
}
/// Generic delete - checks if exists in HeroDB and deletes
///
/// Usage in Rhai:
/// ```rhai
/// let deleted = ctx.delete("residents", "resident_123");
/// if deleted {
/// print("Deleted successfully");
/// }
/// ```
pub fn delete(&self, collection: String, id: String) -> Result<bool, Box<EvalAltResult>> {
let store = self.store.clone();
tokio::task::block_in_place(|| {
tokio::runtime::Handle::current().block_on(async move {
// Check if exists by trying to get it
match store.get::<Note>(&collection, &id).await {
Ok(note) => {
// Exists, now delete it
store.delete(&note).await
.map_err(|e| format!("Failed to delete from HeroDB: {}", e))
}
Err(_) => {
// Doesn't exist
Ok(false)
}
}
})
}).map_err(|e: String| e.into())
}
/// Check if an object exists in the context
pub fn exists(&self, collection: String, id: String) -> Result<bool, Box<EvalAltResult>> {
let store = self.store.clone();
tokio::task::block_in_place(|| {
tokio::runtime::Handle::current().block_on(async move {
// Check if exists by trying to get it
match store.get::<Note>(&collection, &id).await {
Ok(_) => Ok(true),
Err(_) => Ok(false),
}
})
}).map_err(|e: String| e.into())
}
/// List all IDs in a collection
pub fn list(&self, collection: String) -> Result<Vec<rhai::Dynamic>, Box<EvalAltResult>> {
let store = self.store.clone();
tokio::task::block_in_place(|| {
tokio::runtime::Handle::current().block_on(async move {
store.get_all_ids(&collection).await
.map(|ids| ids.into_iter().map(rhai::Dynamic::from).collect())
.map_err(|e| format!("Failed to list: {}", e))
})
}).map_err(|e: String| e.into())
}
/// Query objects by field value
pub fn query(&self, collection: String, field: String, value: String) -> Result<Vec<rhai::Dynamic>, Box<EvalAltResult>> {
let store = self.store.clone();
tokio::task::block_in_place(|| {
tokio::runtime::Handle::current().block_on(async move {
store.get_ids_by_index(&collection, &field, &value).await
.map(|ids| ids.into_iter().map(rhai::Dynamic::from).collect())
.map_err(|e| format!("Failed to query: {}", e))
})
}).map_err(|e: String| e.into())
}
}
impl OsirisContext {
/// Generic save method for any Storable object
pub fn save_object<T>(&self, object: T) -> Result<String, Box<EvalAltResult>>
where
T: crate::store::Storable + Send + 'static,
{
let store = self.store.clone();
let id = object.base_data().id;
tokio::task::block_in_place(|| {
tokio::runtime::Handle::current().block_on(async move {
store.put(&object).await
.map_err(|e| format!("Failed to save object: {}", e))?;
Ok(id.to_string())
})
}).map_err(|e: String| e.into())
}
}
impl CustomType for OsirisContext {
fn build(mut builder: TypeBuilder<Self>) {
builder
.with_name("OsirisContext")
.with_fn("participants", |ctx: &mut OsirisContext| ctx.participants())
.with_fn("context_id", |ctx: &mut OsirisContext| ctx.context_id())
// Generic CRUD (with collection name)
.with_fn("save", |ctx: &mut OsirisContext, collection: String, id: String, data: rhai::Dynamic| ctx.save(collection, id, data))
// Typed save methods (no collection name needed - Rhai will pick the right one based on type)
.with_fn("save", |ctx: &mut OsirisContext, note: Note| ctx.save_object(note))
.with_fn("save", |ctx: &mut OsirisContext, event: crate::objects::Event| ctx.save_object(event))
.with_fn("save", |ctx: &mut OsirisContext, user: User| ctx.save_object(user))
.with_fn("save", |ctx: &mut OsirisContext, group: Group| ctx.save_object(group))
.with_fn("save", |ctx: &mut OsirisContext, account: Account| ctx.save_object(account))
.with_fn("save", |ctx: &mut OsirisContext, zone: DNSZone| ctx.save_object(zone))
.with_fn("get", |ctx: &mut OsirisContext, collection: String, id: String| ctx.get(collection, id))
.with_fn("delete", |ctx: &mut OsirisContext, collection: String, id: String| ctx.delete(collection, id))
.with_fn("list", |ctx: &mut OsirisContext, collection: String| ctx.list(collection))
.with_fn("query", |ctx: &mut OsirisContext, collection: String, field: String, value: String| ctx.query(collection, field, value));
}
}
// ============================================================================
// OsirisContextBuilder
// ============================================================================
/// Builder for OsirisContext
pub struct OsirisContextBuilder {
participants: Option<Vec<String>>,
herodb_url: Option<String>,
db_id: Option<u16>,
}
impl OsirisContextBuilder {
/// Create a new builder
pub fn new() -> Self {
Self {
participants: None,
herodb_url: None,
db_id: None,
}
}
/// Set the context participants (public keys)
pub fn participants(mut self, participants: Vec<String>) -> Self {
self.participants = Some(participants);
self
}
/// Set a single participant (for backwards compatibility)
pub fn name(mut self, name: impl ToString) -> Self {
self.participants = Some(vec![name.to_string()]);
self
}
/// Set owner (deprecated, use participants instead)
#[deprecated(note = "Use participants() instead")]
pub fn owner(mut self, owner_id: impl ToString) -> Self {
self.participants = Some(vec![owner_id.to_string()]);
self
}
/// Set the HeroDB URL
pub fn herodb_url(mut self, url: impl ToString) -> Self {
self.herodb_url = Some(url.to_string());
self
}
/// Set the HeroDB database ID
pub fn db_id(mut self, db_id: u16) -> Self {
self.db_id = Some(db_id);
self
}
/// Build the OsirisContext
pub fn build(self) -> Result<OsirisContext, Box<dyn std::error::Error>> {
let participants = self.participants.ok_or("Context participants are required")?;
// HeroDB URL and DB ID are now optional - context can work without storage
let herodb_url = self.herodb_url.unwrap_or_else(|| "redis://localhost:6379".to_string());
let db_id = self.db_id.unwrap_or(1);
if participants.is_empty() {
return Err("At least one participant is required".into());
}
// Create HeroDB client
let client = HeroDbClient::new(&herodb_url, db_id)?;
// Create store
let store = GenericStore::new(client);
Ok(OsirisContext {
participants,
store: Arc::new(store),
})
}
}
impl Default for OsirisContextBuilder {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_context_creation() {
let ctx = OsirisContext::new("test_ctx", "redis://localhost:6379", 1);
assert!(ctx.is_ok());
let ctx = ctx.unwrap();
assert_eq!(ctx.participants(), vec!["test_ctx".to_string()]);
assert_eq!(ctx.context_id(), "test_ctx");
}
#[test]
fn test_builder_basic() {
let ctx = OsirisContextBuilder::new()
.participants(vec!["pk1".to_string()])
.herodb_url("redis://localhost:6379")
.db_id(1)
.build();
assert!(ctx.is_ok());
let ctx = ctx.unwrap();
assert_eq!(ctx.participants(), vec!["pk1".to_string()]);
assert_eq!(ctx.context_id(), "pk1");
}
#[test]
fn test_builder_with_multiple_participants() {
let ctx = OsirisContextBuilder::new()
.participants(vec!["pk1".to_string(), "pk2".to_string(), "pk3".to_string()])
.herodb_url("redis://localhost:6379")
.db_id(1)
.build();
assert!(ctx.is_ok());
let ctx = ctx.unwrap();
assert_eq!(ctx.participants().len(), 3);
// Context ID should be sorted
assert_eq!(ctx.context_id(), "pk1,pk2,pk3");
}
#[test]
fn test_builder_missing_participants() {
let ctx = OsirisContextBuilder::new()
.herodb_url("redis://localhost:6379")
.db_id(1)
.build();
assert!(ctx.is_err());
assert!(ctx.unwrap_err().to_string().contains("participants are required"));
}
}

46
lib/osiris/core/error.rs Normal file
View File

@@ -0,0 +1,46 @@
use std::fmt;
#[derive(Debug)]
pub enum Error {
Redis(redis::RedisError),
Serialization(serde_json::Error),
NotFound(String),
InvalidInput(String),
Config(String),
Io(std::io::Error),
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Error::Redis(e) => write!(f, "Redis error: {}", e),
Error::Serialization(e) => write!(f, "Serialization error: {}", e),
Error::NotFound(msg) => write!(f, "Not found: {}", msg),
Error::InvalidInput(msg) => write!(f, "Invalid input: {}", msg),
Error::Config(msg) => write!(f, "Configuration error: {}", msg),
Error::Io(e) => write!(f, "IO error: {}", e),
}
}
}
impl std::error::Error for Error {}
impl From<redis::RedisError> for Error {
fn from(e: redis::RedisError) -> Self {
Error::Redis(e)
}
}
impl From<serde_json::Error> for Error {
fn from(e: serde_json::Error) -> Self {
Error::Serialization(e)
}
}
impl From<std::io::Error> for Error {
fn from(e: std::io::Error) -> Self {
Error::Io(e)
}
}
pub type Result<T> = std::result::Result<T, Error>;

View File

@@ -0,0 +1,140 @@
use crate::error::Result;
use crate::store::{HeroDbClient, OsirisObject};
/// Field indexing for fast filtering by tags and metadata
#[derive(Debug, Clone)]
pub struct FieldIndex {
client: HeroDbClient,
}
impl FieldIndex {
/// Create a new field index
pub fn new(client: HeroDbClient) -> Self {
Self { client }
}
/// Index an object (add to field indexes)
pub async fn index_object(&self, obj: &OsirisObject) -> Result<()> {
// Index tags
for (key, value) in &obj.meta.tags {
let field_key = format!("field:tag:{}={}", key, value);
self.client.sadd(&field_key, &obj.id).await?;
}
// Index MIME type if present
if let Some(mime) = &obj.meta.mime {
let field_key = format!("field:mime:{}", mime);
self.client.sadd(&field_key, &obj.id).await?;
}
// Index title if present (for exact match)
if let Some(title) = &obj.meta.title {
let field_key = format!("field:title:{}", title);
self.client.sadd(&field_key, &obj.id).await?;
}
// Add to scan index for text search
self.client.sadd("scan:index", &obj.id).await?;
Ok(())
}
/// Remove an object from indexes
pub async fn deindex_object(&self, obj: &OsirisObject) -> Result<()> {
// Remove from tag indexes
for (key, value) in &obj.meta.tags {
let field_key = format!("field:tag:{}={}", key, value);
self.client.srem(&field_key, &obj.id).await?;
}
// Remove from MIME index
if let Some(mime) = &obj.meta.mime {
let field_key = format!("field:mime:{}", mime);
self.client.srem(&field_key, &obj.id).await?;
}
// Remove from title index
if let Some(title) = &obj.meta.title {
let field_key = format!("field:title:{}", title);
self.client.srem(&field_key, &obj.id).await?;
}
// Remove from scan index
self.client.srem("scan:index", &obj.id).await?;
Ok(())
}
/// Update object indexes (remove old, add new)
pub async fn reindex_object(&self, old_obj: &OsirisObject, new_obj: &OsirisObject) -> Result<()> {
self.deindex_object(old_obj).await?;
self.index_object(new_obj).await?;
Ok(())
}
/// Get all IDs matching a tag filter
pub async fn get_ids_by_tag(&self, key: &str, value: &str) -> Result<Vec<String>> {
let field_key = format!("field:tag:{}={}", key, value);
self.client.smembers(&field_key).await
}
/// Get all IDs matching a MIME type
pub async fn get_ids_by_mime(&self, mime: &str) -> Result<Vec<String>> {
let field_key = format!("field:mime:{}", mime);
self.client.smembers(&field_key).await
}
/// Get all IDs matching a title
pub async fn get_ids_by_title(&self, title: &str) -> Result<Vec<String>> {
let field_key = format!("field:title:{}", title);
self.client.smembers(&field_key).await
}
/// Get all IDs in the scan index
pub async fn get_all_ids(&self) -> Result<Vec<String>> {
self.client.smembers("scan:index").await
}
/// Get intersection of multiple field filters
pub async fn get_ids_by_filters(&self, filters: &[(String, String)]) -> Result<Vec<String>> {
if filters.is_empty() {
return self.get_all_ids().await;
}
let keys: Vec<String> = filters
.iter()
.map(|(k, v)| {
if k == "mime" {
format!("field:mime:{}", v)
} else if k == "title" {
format!("field:title:{}", v)
} else {
format!("field:tag:{}={}", k, v)
}
})
.collect();
self.client.sinter(&keys).await
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
#[ignore]
async fn test_index_object() {
let client = HeroDbClient::new("redis://localhost:6379", 1).unwrap();
let index = FieldIndex::new(client);
let mut obj = OsirisObject::new("test".to_string(), Some("Hello".to_string()));
obj.set_tag("topic".to_string(), "rust".to_string());
obj.set_mime(Some("text/plain".to_string()));
index.index_object(&obj).await.unwrap();
let ids = index.get_ids_by_tag("topic", "rust").await.unwrap();
assert!(ids.contains(&obj.id));
}
}

View File

@@ -0,0 +1,3 @@
pub mod field_index;
pub use field_index::FieldIndex;

View File

@@ -0,0 +1,408 @@
use crate::config::{self, NamespaceConfig};
use crate::error::{Error, Result};
use crate::index::FieldIndex;
use crate::retrieve::{RetrievalQuery, SearchEngine};
use crate::store::{HeroDbClient, OsirisObject};
use clap::{Parser, Subcommand};
use std::collections::BTreeMap;
use std::fs;
use std::io::{self, Read};
use std::path::PathBuf;
#[derive(Parser, Debug)]
#[command(name = "osiris")]
#[command(about = "OSIRIS - Object Storage, Indexing & Retrieval Intelligent System", long_about = None)]
pub struct Cli {
#[command(subcommand)]
pub command: Commands,
}
#[derive(Subcommand, Debug)]
pub enum Commands {
/// Initialize OSIRIS configuration
Init {
/// HeroDB URL
#[arg(long, default_value = "redis://localhost:6379")]
herodb: String,
},
/// Namespace management
Ns {
#[command(subcommand)]
command: NsCommands,
},
/// Put an object
Put {
/// Object path (namespace/name)
path: String,
/// File to upload (use '-' for stdin)
file: String,
/// Tags (key=value pairs, comma-separated)
#[arg(long)]
tags: Option<String>,
/// MIME type
#[arg(long)]
mime: Option<String>,
/// Title
#[arg(long)]
title: Option<String>,
},
/// Get an object
Get {
/// Object path (namespace/name or namespace/id)
path: String,
/// Output file (default: stdout)
#[arg(long)]
output: Option<PathBuf>,
/// Output raw content only (no metadata)
#[arg(long)]
raw: bool,
},
/// Delete an object
Del {
/// Object path (namespace/name or namespace/id)
path: String,
},
/// Search/find objects
Find {
/// Text query (optional)
query: Option<String>,
/// Namespace to search
#[arg(long)]
ns: String,
/// Filters (key=value pairs, comma-separated)
#[arg(long)]
filter: Option<String>,
/// Maximum number of results
#[arg(long, default_value = "10")]
topk: usize,
/// Output as JSON
#[arg(long)]
json: bool,
},
/// Show statistics
Stats {
/// Namespace (optional, shows all if not specified)
#[arg(long)]
ns: Option<String>,
},
}
#[derive(Subcommand, Debug, Clone)]
pub enum NsCommands {
/// Create a new namespace
Create {
/// Namespace name
name: String,
},
/// List all namespaces
List,
/// Delete a namespace
Delete {
/// Namespace name
name: String,
},
}
impl Cli {
pub async fn run(self) -> Result<()> {
match self.command {
Commands::Init { herodb } => {
let config = config::create_default_config(herodb);
config::save_config(&config, None)?;
println!("✓ OSIRIS initialized");
println!(" Config: {}", config::default_config_path().display());
Ok(())
}
Commands::Ns { ref command } => self.handle_ns_command(command.clone()).await,
Commands::Put { ref path, ref file, ref tags, ref mime, ref title } => {
self.handle_put(path.clone(), file.clone(), tags.clone(), mime.clone(), title.clone()).await
}
Commands::Get { ref path, ref output, raw } => {
self.handle_get(path.clone(), output.clone(), raw).await
}
Commands::Del { ref path } => self.handle_del(path.clone()).await,
Commands::Find { ref query, ref ns, ref filter, topk, json } => {
self.handle_find(query.clone(), ns.clone(), filter.clone(), topk, json).await
}
Commands::Stats { ref ns } => self.handle_stats(ns.clone()).await,
}
}
async fn handle_ns_command(&self, command: NsCommands) -> Result<()> {
let mut config = config::load_config(None)?;
match command {
NsCommands::Create { name } => {
if config.get_namespace(&name).is_some() {
return Err(Error::InvalidInput(format!(
"Namespace '{}' already exists",
name
)));
}
let db_id = config.next_db_id();
let ns_config = NamespaceConfig { db_id };
config.set_namespace(name.clone(), ns_config);
config::save_config(&config, None)?;
println!("✓ Created namespace '{}' (DB {})", name, db_id);
Ok(())
}
NsCommands::List => {
if config.namespaces.is_empty() {
println!("No namespaces configured");
} else {
println!("Namespaces:");
for (name, ns_config) in &config.namespaces {
println!(" {} → DB {}", name, ns_config.db_id);
}
}
Ok(())
}
NsCommands::Delete { name } => {
if config.remove_namespace(&name).is_none() {
return Err(Error::NotFound(format!("Namespace '{}'", name)));
}
config::save_config(&config, None)?;
println!("✓ Deleted namespace '{}'", name);
Ok(())
}
}
}
async fn handle_put(
&self,
path: String,
file: String,
tags: Option<String>,
mime: Option<String>,
title: Option<String>,
) -> Result<()> {
let (ns, name) = parse_path(&path)?;
let config = config::load_config(None)?;
let ns_config = config.get_namespace(&ns)
.ok_or_else(|| Error::NotFound(format!("Namespace '{}'", ns)))?;
// Read content
let content = if file == "-" {
let mut buffer = String::new();
io::stdin().read_to_string(&mut buffer)?;
buffer
} else {
fs::read_to_string(&file)?
};
// Create object
let mut obj = OsirisObject::with_id(name.clone(), ns.clone(), Some(content));
if let Some(title) = title {
obj.set_title(Some(title));
}
if let Some(mime) = mime {
obj.set_mime(Some(mime));
}
// Parse tags
if let Some(tags_str) = tags {
let tag_map = parse_tags(&tags_str)?;
for (key, value) in tag_map {
obj.set_tag(key, value);
}
}
// Store object
let client = HeroDbClient::new(&config.herodb.url, ns_config.db_id)?;
let index = FieldIndex::new(client.clone());
client.put_object(&obj).await?;
index.index_object(&obj).await?;
println!("✓ Stored {}/{}", ns, obj.id);
Ok(())
}
async fn handle_get(&self, path: String, output: Option<PathBuf>, raw: bool) -> Result<()> {
let (ns, id) = parse_path(&path)?;
let config = config::load_config(None)?;
let ns_config = config.get_namespace(&ns)
.ok_or_else(|| Error::NotFound(format!("Namespace '{}'", ns)))?;
let client = HeroDbClient::new(&config.herodb.url, ns_config.db_id)?;
let obj = client.get_object(&id).await?;
if raw {
// Output raw content only
let content = obj.text.unwrap_or_default();
if let Some(output_path) = output {
fs::write(output_path, content)?;
} else {
print!("{}", content);
}
} else {
// Output full object as JSON
let json = serde_json::to_string_pretty(&obj)?;
if let Some(output_path) = output {
fs::write(output_path, json)?;
} else {
println!("{}", json);
}
}
Ok(())
}
async fn handle_del(&self, path: String) -> Result<()> {
let (ns, id) = parse_path(&path)?;
let config = config::load_config(None)?;
let ns_config = config.get_namespace(&ns)
.ok_or_else(|| Error::NotFound(format!("Namespace '{}'", ns)))?;
let client = HeroDbClient::new(&config.herodb.url, ns_config.db_id)?;
let index = FieldIndex::new(client.clone());
// Get object first to deindex it
let obj = client.get_object(&id).await?;
index.deindex_object(&obj).await?;
let deleted = client.delete_object(&id).await?;
if deleted {
println!("✓ Deleted {}/{}", ns, id);
Ok(())
} else {
Err(Error::NotFound(format!("{}/{}", ns, id)))
}
}
async fn handle_find(
&self,
query: Option<String>,
ns: String,
filter: Option<String>,
topk: usize,
json: bool,
) -> Result<()> {
let config = config::load_config(None)?;
let ns_config = config.get_namespace(&ns)
.ok_or_else(|| Error::NotFound(format!("Namespace '{}'", ns)))?;
let client = HeroDbClient::new(&config.herodb.url, ns_config.db_id)?;
let engine = SearchEngine::new(client.clone());
// Build query
let mut retrieval_query = RetrievalQuery::new(ns.clone()).with_top_k(topk);
if let Some(text) = query {
retrieval_query = retrieval_query.with_text(text);
}
if let Some(filter_str) = filter {
let filters = parse_tags(&filter_str)?;
for (key, value) in filters {
retrieval_query = retrieval_query.with_filter(key, value);
}
}
// Execute search
let results = engine.search(&retrieval_query).await?;
if json {
println!("{}", serde_json::to_string_pretty(&results)?);
} else {
if results.is_empty() {
println!("No results found");
} else {
println!("Found {} result(s):\n", results.len());
for (i, result) in results.iter().enumerate() {
println!("{}. {} (score: {:.2})", i + 1, result.id, result.score);
if let Some(snippet) = &result.snippet {
println!(" {}", snippet);
}
println!();
}
}
}
Ok(())
}
async fn handle_stats(&self, ns: Option<String>) -> Result<()> {
let config = config::load_config(None)?;
if let Some(ns_name) = ns {
let ns_config = config.get_namespace(&ns_name)
.ok_or_else(|| Error::NotFound(format!("Namespace '{}'", ns_name)))?;
let client = HeroDbClient::new(&config.herodb.url, ns_config.db_id)?;
let size = client.dbsize().await?;
println!("Namespace: {}", ns_name);
println!(" DB ID: {}", ns_config.db_id);
println!(" Keys: {}", size);
} else {
println!("OSIRIS Statistics\n");
println!("Namespaces: {}", config.namespaces.len());
for (name, ns_config) in &config.namespaces {
let client = HeroDbClient::new(&config.herodb.url, ns_config.db_id)?;
let size = client.dbsize().await?;
println!(" {} (DB {}) → {} keys", name, ns_config.db_id, size);
}
}
Ok(())
}
}
/// Parse a path into namespace and name/id
fn parse_path(path: &str) -> Result<(String, String)> {
let parts: Vec<&str> = path.splitn(2, '/').collect();
if parts.len() != 2 {
return Err(Error::InvalidInput(format!(
"Invalid path format. Expected 'namespace/name', got '{}'",
path
)));
}
Ok((parts[0].to_string(), parts[1].to_string()))
}
/// Parse tags from comma-separated key=value pairs
fn parse_tags(tags_str: &str) -> Result<BTreeMap<String, String>> {
let mut tags = BTreeMap::new();
for pair in tags_str.split(',') {
let parts: Vec<&str> = pair.trim().splitn(2, '=').collect();
if parts.len() != 2 {
return Err(Error::InvalidInput(format!(
"Invalid tag format. Expected 'key=value', got '{}'",
pair
)));
}
tags.insert(parts[0].to_string(), parts[1].to_string());
}
Ok(tags)
}

View File

@@ -0,0 +1,3 @@
pub mod cli;
pub use cli::Cli;

23
lib/osiris/core/lib.rs Normal file
View File

@@ -0,0 +1,23 @@
// Allow the crate to reference itself as ::osiris for the derive macro
extern crate self as osiris;
pub mod config;
pub mod error;
pub mod index;
pub mod interfaces;
pub mod objects;
pub mod retrieve;
pub mod store;
// Rhai integration modules (top-level)
pub mod context;
pub use error::{Error, Result};
pub use store::{BaseData, IndexKey, Object, Storable};
pub use objects::{Event, Note};
// OsirisContext is the main type for Rhai integration
pub use context::{OsirisContext, OsirisInstance, OsirisContextBuilder};
// Re-export the derive macro
pub use osiris_derive::Object as DeriveObject;

22
lib/osiris/core/main.rs Normal file
View File

@@ -0,0 +1,22 @@
use clap::Parser;
use osiris::interfaces::Cli;
use tracing_subscriber::{fmt, EnvFilter};
#[tokio::main]
async fn main() {
// Initialize tracing
fmt()
.with_env_filter(
EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")),
)
.init();
// Parse CLI arguments
let cli = Cli::parse();
// Run the command
if let Err(e) = cli.run().await {
eprintln!("Error: {}", e);
std::process::exit(1);
}
}

View File

@@ -0,0 +1,151 @@
/// Expense Object for Accounting
use crate::store::BaseData;
use serde::{Deserialize, Serialize};
/// Expense category
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum ExpenseCategory {
Registration,
Subscription,
Service,
Product,
Other,
}
impl Default for ExpenseCategory {
fn default() -> Self {
ExpenseCategory::Other
}
}
/// Expense status
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum ExpenseStatus {
Pending,
Approved,
Paid,
Rejected,
}
impl Default for ExpenseStatus {
fn default() -> Self {
ExpenseStatus::Pending
}
}
/// Expense record
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, crate::DeriveObject)]
pub struct Expense {
/// Base data for object storage
pub base_data: BaseData,
/// User/entity ID who incurred the expense
pub user_id: u32,
/// Amount
pub amount: f64,
/// Currency
pub currency: String,
/// Description
pub description: String,
/// Category
pub category: ExpenseCategory,
/// Status
pub status: ExpenseStatus,
/// Date incurred (unix timestamp)
pub expense_date: u64,
/// Related invoice ID (if any)
pub invoice_id: Option<u32>,
}
impl Expense {
/// Create a new expense
pub fn new(id: u32) -> Self {
let base_data = BaseData::with_id(id, String::new());
let now = time::OffsetDateTime::now_utc().unix_timestamp() as u64;
Self {
base_data,
user_id: 0,
amount: 0.0,
currency: String::from("USD"),
description: String::new(),
category: ExpenseCategory::default(),
status: ExpenseStatus::default(),
expense_date: now,
invoice_id: None,
}
}
/// Set user ID (fluent)
pub fn user_id(mut self, id: u32) -> Self {
self.user_id = id;
self
}
/// Set amount (fluent)
pub fn amount(mut self, amount: f64) -> Self {
self.amount = amount;
self
}
/// Set currency (fluent)
pub fn currency(mut self, currency: impl ToString) -> Self {
self.currency = currency.to_string();
self
}
/// Set description (fluent)
pub fn description(mut self, description: impl ToString) -> Self {
self.description = description.to_string();
self
}
/// Set category (fluent)
pub fn category(mut self, category: ExpenseCategory) -> Self {
self.category = category;
self
}
/// Set category from string (fluent)
pub fn category_str(mut self, category: &str) -> Self {
self.category = match category.to_lowercase().as_str() {
"registration" => ExpenseCategory::Registration,
"subscription" => ExpenseCategory::Subscription,
"service" => ExpenseCategory::Service,
"product" => ExpenseCategory::Product,
_ => ExpenseCategory::Other,
};
self
}
/// Set invoice ID (fluent)
pub fn invoice_id(mut self, id: u32) -> Self {
self.invoice_id = Some(id);
self
}
/// Approve expense
pub fn approve(mut self) -> Self {
self.status = ExpenseStatus::Approved;
self
}
/// Mark as paid
pub fn mark_paid(mut self) -> Self {
self.status = ExpenseStatus::Paid;
self
}
/// Reject expense
pub fn reject(mut self) -> Self {
self.status = ExpenseStatus::Rejected;
self
}
}

View File

@@ -0,0 +1,130 @@
/// Invoice Object for Accounting
use crate::store::BaseData;
use serde::{Deserialize, Serialize};
/// Invoice status
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum InvoiceStatus {
Draft,
Sent,
Paid,
Overdue,
Cancelled,
}
impl Default for InvoiceStatus {
fn default() -> Self {
InvoiceStatus::Draft
}
}
/// Invoice for billing
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, crate::DeriveObject)]
pub struct Invoice {
/// Base data for object storage
pub base_data: BaseData,
/// Invoice number
pub invoice_number: String,
/// Customer/payer ID
pub customer_id: u32,
/// Amount
pub amount: f64,
/// Currency
pub currency: String,
/// Description
pub description: String,
/// Status
pub status: InvoiceStatus,
/// Due date (unix timestamp)
pub due_date: Option<u64>,
/// Payment date (unix timestamp)
pub paid_date: Option<u64>,
}
impl Invoice {
/// Create a new invoice
pub fn new(id: u32) -> Self {
let base_data = BaseData::with_id(id, String::new());
Self {
base_data,
invoice_number: String::new(),
customer_id: 0,
amount: 0.0,
currency: String::from("USD"),
description: String::new(),
status: InvoiceStatus::default(),
due_date: None,
paid_date: None,
}
}
/// Set invoice number (fluent)
pub fn invoice_number(mut self, number: impl ToString) -> Self {
self.invoice_number = number.to_string();
self
}
/// Set customer ID (fluent)
pub fn customer_id(mut self, id: u32) -> Self {
self.customer_id = id;
self
}
/// Set amount (fluent)
pub fn amount(mut self, amount: f64) -> Self {
self.amount = amount;
self
}
/// Set currency (fluent)
pub fn currency(mut self, currency: impl ToString) -> Self {
self.currency = currency.to_string();
self
}
/// Set description (fluent)
pub fn description(mut self, description: impl ToString) -> Self {
self.description = description.to_string();
self
}
/// Set due date (fluent)
pub fn due_date(mut self, date: u64) -> Self {
self.due_date = Some(date);
self
}
/// Mark as sent
pub fn send(mut self) -> Self {
self.status = InvoiceStatus::Sent;
self
}
/// Mark as paid
pub fn mark_paid(mut self) -> Self {
self.status = InvoiceStatus::Paid;
self.paid_date = Some(time::OffsetDateTime::now_utc().unix_timestamp() as u64);
self
}
/// Mark as overdue
pub fn mark_overdue(mut self) -> Self {
self.status = InvoiceStatus::Overdue;
self
}
/// Cancel invoice
pub fn cancel(mut self) -> Self {
self.status = InvoiceStatus::Cancelled;
self
}
}

View File

@@ -0,0 +1,11 @@
/// Accounting Module
///
/// Provides Invoice and Expense objects for financial tracking
pub mod invoice;
pub mod expense;
pub mod rhai;
pub use invoice::{Invoice, InvoiceStatus};
pub use expense::{Expense, ExpenseCategory, ExpenseStatus};
// pub use rhai::register_accounting_modules; // TODO: Implement when needed

View File

@@ -0,0 +1,489 @@
/// Email Client
///
/// Real SMTP email client for sending emails including verification emails.
use serde::{Deserialize, Serialize};
use super::verification::Verification;
use crate::store::{BaseData, Object, Storable};
use lettre::{
Message, SmtpTransport, Transport,
message::{header::ContentType, MultiPart, SinglePart},
transport::smtp::authentication::Credentials,
};
/// Email client with SMTP configuration
#[derive(Debug, Clone, Serialize, Deserialize, crate::DeriveObject)]
pub struct EmailClient {
#[serde(flatten)]
pub base_data: BaseData,
/// SMTP server hostname
pub smtp_host: String,
/// SMTP port
pub smtp_port: u16,
/// Username for SMTP auth
pub username: String,
/// Password for SMTP auth
pub password: String,
/// From address
pub from_address: String,
/// From name
pub from_name: String,
/// Use TLS
pub use_tls: bool,
}
/// Mail template with placeholders
#[derive(Debug, Clone, Serialize, Deserialize, crate::DeriveObject)]
pub struct MailTemplate {
#[serde(flatten)]
pub base_data: BaseData,
/// Template ID
pub id: String,
/// Template name
pub name: String,
/// Email subject (can contain placeholders like ${name})
pub subject: String,
/// Email body (can contain placeholders like ${code}, ${url})
pub body: String,
/// HTML body (optional, can contain placeholders)
pub html_body: Option<String>,
}
impl Default for MailTemplate {
fn default() -> Self {
Self {
base_data: BaseData::new(),
id: String::new(),
name: String::new(),
subject: String::new(),
body: String::new(),
html_body: None,
}
}
}
/// Email message created from a template
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct Mail {
/// Recipient email address
pub to: String,
/// Template ID to use
pub template_id: Option<String>,
/// Parameters to replace in template
pub parameters: std::collections::HashMap<String, String>,
/// Direct subject (if not using template)
pub subject: Option<String>,
/// Direct body (if not using template)
pub body: Option<String>,
}
impl Default for EmailClient {
fn default() -> Self {
Self {
base_data: BaseData::new(),
smtp_host: "localhost".to_string(),
smtp_port: 587,
username: String::new(),
password: String::new(),
from_address: "noreply@example.com".to_string(),
from_name: "No Reply".to_string(),
use_tls: true,
}
}
}
impl MailTemplate {
/// Create a new mail template
pub fn new() -> Self {
Self::default()
}
/// Builder: Set template ID
pub fn id(mut self, id: String) -> Self {
self.id = id;
self
}
/// Builder: Set template name
pub fn name(mut self, name: String) -> Self {
self.name = name;
self
}
/// Builder: Set subject
pub fn subject(mut self, subject: String) -> Self {
self.subject = subject;
self
}
/// Builder: Set body
pub fn body(mut self, body: String) -> Self {
self.body = body;
self
}
/// Builder: Set HTML body
pub fn html_body(mut self, html_body: String) -> Self {
self.html_body = Some(html_body);
self
}
/// Replace placeholders in text
fn replace_placeholders(&self, text: &str, parameters: &std::collections::HashMap<String, String>) -> String {
let mut result = text.to_string();
for (key, value) in parameters {
let placeholder = format!("${{{}}}", key);
result = result.replace(&placeholder, value);
}
result
}
/// Render subject with parameters
pub fn render_subject(&self, parameters: &std::collections::HashMap<String, String>) -> String {
self.replace_placeholders(&self.subject, parameters)
}
/// Render body with parameters
pub fn render_body(&self, parameters: &std::collections::HashMap<String, String>) -> String {
self.replace_placeholders(&self.body, parameters)
}
/// Render HTML body with parameters
pub fn render_html_body(&self, parameters: &std::collections::HashMap<String, String>) -> Option<String> {
self.html_body.as_ref().map(|html| self.replace_placeholders(html, parameters))
}
}
impl Mail {
/// Create a new mail
pub fn new() -> Self {
Self::default()
}
/// Builder: Set recipient
pub fn to(mut self, to: String) -> Self {
self.to = to;
self
}
/// Builder: Set template ID
pub fn template(mut self, template_id: String) -> Self {
self.template_id = Some(template_id);
self
}
/// Builder: Add a parameter
pub fn parameter(mut self, key: String, value: String) -> Self {
self.parameters.insert(key, value);
self
}
/// Builder: Set subject (for non-template emails)
pub fn subject(mut self, subject: String) -> Self {
self.subject = Some(subject);
self
}
/// Builder: Set body (for non-template emails)
pub fn body(mut self, body: String) -> Self {
self.body = Some(body);
self
}
}
impl EmailClient {
/// Create a new email client
pub fn new() -> Self {
Self::default()
}
/// Builder: Set SMTP host
pub fn smtp_host(mut self, host: String) -> Self {
self.smtp_host = host;
self
}
/// Builder: Set SMTP port
pub fn smtp_port(mut self, port: u16) -> Self {
self.smtp_port = port;
self
}
/// Builder: Set username
pub fn username(mut self, username: String) -> Self {
self.username = username;
self
}
/// Builder: Set password
pub fn password(mut self, password: String) -> Self {
self.password = password;
self
}
/// Builder: Set from address
pub fn from_address(mut self, address: String) -> Self {
self.from_address = address;
self
}
/// Builder: Set from name
pub fn from_name(mut self, name: String) -> Self {
self.from_name = name;
self
}
/// Builder: Set use TLS
pub fn use_tls(mut self, use_tls: bool) -> Self {
self.use_tls = use_tls;
self
}
/// Build SMTP transport
fn build_transport(&self) -> Result<SmtpTransport, String> {
let creds = Credentials::new(
self.username.clone(),
self.password.clone(),
);
let transport = if self.use_tls {
SmtpTransport::starttls_relay(&self.smtp_host)
.map_err(|e| format!("Failed to create SMTP transport: {}", e))?
.credentials(creds)
.port(self.smtp_port)
.build()
} else {
SmtpTransport::builder_dangerous(&self.smtp_host)
.credentials(creds)
.port(self.smtp_port)
.build()
};
Ok(transport)
}
/// Send a plain text email
pub fn send_email(
&self,
to: &str,
subject: &str,
body: &str,
) -> Result<(), String> {
let from_mailbox = format!("{} <{}>", self.from_name, self.from_address)
.parse()
.map_err(|e| format!("Invalid from address: {}", e))?;
let to_mailbox = to.parse()
.map_err(|e| format!("Invalid to address: {}", e))?;
let email = Message::builder()
.from(from_mailbox)
.to(to_mailbox)
.subject(subject)
.body(body.to_string())
.map_err(|e| format!("Failed to build email: {}", e))?;
let transport = self.build_transport()?;
transport.send(&email)
.map_err(|e| format!("Failed to send email: {}", e))?;
Ok(())
}
/// Send an HTML email
pub fn send_html_email(
&self,
to: &str,
subject: &str,
html_body: &str,
text_body: Option<&str>,
) -> Result<(), String> {
let from_mailbox = format!("{} <{}>", self.from_name, self.from_address)
.parse()
.map_err(|e| format!("Invalid from address: {}", e))?;
let to_mailbox = to.parse()
.map_err(|e| format!("Invalid to address: {}", e))?;
// Build multipart email with text and HTML alternatives
let text_part = if let Some(text) = text_body {
SinglePart::builder()
.header(ContentType::TEXT_PLAIN)
.body(text.to_string())
} else {
SinglePart::builder()
.header(ContentType::TEXT_PLAIN)
.body(String::new())
};
let html_part = SinglePart::builder()
.header(ContentType::TEXT_HTML)
.body(html_body.to_string());
let multipart = MultiPart::alternative()
.singlepart(text_part)
.singlepart(html_part);
let email = Message::builder()
.from(from_mailbox)
.to(to_mailbox)
.subject(subject)
.multipart(multipart)
.map_err(|e| format!("Failed to build email: {}", e))?;
let transport = self.build_transport()?;
transport.send(&email)
.map_err(|e| format!("Failed to send email: {}", e))?;
Ok(())
}
/// Send a mail using a template
pub fn send_mail(&self, mail: &Mail, template: &MailTemplate) -> Result<(), String> {
// Render subject and body with parameters
let subject = template.render_subject(&mail.parameters);
let body_text = template.render_body(&mail.parameters);
let html_body = template.render_html_body(&mail.parameters);
// Send email
if let Some(html) = html_body {
self.send_html_email(&mail.to, &subject, &html, Some(&body_text))
} else {
self.send_email(&mail.to, &subject, &body_text)
}
}
/// Send a verification email with code
pub fn send_verification_code_email(
&self,
verification: &Verification,
) -> Result<(), String> {
let subject = "Verify your email address";
let body = format!(
"Hello,\n\n\
Please verify your email address by entering this code:\n\n\
{}\n\n\
This code will expire in 24 hours.\n\n\
If you didn't request this, please ignore this email.",
verification.verification_code
);
self.send_email(&verification.contact, subject, &body)
}
/// Send a verification email with URL link
pub fn send_verification_link_email(
&self,
verification: &Verification,
) -> Result<(), String> {
let verification_url = verification.get_verification_url()
.ok_or_else(|| "No callback URL configured".to_string())?;
let subject = "Verify your email address";
let html_body = format!(
r#"<!DOCTYPE html>
<html>
<head>
<style>
body {{ font-family: Arial, sans-serif; line-height: 1.6; color: #333; }}
.container {{ max-width: 600px; margin: 0 auto; padding: 20px; }}
.button {{
display: inline-block;
padding: 12px 24px;
background-color: #007bff;
color: white;
text-decoration: none;
border-radius: 4px;
margin: 20px 0;
}}
.code {{
font-size: 24px;
font-weight: bold;
letter-spacing: 4px;
padding: 10px;
background-color: #f5f5f5;
display: inline-block;
margin: 10px 0;
}}
</style>
</head>
<body>
<div class="container">
<h2>Verify your email address</h2>
<p>Hello,</p>
<p>Please verify your email address by clicking the button below:</p>
<a href="{}" class="button">Verify Email</a>
<p>Or enter this verification code:</p>
<div class="code">{}</div>
<p>This link and code will expire in 24 hours.</p>
<p>If you didn't request this, please ignore this email.</p>
</div>
</body>
</html>"#,
verification_url, verification.verification_code
);
let text_body = format!(
"Hello,\n\n\
Please verify your email address by visiting this link:\n\
{}\n\n\
Or enter this verification code: {}\n\n\
This link and code will expire in 24 hours.\n\n\
If you didn't request this, please ignore this email.",
verification_url, verification.verification_code
);
self.send_html_email(
&verification.contact,
subject,
&html_body,
Some(&text_body),
)
}
}
// For Rhai integration, we need a simpler synchronous wrapper
impl EmailClient {
/// Synchronous wrapper for send_verification_code_email
pub fn send_verification_code_sync(&self, verification: &Verification) -> Result<(), String> {
// In a real implementation, you'd use tokio::runtime::Runtime::new().block_on()
// For now, just simulate
println!("=== VERIFICATION CODE EMAIL ===");
println!("To: {}", verification.contact);
println!("Code: {}", verification.verification_code);
println!("===============================");
Ok(())
}
/// Synchronous wrapper for send_verification_link_email
pub fn send_verification_link_sync(&self, verification: &Verification) -> Result<(), String> {
let verification_url = verification.get_verification_url()
.ok_or_else(|| "No callback URL configured".to_string())?;
println!("=== VERIFICATION LINK EMAIL ===");
println!("To: {}", verification.contact);
println!("Code: {}", verification.verification_code);
println!("Link: {}", verification_url);
println!("===============================");
Ok(())
}
}

View File

@@ -0,0 +1,10 @@
/// Communication Module
///
/// Transport-agnostic verification and email client.
pub mod verification;
pub mod email;
pub mod rhai;
pub use verification::{Verification, VerificationStatus, VerificationTransport};
pub use email::EmailClient;

View File

@@ -0,0 +1,407 @@
/// Rhai bindings for Communication (Verification and Email)
use ::rhai::plugin::*;
use ::rhai::{CustomType, Dynamic, Engine, EvalAltResult, Module, TypeBuilder};
use super::verification::{Verification, VerificationStatus, VerificationTransport};
use super::email::{EmailClient, MailTemplate, Mail};
// ============================================================================
// Verification Module
// ============================================================================
type RhaiVerification = Verification;
#[export_module]
mod rhai_verification_module {
use super::RhaiVerification;
use super::super::verification::{Verification, VerificationTransport};
#[rhai_fn(name = "new_verification", return_raw)]
pub fn new_verification(
entity_id: String,
contact: String,
) -> Result<RhaiVerification, Box<EvalAltResult>> {
// Default to email transport
Ok(Verification::new(0, entity_id, contact, VerificationTransport::Email))
}
#[rhai_fn(name = "callback_url", return_raw)]
pub fn set_callback_url(
verification: &mut RhaiVerification,
url: String,
) -> Result<RhaiVerification, Box<EvalAltResult>> {
let owned = std::mem::take(verification);
*verification = owned.callback_url(url);
Ok(verification.clone())
}
#[rhai_fn(name = "mark_sent", return_raw)]
pub fn mark_sent(
verification: &mut RhaiVerification,
) -> Result<(), Box<EvalAltResult>> {
verification.mark_sent();
Ok(())
}
#[rhai_fn(name = "verify_code", return_raw)]
pub fn verify_code(
verification: &mut RhaiVerification,
code: String,
) -> Result<(), Box<EvalAltResult>> {
verification.verify_code(&code)
.map_err(|e| e.into())
}
#[rhai_fn(name = "verify_nonce", return_raw)]
pub fn verify_nonce(
verification: &mut RhaiVerification,
nonce: String,
) -> Result<(), Box<EvalAltResult>> {
verification.verify_nonce(&nonce)
.map_err(|e| e.into())
}
#[rhai_fn(name = "resend", return_raw)]
pub fn resend(
verification: &mut RhaiVerification,
) -> Result<(), Box<EvalAltResult>> {
verification.resend();
Ok(())
}
// Getters
#[rhai_fn(name = "get_entity_id")]
pub fn get_entity_id(verification: &mut RhaiVerification) -> String {
verification.entity_id.clone()
}
#[rhai_fn(name = "get_contact")]
pub fn get_contact(verification: &mut RhaiVerification) -> String {
verification.contact.clone()
}
#[rhai_fn(name = "get_code")]
pub fn get_code(verification: &mut RhaiVerification) -> String {
verification.verification_code.clone()
}
#[rhai_fn(name = "get_nonce")]
pub fn get_nonce(verification: &mut RhaiVerification) -> String {
verification.verification_nonce.clone()
}
#[rhai_fn(name = "get_verification_url")]
pub fn get_verification_url(verification: &mut RhaiVerification) -> String {
verification.get_verification_url().unwrap_or_default()
}
#[rhai_fn(name = "get_status")]
pub fn get_status(verification: &mut RhaiVerification) -> String {
format!("{:?}", verification.status)
}
#[rhai_fn(name = "get_attempts")]
pub fn get_attempts(verification: &mut RhaiVerification) -> i64 {
verification.attempts as i64
}
}
// ============================================================================
// Mail Template Module
// ============================================================================
type RhaiMailTemplate = MailTemplate;
#[export_module]
mod rhai_mail_template_module {
use super::RhaiMailTemplate;
use super::super::email::MailTemplate;
use ::rhai::EvalAltResult;
#[rhai_fn(name = "new_mail_template", return_raw)]
pub fn new_mail_template() -> Result<RhaiMailTemplate, Box<EvalAltResult>> {
Ok(MailTemplate::new())
}
#[rhai_fn(name = "id", return_raw)]
pub fn set_id(
template: &mut RhaiMailTemplate,
id: String,
) -> Result<RhaiMailTemplate, Box<EvalAltResult>> {
let owned = std::mem::take(template);
*template = owned.id(id);
Ok(template.clone())
}
#[rhai_fn(name = "name", return_raw)]
pub fn set_name(
template: &mut RhaiMailTemplate,
name: String,
) -> Result<RhaiMailTemplate, Box<EvalAltResult>> {
let owned = std::mem::take(template);
*template = owned.name(name);
Ok(template.clone())
}
#[rhai_fn(name = "subject", return_raw)]
pub fn set_subject(
template: &mut RhaiMailTemplate,
subject: String,
) -> Result<RhaiMailTemplate, Box<EvalAltResult>> {
let owned = std::mem::take(template);
*template = owned.subject(subject);
Ok(template.clone())
}
#[rhai_fn(name = "body", return_raw)]
pub fn set_body(
template: &mut RhaiMailTemplate,
body: String,
) -> Result<RhaiMailTemplate, Box<EvalAltResult>> {
let owned = std::mem::take(template);
*template = owned.body(body);
Ok(template.clone())
}
#[rhai_fn(name = "html_body", return_raw)]
pub fn set_html_body(
template: &mut RhaiMailTemplate,
html_body: String,
) -> Result<RhaiMailTemplate, Box<EvalAltResult>> {
let owned = std::mem::take(template);
*template = owned.html_body(html_body);
Ok(template.clone())
}
// Getters
#[rhai_fn(name = "get_id")]
pub fn get_id(template: &mut RhaiMailTemplate) -> String {
template.id.clone()
}
}
// ============================================================================
// Mail Module
// ============================================================================
type RhaiMail = Mail;
#[export_module]
mod rhai_mail_module {
use super::RhaiMail;
use super::super::email::Mail;
use ::rhai::EvalAltResult;
#[rhai_fn(name = "new_mail", return_raw)]
pub fn new_mail() -> Result<RhaiMail, Box<EvalAltResult>> {
Ok(Mail::new())
}
#[rhai_fn(name = "to", return_raw)]
pub fn set_to(
mail: &mut RhaiMail,
to: String,
) -> Result<RhaiMail, Box<EvalAltResult>> {
let owned = std::mem::take(mail);
*mail = owned.to(to);
Ok(mail.clone())
}
#[rhai_fn(name = "template", return_raw)]
pub fn set_template(
mail: &mut RhaiMail,
template_id: String,
) -> Result<RhaiMail, Box<EvalAltResult>> {
let owned = std::mem::take(mail);
*mail = owned.template(template_id);
Ok(mail.clone())
}
#[rhai_fn(name = "parameter", return_raw)]
pub fn add_parameter(
mail: &mut RhaiMail,
key: String,
value: String,
) -> Result<RhaiMail, Box<EvalAltResult>> {
let owned = std::mem::take(mail);
*mail = owned.parameter(key, value);
Ok(mail.clone())
}
}
// ============================================================================
// Email Client Module
// ============================================================================
type RhaiEmailClient = EmailClient;
#[export_module]
mod rhai_email_module {
use super::RhaiEmailClient;
use super::RhaiMail;
use super::RhaiMailTemplate;
use super::super::email::{EmailClient, Mail, MailTemplate};
use super::super::verification::Verification;
use ::rhai::EvalAltResult;
#[rhai_fn(name = "new_email_client", return_raw)]
pub fn new_email_client() -> Result<RhaiEmailClient, Box<EvalAltResult>> {
Ok(EmailClient::new())
}
#[rhai_fn(name = "smtp_host", return_raw)]
pub fn set_smtp_host(
client: &mut RhaiEmailClient,
host: String,
) -> Result<RhaiEmailClient, Box<EvalAltResult>> {
let owned = std::mem::take(client);
*client = owned.smtp_host(host);
Ok(client.clone())
}
#[rhai_fn(name = "smtp_port", return_raw)]
pub fn set_smtp_port(
client: &mut RhaiEmailClient,
port: i64,
) -> Result<RhaiEmailClient, Box<EvalAltResult>> {
let owned = std::mem::take(client);
*client = owned.smtp_port(port as u16);
Ok(client.clone())
}
#[rhai_fn(name = "username", return_raw)]
pub fn set_username(
client: &mut RhaiEmailClient,
username: String,
) -> Result<RhaiEmailClient, Box<EvalAltResult>> {
let owned = std::mem::take(client);
*client = owned.username(username);
Ok(client.clone())
}
#[rhai_fn(name = "password", return_raw)]
pub fn set_password(
client: &mut RhaiEmailClient,
password: String,
) -> Result<RhaiEmailClient, Box<EvalAltResult>> {
let owned = std::mem::take(client);
*client = owned.password(password);
Ok(client.clone())
}
#[rhai_fn(name = "from_email", return_raw)]
pub fn set_from_email(
client: &mut RhaiEmailClient,
email: String,
) -> Result<RhaiEmailClient, Box<EvalAltResult>> {
let owned = std::mem::take(client);
*client = owned.from_address(email);
Ok(client.clone())
}
#[rhai_fn(name = "from_name", return_raw)]
pub fn set_from_name(
client: &mut RhaiEmailClient,
name: String,
) -> Result<RhaiEmailClient, Box<EvalAltResult>> {
let owned = std::mem::take(client);
*client = owned.from_name(name);
Ok(client.clone())
}
#[rhai_fn(name = "use_tls", return_raw)]
pub fn set_use_tls(
client: &mut RhaiEmailClient,
use_tls: bool,
) -> Result<RhaiEmailClient, Box<EvalAltResult>> {
let owned = std::mem::take(client);
*client = owned.use_tls(use_tls);
Ok(client.clone())
}
#[rhai_fn(name = "send_mail", return_raw)]
pub fn send_mail(
client: &mut RhaiEmailClient,
mail: RhaiMail,
template: RhaiMailTemplate,
) -> Result<(), Box<EvalAltResult>> {
client.send_mail(&mail, &template)
.map_err(|e| e.into())
}
#[rhai_fn(name = "send_verification_code", return_raw)]
pub fn send_verification_code(
client: &mut RhaiEmailClient,
verification: Verification,
) -> Result<(), Box<EvalAltResult>> {
client.send_verification_code_sync(&verification)
.map_err(|e| e.into())
}
#[rhai_fn(name = "send_verification_link", return_raw)]
pub fn send_verification_link(
client: &mut RhaiEmailClient,
verification: Verification,
) -> Result<(), Box<EvalAltResult>> {
client.send_verification_link_sync(&verification)
.map_err(|e| e.into())
}
}
// ============================================================================
// Registration Functions
// ============================================================================
/// Register Communication modules into a Rhai Module
pub fn register_communication_modules(parent_module: &mut Module) {
// Register custom types
parent_module.set_custom_type::<Verification>("Verification");
parent_module.set_custom_type::<MailTemplate>("MailTemplate");
parent_module.set_custom_type::<Mail>("Mail");
parent_module.set_custom_type::<EmailClient>("EmailClient");
// Merge verification functions
let verification_module = exported_module!(rhai_verification_module);
parent_module.combine_flatten(verification_module);
// Merge mail template functions
let mail_template_module = exported_module!(rhai_mail_template_module);
parent_module.combine_flatten(mail_template_module);
// Merge mail functions
let mail_module = exported_module!(rhai_mail_module);
parent_module.combine_flatten(mail_module);
// Merge email client functions
let email_module = exported_module!(rhai_email_module);
parent_module.combine_flatten(email_module);
}
// ============================================================================
// CustomType Implementations
// ============================================================================
impl CustomType for Verification {
fn build(mut builder: TypeBuilder<Self>) {
builder.with_name("Verification");
}
}
impl CustomType for MailTemplate {
fn build(mut builder: TypeBuilder<Self>) {
builder.with_name("MailTemplate");
}
}
impl CustomType for Mail {
fn build(mut builder: TypeBuilder<Self>) {
builder.with_name("Mail");
}
}
impl CustomType for EmailClient {
fn build(mut builder: TypeBuilder<Self>) {
builder.with_name("EmailClient");
}
}

View File

@@ -0,0 +1,239 @@
/// Transport-Agnostic Verification
///
/// Manages verification sessions with codes and nonces for email, SMS, etc.
use crate::store::{BaseData, Object, Storable};
use serde::{Deserialize, Serialize};
/// Verification transport type
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum VerificationTransport {
Email,
Sms,
WhatsApp,
Telegram,
Other(String),
}
impl Default for VerificationTransport {
fn default() -> Self {
VerificationTransport::Email
}
}
/// Verification status
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
#[serde(rename_all = "lowercase")]
pub enum VerificationStatus {
#[default]
Pending,
Sent,
Verified,
Expired,
Failed,
}
/// Verification Session
///
/// Transport-agnostic verification that can be used for email, SMS, etc.
/// Supports both code-based verification and URL-based (nonce) verification.
#[derive(Debug, Clone, Serialize, Deserialize, Default, crate::DeriveObject)]
pub struct Verification {
#[serde(flatten)]
pub base_data: BaseData,
/// User/entity ID this verification is for
pub entity_id: String,
/// Contact address (email, phone, etc.)
pub contact: String,
/// Transport type
pub transport: VerificationTransport,
/// Verification code (6 digits for user entry)
pub verification_code: String,
/// Verification nonce (for URL-based verification)
pub verification_nonce: String,
/// Current status
pub status: VerificationStatus,
/// When verification was sent
pub sent_at: Option<u64>,
/// When verification was completed
pub verified_at: Option<u64>,
/// When verification expires
pub expires_at: Option<u64>,
/// Number of attempts
pub attempts: u32,
/// Maximum attempts allowed
pub max_attempts: u32,
/// Callback URL (for server to construct verification link)
pub callback_url: Option<String>,
/// Additional metadata
#[serde(default)]
pub metadata: std::collections::HashMap<String, String>,
}
impl Verification {
/// Create a new verification
pub fn new(id: u32, entity_id: String, contact: String, transport: VerificationTransport) -> Self {
let mut base_data = BaseData::new();
base_data.id = id;
// Generate verification code (6 digits)
let code = Self::generate_code();
// Generate verification nonce (32 char hex)
let nonce = Self::generate_nonce();
// Set expiry to 24 hours from now
let expires_at = Self::now() + (24 * 60 * 60);
Self {
base_data,
entity_id,
contact,
transport,
verification_code: code,
verification_nonce: nonce,
status: VerificationStatus::Pending,
sent_at: None,
verified_at: None,
expires_at: Some(expires_at),
attempts: 0,
max_attempts: 3,
callback_url: None,
metadata: std::collections::HashMap::new(),
}
}
/// Generate a 6-digit verification code
fn generate_code() -> String {
use std::time::{SystemTime, UNIX_EPOCH};
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_nanos();
format!("{:06}", (timestamp % 1_000_000) as u32)
}
/// Generate a verification nonce (32 char hex string)
fn generate_nonce() -> String {
use std::time::{SystemTime, UNIX_EPOCH};
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_nanos();
format!("{:032x}", timestamp)
}
/// Set callback URL
pub fn callback_url(mut self, url: String) -> Self {
self.callback_url = Some(url);
self
}
/// Get verification URL (callback_url + nonce)
pub fn get_verification_url(&self) -> Option<String> {
self.callback_url.as_ref().map(|base_url| {
if base_url.contains('?') {
format!("{}&nonce={}", base_url, self.verification_nonce)
} else {
format!("{}?nonce={}", base_url, self.verification_nonce)
}
})
}
/// Mark as sent
pub fn mark_sent(&mut self) {
self.status = VerificationStatus::Sent;
self.sent_at = Some(Self::now());
self.base_data.update_modified();
}
/// Verify with code
pub fn verify_code(&mut self, code: &str) -> Result<(), String> {
// Check if expired
if let Some(expires_at) = self.expires_at {
if Self::now() > expires_at {
self.status = VerificationStatus::Expired;
self.base_data.update_modified();
return Err("Verification code expired".to_string());
}
}
// Check attempts
self.attempts += 1;
if self.attempts > self.max_attempts {
self.status = VerificationStatus::Failed;
self.base_data.update_modified();
return Err("Maximum attempts exceeded".to_string());
}
// Check code
if code != self.verification_code {
self.base_data.update_modified();
return Err("Invalid verification code".to_string());
}
// Success
self.status = VerificationStatus::Verified;
self.verified_at = Some(Self::now());
self.base_data.update_modified();
Ok(())
}
/// Verify with nonce (for URL-based verification)
pub fn verify_nonce(&mut self, nonce: &str) -> Result<(), String> {
// Check if expired
if let Some(expires_at) = self.expires_at {
if Self::now() > expires_at {
self.status = VerificationStatus::Expired;
self.base_data.update_modified();
return Err("Verification link expired".to_string());
}
}
// Check nonce
if nonce != self.verification_nonce {
self.base_data.update_modified();
return Err("Invalid verification link".to_string());
}
// Success
self.status = VerificationStatus::Verified;
self.verified_at = Some(Self::now());
self.base_data.update_modified();
Ok(())
}
/// Resend verification (generate new code and nonce)
pub fn resend(&mut self) {
self.verification_code = Self::generate_code();
self.verification_nonce = Self::generate_nonce();
self.status = VerificationStatus::Pending;
self.attempts = 0;
// Extend expiry
self.expires_at = Some(Self::now() + (24 * 60 * 60));
self.base_data.update_modified();
}
/// Helper to get current timestamp
fn now() -> u64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs()
}
}

View File

@@ -0,0 +1,155 @@
/// Email Verification
///
/// Manages email verification sessions and status.
use crate::store::{BaseData, Object, Storable};
use serde::{Deserialize, Serialize};
/// Email verification status
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
#[serde(rename_all = "lowercase")]
pub enum VerificationStatus {
#[default]
Pending,
Sent,
Verified,
Expired,
Failed,
}
/// Email Verification Session
#[derive(Debug, Clone, Serialize, Deserialize, Default, crate::DeriveObject)]
pub struct EmailVerification {
#[serde(flatten)]
pub base_data: BaseData,
/// User/entity ID this verification is for
pub entity_id: String,
/// Email address to verify
pub email: String,
/// Verification code/token
pub verification_code: String,
/// Current status
pub status: VerificationStatus,
/// When verification was sent
pub sent_at: Option<u64>,
/// When verification was completed
pub verified_at: Option<u64>,
/// When verification expires
pub expires_at: Option<u64>,
/// Number of attempts
pub attempts: u32,
/// Maximum attempts allowed
pub max_attempts: u32,
/// Additional metadata
#[serde(default)]
pub metadata: std::collections::HashMap<String, String>,
}
impl EmailVerification {
/// Create a new email verification
pub fn new(id: u32, entity_id: String, email: String) -> Self {
let mut base_data = BaseData::new();
base_data.id = id;
// Generate verification code (6 digits)
let code = Self::generate_code();
// Set expiry to 24 hours from now
let expires_at = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs() + (24 * 60 * 60);
Self {
base_data,
entity_id,
email,
verification_code: code,
status: VerificationStatus::Pending,
sent_at: None,
verified_at: None,
expires_at: Some(expires_at),
attempts: 0,
max_attempts: 3,
metadata: std::collections::HashMap::new(),
}
}
/// Generate a 6-digit verification code
fn generate_code() -> String {
use std::time::{SystemTime, UNIX_EPOCH};
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_nanos();
format!("{:06}", (timestamp % 1_000_000) as u32)
}
/// Mark as sent
pub fn mark_sent(&mut self) {
self.status = VerificationStatus::Sent;
self.sent_at = Some(Self::now());
self.base_data.update_modified();
}
/// Verify with code
pub fn verify(&mut self, code: &str) -> Result<(), String> {
// Check if expired
if let Some(expires_at) = self.expires_at {
if Self::now() > expires_at {
self.status = VerificationStatus::Expired;
self.base_data.update_modified();
return Err("Verification code expired".to_string());
}
}
// Check attempts
self.attempts += 1;
if self.attempts > self.max_attempts {
self.status = VerificationStatus::Failed;
self.base_data.update_modified();
return Err("Maximum attempts exceeded".to_string());
}
// Check code
if code != self.verification_code {
self.base_data.update_modified();
return Err("Invalid verification code".to_string());
}
// Success
self.status = VerificationStatus::Verified;
self.verified_at = Some(Self::now());
self.base_data.update_modified();
Ok(())
}
/// Resend verification (generate new code)
pub fn resend(&mut self) {
self.verification_code = Self::generate_code();
self.status = VerificationStatus::Pending;
self.attempts = 0;
// Extend expiry
self.expires_at = Some(Self::now() + (24 * 60 * 60));
self.base_data.update_modified();
}
/// Helper to get current timestamp
fn now() -> u64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs()
}
}

View File

@@ -0,0 +1,139 @@
use crate::store::BaseData;
use serde::{Deserialize, Serialize};
use time::OffsetDateTime;
pub mod rhai;
/// Event status
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
pub enum EventStatus {
#[default]
Draft,
Published,
Cancelled,
}
/// A calendar event object
#[derive(Debug, Clone, Serialize, Deserialize, crate::DeriveObject)]
pub struct Event {
/// Base data
pub base_data: BaseData,
/// Title of the event
#[index]
pub title: String,
/// Optional description
pub description: Option<String>,
/// Start time
#[index]
#[serde(with = "time::serde::timestamp")]
pub start_time: OffsetDateTime,
/// End time
#[serde(with = "time::serde::timestamp")]
pub end_time: OffsetDateTime,
/// Optional location
#[index]
pub location: Option<String>,
/// Event status
#[index]
pub status: EventStatus,
/// Whether this is an all-day event
pub all_day: bool,
/// Optional category
#[index]
pub category: Option<String>,
}
impl Event {
/// Create a new event
pub fn new(ns: String, title: impl ToString) -> Self {
let now = OffsetDateTime::now_utc();
Self {
base_data: BaseData::with_ns(ns),
title: title.to_string(),
description: None,
start_time: now,
end_time: now,
location: None,
status: EventStatus::default(),
all_day: false,
category: None,
}
}
/// Create an event with specific ID
pub fn with_id(id: String, ns: String, title: impl ToString) -> Self {
let now = OffsetDateTime::now_utc();
let id_u32 = id.parse::<u32>().unwrap_or(0);
Self {
base_data: BaseData::with_id(id_u32, ns),
title: title.to_string(),
description: None,
start_time: now,
end_time: now,
location: None,
status: EventStatus::default(),
all_day: false,
category: None,
}
}
/// Set the description
pub fn set_description(mut self, description: impl ToString) -> Self {
self.description = Some(description.to_string());
self.base_data.update_modified();
self
}
/// Set the start time
pub fn set_start_time(mut self, start_time: OffsetDateTime) -> Self {
self.start_time = start_time;
self.base_data.update_modified();
self
}
/// Set the end time
pub fn set_end_time(mut self, end_time: OffsetDateTime) -> Self {
self.end_time = end_time;
self.base_data.update_modified();
self
}
/// Set the location
pub fn set_location(mut self, location: impl ToString) -> Self {
self.location = Some(location.to_string());
self.base_data.update_modified();
self
}
/// Set the status
pub fn set_status(mut self, status: EventStatus) -> Self {
self.status = status;
self.base_data.update_modified();
self
}
/// Set as all-day event
pub fn set_all_day(mut self, all_day: bool) -> Self {
self.all_day = all_day;
self.base_data.update_modified();
self
}
/// Set the category
pub fn set_category(mut self, category: impl ToString) -> Self {
self.category = Some(category.to_string());
self.base_data.update_modified();
self
}
}
// Object trait implementation is auto-generated by #[derive(DeriveObject)]
// The derive macro generates: object_type(), base_data(), base_data_mut(), index_keys(), indexed_fields()

View File

@@ -0,0 +1,89 @@
use crate::objects::Event;
use rhai::{CustomType, Engine, TypeBuilder, Module, FuncRegistration};
impl CustomType for Event {
fn build(mut builder: TypeBuilder<Self>) {
builder
.with_name("Event")
.with_fn("new", |ns: String, title: String| Event::new(ns, title))
.with_fn("set_description", |event: &mut Event, desc: String| {
event.description = Some(desc);
event.base_data.update_modified();
})
.with_fn("set_location", |event: &mut Event, location: String| {
event.location = Some(location);
event.base_data.update_modified();
})
.with_fn("set_category", |event: &mut Event, category: String| {
event.category = Some(category);
event.base_data.update_modified();
})
.with_fn("set_all_day", |event: &mut Event, all_day: bool| {
event.all_day = all_day;
event.base_data.update_modified();
})
.with_fn("get_id", |event: &mut Event| event.base_data.id.clone())
.with_fn("get_title", |event: &mut Event| event.title.clone())
.with_fn("to_json", |event: &mut Event| {
serde_json::to_string_pretty(event).unwrap_or_default()
});
}
}
/// Register Event API in Rhai engine
pub fn register_event_api(engine: &mut Engine) {
engine.build_type::<Event>();
// Register builder-style constructor (namespace only, like note())
engine.register_fn("event", |ns: String| Event::new(ns, String::new()));
// Register title as a chainable method
engine.register_fn("title", |mut event: Event, title: String| {
event.title = title;
event.base_data.update_modified();
event
});
// Register chainable methods that return Self
engine.register_fn("description", |mut event: Event, desc: String| {
event.description = Some(desc);
event.base_data.update_modified();
event
});
engine.register_fn("location", |mut event: Event, location: String| {
event.location = Some(location);
event.base_data.update_modified();
event
});
engine.register_fn("category", |mut event: Event, category: String| {
event.category = Some(category);
event.base_data.update_modified();
event
});
engine.register_fn("all_day", |mut event: Event, all_day: bool| {
event.all_day = all_day;
event.base_data.update_modified();
event
});
}
/// Register Event functions into a module (for use in packages)
pub fn register_event_functions(module: &mut Module) {
// Register Event type
module.set_custom_type::<Event>("Event");
// Register builder-style constructor
FuncRegistration::new("event")
.set_into_module(module, |ns: String, title: String| Event::new(ns, title));
// Register chainable methods
FuncRegistration::new("description")
.set_into_module(module, |mut event: Event, desc: String| {
event.description = Some(desc);
event.base_data.update_modified();
event
});
}

View File

@@ -0,0 +1,241 @@
/// Flow Instance
///
/// Represents an active instance of a flow template for a specific entity (e.g., user).
use crate::store::{BaseData, Object, Storable};
use serde::{Deserialize, Serialize};
/// Status of a step in a flow instance
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
#[serde(rename_all = "lowercase")]
pub enum StepStatus {
#[default]
Pending,
Active,
Completed,
Skipped,
Failed,
}
/// A step instance in a flow
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct StepInstance {
/// Step name (from template)
pub name: String,
/// Current status
pub status: StepStatus,
/// When step was started
pub started_at: Option<u64>,
/// When step was completed
pub completed_at: Option<u64>,
/// Step result data
#[serde(default)]
pub result: std::collections::HashMap<String, String>,
/// Error message if failed
pub error: Option<String>,
}
impl StepInstance {
pub fn new(name: String) -> Self {
Self {
name,
status: StepStatus::Pending,
started_at: None,
completed_at: None,
result: std::collections::HashMap::new(),
error: None,
}
}
}
/// Overall flow status
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
#[serde(rename_all = "lowercase")]
pub enum FlowStatus {
#[default]
Created,
Running,
Completed,
Failed,
Cancelled,
}
/// Flow Instance - an active execution of a flow template
#[derive(Debug, Clone, Serialize, Deserialize, Default, crate::DeriveObject)]
pub struct FlowInstance {
#[serde(flatten)]
pub base_data: BaseData,
/// Instance name (typically entity_id or unique identifier)
pub name: String,
/// Template name this instance is based on
pub template_name: String,
/// Entity ID this flow is for (e.g., user_id)
pub entity_id: String,
/// Current flow status
pub status: FlowStatus,
/// Step instances
pub steps: Vec<StepInstance>,
/// Current step index
pub current_step: usize,
/// When flow was started
pub started_at: Option<u64>,
/// When flow was completed
pub completed_at: Option<u64>,
/// Instance metadata
#[serde(default)]
pub metadata: std::collections::HashMap<String, String>,
}
impl FlowInstance {
/// Create a new flow instance
pub fn new(id: u32, name: String, template_name: String, entity_id: String) -> Self {
let mut base_data = BaseData::new();
base_data.id = id;
Self {
base_data,
name,
template_name,
entity_id,
status: FlowStatus::Created,
steps: Vec::new(),
current_step: 0,
started_at: None,
completed_at: None,
metadata: std::collections::HashMap::new(),
}
}
/// Initialize steps from template
pub fn init_steps(&mut self, step_names: Vec<String>) {
self.steps = step_names.into_iter().map(StepInstance::new).collect();
self.base_data.update_modified();
}
/// Start the flow
pub fn start(&mut self) {
// Initialize default steps if none exist
if self.steps.is_empty() {
// Create default steps based on common workflow
self.steps = vec![
StepInstance::new("registration".to_string()),
StepInstance::new("kyc".to_string()),
StepInstance::new("email".to_string()),
];
}
self.status = FlowStatus::Running;
self.started_at = Some(Self::now());
// Start first step if exists
if let Some(step) = self.steps.first_mut() {
step.status = StepStatus::Active;
step.started_at = Some(Self::now());
}
self.base_data.update_modified();
}
/// Complete a step by name
pub fn complete_step(&mut self, step_name: &str) -> Result<(), String> {
let step_idx = self.steps.iter().position(|s| s.name == step_name)
.ok_or_else(|| format!("Step '{}' not found", step_name))?;
let step = &mut self.steps[step_idx];
step.status = StepStatus::Completed;
step.completed_at = Some(Self::now());
// Move to next step if this was the current step
if step_idx == self.current_step {
self.current_step += 1;
// Start next step if exists
if let Some(next_step) = self.steps.get_mut(self.current_step) {
next_step.status = StepStatus::Active;
next_step.started_at = Some(Self::now());
} else {
// All steps completed
self.status = FlowStatus::Completed;
self.completed_at = Some(Self::now());
}
}
self.base_data.update_modified();
Ok(())
}
/// Fail a step
pub fn fail_step(&mut self, step_name: &str, error: String) -> Result<(), String> {
let step = self.steps.iter_mut()
.find(|s| s.name == step_name)
.ok_or_else(|| format!("Step '{}' not found", step_name))?;
step.status = StepStatus::Failed;
step.error = Some(error);
step.completed_at = Some(Self::now());
self.status = FlowStatus::Failed;
self.base_data.update_modified();
Ok(())
}
/// Skip a step
pub fn skip_step(&mut self, step_name: &str) -> Result<(), String> {
let step = self.steps.iter_mut()
.find(|s| s.name == step_name)
.ok_or_else(|| format!("Step '{}' not found", step_name))?;
step.status = StepStatus::Skipped;
step.completed_at = Some(Self::now());
self.base_data.update_modified();
Ok(())
}
/// Get current step
pub fn get_current_step(&self) -> Option<&StepInstance> {
self.steps.get(self.current_step)
}
/// Get step by name
pub fn get_step(&self, name: &str) -> Option<&StepInstance> {
self.steps.iter().find(|s| s.name == name)
}
/// Set step result data
pub fn set_step_result(&mut self, step_name: &str, key: String, value: String) -> Result<(), String> {
let step = self.steps.iter_mut()
.find(|s| s.name == step_name)
.ok_or_else(|| format!("Step '{}' not found", step_name))?;
step.result.insert(key, value);
self.base_data.update_modified();
Ok(())
}
/// Add metadata
pub fn add_metadata(&mut self, key: String, value: String) {
self.metadata.insert(key, value);
self.base_data.update_modified();
}
/// Helper to get current timestamp
fn now() -> u64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs()
}
}

View File

@@ -0,0 +1,10 @@
/// Flow Module
///
/// Provides workflow/flow management with templates and instances.
pub mod template;
pub mod instance;
pub mod rhai;
pub use template::{FlowTemplate, FlowStep};
pub use instance::{FlowInstance, FlowStatus, StepStatus, StepInstance};

View File

@@ -0,0 +1,183 @@
/// Rhai bindings for Flow objects
use ::rhai::plugin::*;
use ::rhai::{CustomType, Dynamic, Engine, EvalAltResult, Module, TypeBuilder};
use super::template::{FlowTemplate, FlowStep};
use super::instance::{FlowInstance, FlowStatus, StepStatus};
// ============================================================================
// Flow Template Module
// ============================================================================
type RhaiFlowTemplate = FlowTemplate;
#[export_module]
mod rhai_flow_template_module {
use super::RhaiFlowTemplate;
#[rhai_fn(name = "new_flow", return_raw)]
pub fn new_flow() -> Result<RhaiFlowTemplate, Box<EvalAltResult>> {
Ok(FlowTemplate::new(0))
}
#[rhai_fn(name = "name", return_raw)]
pub fn set_name(
template: &mut RhaiFlowTemplate,
name: String,
) -> Result<RhaiFlowTemplate, Box<EvalAltResult>> {
let owned = std::mem::take(template);
*template = owned.name(name);
Ok(template.clone())
}
#[rhai_fn(name = "description", return_raw)]
pub fn set_description(
template: &mut RhaiFlowTemplate,
description: String,
) -> Result<RhaiFlowTemplate, Box<EvalAltResult>> {
let owned = std::mem::take(template);
*template = owned.description(description);
Ok(template.clone())
}
#[rhai_fn(name = "add_step", return_raw)]
pub fn add_step(
template: &mut RhaiFlowTemplate,
name: String,
description: String,
) -> Result<(), Box<EvalAltResult>> {
template.add_step(name, description);
Ok(())
}
#[rhai_fn(name = "build", return_raw)]
pub fn build(
template: &mut RhaiFlowTemplate,
) -> Result<RhaiFlowTemplate, Box<EvalAltResult>> {
Ok(template.clone())
}
// Getters
#[rhai_fn(name = "get_name")]
pub fn get_name(template: &mut RhaiFlowTemplate) -> String {
template.name.clone()
}
#[rhai_fn(name = "get_description")]
pub fn get_description(template: &mut RhaiFlowTemplate) -> String {
template.description.clone()
}
}
// ============================================================================
// Flow Instance Module
// ============================================================================
type RhaiFlowInstance = FlowInstance;
#[export_module]
mod rhai_flow_instance_module {
use super::RhaiFlowInstance;
#[rhai_fn(name = "new_flow_instance", return_raw)]
pub fn new_instance(
name: String,
template_name: String,
entity_id: String,
) -> Result<RhaiFlowInstance, Box<EvalAltResult>> {
Ok(FlowInstance::new(0, name, template_name, entity_id))
}
#[rhai_fn(name = "start", return_raw)]
pub fn start(
instance: &mut RhaiFlowInstance,
) -> Result<(), Box<EvalAltResult>> {
instance.start();
Ok(())
}
#[rhai_fn(name = "complete_step", return_raw)]
pub fn complete_step(
instance: &mut RhaiFlowInstance,
step_name: String,
) -> Result<(), Box<EvalAltResult>> {
instance.complete_step(&step_name)
.map_err(|e| e.into())
}
#[rhai_fn(name = "fail_step", return_raw)]
pub fn fail_step(
instance: &mut RhaiFlowInstance,
step_name: String,
error: String,
) -> Result<(), Box<EvalAltResult>> {
instance.fail_step(&step_name, error)
.map_err(|e| e.into())
}
#[rhai_fn(name = "skip_step", return_raw)]
pub fn skip_step(
instance: &mut RhaiFlowInstance,
step_name: String,
) -> Result<(), Box<EvalAltResult>> {
instance.skip_step(&step_name)
.map_err(|e| e.into())
}
// Getters
#[rhai_fn(name = "get_name")]
pub fn get_name(instance: &mut RhaiFlowInstance) -> String {
instance.name.clone()
}
#[rhai_fn(name = "get_template_name")]
pub fn get_template_name(instance: &mut RhaiFlowInstance) -> String {
instance.template_name.clone()
}
#[rhai_fn(name = "get_entity_id")]
pub fn get_entity_id(instance: &mut RhaiFlowInstance) -> String {
instance.entity_id.clone()
}
#[rhai_fn(name = "get_status")]
pub fn get_status(instance: &mut RhaiFlowInstance) -> String {
format!("{:?}", instance.status)
}
}
// ============================================================================
// Registration Functions
// ============================================================================
/// Register Flow modules into a Rhai Module (for use in packages)
pub fn register_flow_modules(parent_module: &mut Module) {
// Register custom types
parent_module.set_custom_type::<FlowTemplate>("FlowTemplate");
parent_module.set_custom_type::<FlowInstance>("FlowInstance");
// Merge flow template functions
let template_module = exported_module!(rhai_flow_template_module);
parent_module.merge(&template_module);
// Merge flow instance functions
let instance_module = exported_module!(rhai_flow_instance_module);
parent_module.merge(&instance_module);
}
// ============================================================================
// CustomType Implementations
// ============================================================================
impl CustomType for FlowTemplate {
fn build(mut builder: TypeBuilder<Self>) {
builder.with_name("FlowTemplate");
}
}
impl CustomType for FlowInstance {
fn build(mut builder: TypeBuilder<Self>) {
builder.with_name("FlowInstance");
}
}

View File

@@ -0,0 +1,117 @@
/// Flow Template
///
/// Defines a reusable workflow template with steps that can be instantiated multiple times.
use crate::store::{BaseData, Object, Storable};
use serde::{Deserialize, Serialize};
/// A step in a flow template
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct FlowStep {
/// Step name/identifier
pub name: String,
/// Step description
pub description: String,
/// Steps that must be completed before this step can start
#[serde(default)]
pub dependencies: Vec<String>,
}
impl FlowStep {
pub fn new(name: String, description: String) -> Self {
Self {
name,
description,
dependencies: Vec::new(),
}
}
pub fn with_dependencies(mut self, dependencies: Vec<String>) -> Self {
self.dependencies = dependencies;
self
}
pub fn add_dependency(&mut self, dependency: String) {
self.dependencies.push(dependency);
}
}
/// Flow Template - defines a reusable workflow
#[derive(Debug, Clone, Serialize, Deserialize, Default, crate::DeriveObject)]
pub struct FlowTemplate {
#[serde(flatten)]
pub base_data: BaseData,
/// Template name
pub name: String,
/// Template description
pub description: String,
/// Ordered list of steps
pub steps: Vec<FlowStep>,
/// Template metadata
#[serde(default)]
pub metadata: std::collections::HashMap<String, String>,
}
impl FlowTemplate {
/// Create a new flow template
pub fn new(id: u32) -> Self {
let mut base_data = BaseData::new();
base_data.id = id;
Self {
base_data,
name: String::new(),
description: String::new(),
steps: Vec::new(),
metadata: std::collections::HashMap::new(),
}
}
/// Builder: Set name
pub fn name(mut self, name: String) -> Self {
self.name = name;
self.base_data.update_modified();
self
}
/// Builder: Set description
pub fn description(mut self, description: String) -> Self {
self.description = description;
self.base_data.update_modified();
self
}
/// Add a step to the template
pub fn add_step(&mut self, name: String, description: String) {
self.steps.push(FlowStep::new(name, description));
self.base_data.update_modified();
}
/// Add a step with dependencies
pub fn add_step_with_dependencies(&mut self, name: String, description: String, dependencies: Vec<String>) {
let step = FlowStep::new(name, description).with_dependencies(dependencies);
self.steps.push(step);
self.base_data.update_modified();
}
/// Get step by name
pub fn get_step(&self, name: &str) -> Option<&FlowStep> {
self.steps.iter().find(|s| s.name == name)
}
/// Add metadata
pub fn add_metadata(&mut self, key: String, value: String) {
self.metadata.insert(key, value);
self.base_data.update_modified();
}
/// Build (for fluent API compatibility)
pub fn build(self) -> Self {
self
}
}

View File

@@ -0,0 +1,126 @@
use crate::store::BaseData;
use rhai::{CustomType, TypeBuilder};
use serde::{Deserialize, Serialize};
/// Bid status enumeration
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
pub enum BidStatus {
#[default]
Pending,
Confirmed,
Assigned,
Cancelled,
Done,
}
/// Billing period enumeration
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
pub enum BillingPeriod {
#[default]
Hourly,
Monthly,
Yearly,
Biannually,
Triannually,
}
/// I can bid for infra, and optionally get accepted
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default, crate::DeriveObject)]
pub struct Bid {
pub base_data: BaseData,
/// links back to customer for this capacity (user on ledger)
#[index]
pub customer_id: u32,
/// nr of slices I need in 1 machine
pub compute_slices_nr: i32,
/// price per 1 GB slice I want to accept
pub compute_slice_price: f64,
/// nr of storage slices needed
pub storage_slices_nr: i32,
/// price per 1 GB storage slice I want to accept
pub storage_slice_price: f64,
pub status: BidStatus,
/// if obligation then will be charged and money needs to be in escrow, otherwise its an intent
pub obligation: bool,
/// epoch timestamp
pub start_date: u32,
/// epoch timestamp
pub end_date: u32,
/// signature as done by a user/consumer to validate their identity and intent
pub signature_user: String,
pub billing_period: BillingPeriod,
}
impl Bid {
pub fn new() -> Self {
Self {
base_data: BaseData::new(),
customer_id: 0,
compute_slices_nr: 0,
compute_slice_price: 0.0,
storage_slices_nr: 0,
storage_slice_price: 0.0,
status: BidStatus::default(),
obligation: false,
start_date: 0,
end_date: 0,
signature_user: String::new(),
billing_period: BillingPeriod::default(),
}
}
pub fn customer_id(mut self, v: u32) -> Self {
self.customer_id = v;
self
}
pub fn compute_slices_nr(mut self, v: i32) -> Self {
self.compute_slices_nr = v;
self
}
pub fn compute_slice_price(mut self, v: f64) -> Self {
self.compute_slice_price = v;
self
}
pub fn storage_slices_nr(mut self, v: i32) -> Self {
self.storage_slices_nr = v;
self
}
pub fn storage_slice_price(mut self, v: f64) -> Self {
self.storage_slice_price = v;
self
}
pub fn status(mut self, v: BidStatus) -> Self {
self.status = v;
self
}
pub fn obligation(mut self, v: bool) -> Self {
self.obligation = v;
self
}
pub fn start_date(mut self, v: u32) -> Self {
self.start_date = v;
self
}
pub fn end_date(mut self, v: u32) -> Self {
self.end_date = v;
self
}
pub fn signature_user(mut self, v: impl ToString) -> Self {
self.signature_user = v.to_string();
self
}
pub fn billing_period(mut self, v: BillingPeriod) -> Self {
self.billing_period = v;
self
}
}

View File

@@ -0,0 +1,39 @@
use rhai::{CustomType, TypeBuilder};
use serde::{Deserialize, Serialize};
/// SLA policy matching the V spec `SLAPolicy`
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
pub struct SLAPolicy {
/// should +90
pub sla_uptime: i32,
/// minimal mbits we can expect avg over 1h per node, 0 means we don't guarantee
pub sla_bandwidth_mbit: i32,
/// 0-100, percent of money given back in relation to month if sla breached,
/// e.g. 200 means we return 2 months worth of rev if sla missed
pub sla_penalty: i32,
}
impl SLAPolicy {
pub fn new() -> Self { Self::default() }
pub fn sla_uptime(mut self, v: i32) -> Self { self.sla_uptime = v; self }
pub fn sla_bandwidth_mbit(mut self, v: i32) -> Self { self.sla_bandwidth_mbit = v; self }
pub fn sla_penalty(mut self, v: i32) -> Self { self.sla_penalty = v; self }
pub fn build(self) -> Self { self }
}
/// Pricing policy matching the V spec `PricingPolicy`
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
pub struct PricingPolicy {
/// e.g. 30,40,50 means if user has more CC in wallet than 1 year utilization
/// then this provider gives 30%, 2Y 40%, ...
pub marketplace_year_discounts: Vec<i32>,
/// e.g. 10,20,30
pub volume_discounts: Vec<i32>,
}
impl PricingPolicy {
pub fn new() -> Self { Self { marketplace_year_discounts: vec![30, 40, 50], volume_discounts: vec![10, 20, 30] } }
pub fn marketplace_year_discounts(mut self, v: Vec<i32>) -> Self { self.marketplace_year_discounts = v; self }
pub fn volume_discounts(mut self, v: Vec<i32>) -> Self { self.volume_discounts = v; self }
pub fn build(self) -> Self { self }
}

View File

@@ -0,0 +1,217 @@
use crate::store::BaseData;
use rhai::{CustomType, TypeBuilder};
use serde::{Deserialize, Serialize};
use super::bid::BillingPeriod;
/// Contract status enumeration
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
pub enum ContractStatus {
#[default]
Active,
Cancelled,
Error,
Paused,
}
/// Compute slice provisioned for a contract
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
pub struct ComputeSliceProvisioned {
pub node_id: u32,
/// the id of the slice in the node
pub id: u16,
pub mem_gb: f64,
pub storage_gb: f64,
pub passmark: i32,
pub vcores: i32,
pub cpu_oversubscription: i32,
pub tags: String,
}
/// Storage slice provisioned for a contract
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
pub struct StorageSliceProvisioned {
pub node_id: u32,
/// the id of the slice in the node, are tracked in the node itself
pub id: u16,
pub storage_size_gb: i32,
pub tags: String,
}
/// Contract for provisioned infrastructure
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default, crate::DeriveObject)]
pub struct Contract {
pub base_data: BaseData,
/// links back to customer for this capacity (user on ledger)
#[index]
pub customer_id: u32,
pub compute_slices: Vec<ComputeSliceProvisioned>,
pub storage_slices: Vec<StorageSliceProvisioned>,
/// price per 1 GB agreed upon
pub compute_slice_price: f64,
/// price per 1 GB agreed upon
pub storage_slice_price: f64,
/// price per 1 GB agreed upon (transfer)
pub network_slice_price: f64,
pub status: ContractStatus,
/// epoch timestamp
pub start_date: u32,
/// epoch timestamp
pub end_date: u32,
/// signature as done by a user/consumer to validate their identity and intent
pub signature_user: String,
/// signature as done by the hoster
pub signature_hoster: String,
pub billing_period: BillingPeriod,
}
impl Contract {
pub fn new() -> Self {
Self {
base_data: BaseData::new(),
customer_id: 0,
compute_slices: Vec::new(),
storage_slices: Vec::new(),
compute_slice_price: 0.0,
storage_slice_price: 0.0,
network_slice_price: 0.0,
status: ContractStatus::default(),
start_date: 0,
end_date: 0,
signature_user: String::new(),
signature_hoster: String::new(),
billing_period: BillingPeriod::default(),
}
}
pub fn customer_id(mut self, v: u32) -> Self {
self.customer_id = v;
self
}
pub fn add_compute_slice(mut self, slice: ComputeSliceProvisioned) -> Self {
self.compute_slices.push(slice);
self
}
pub fn add_storage_slice(mut self, slice: StorageSliceProvisioned) -> Self {
self.storage_slices.push(slice);
self
}
pub fn compute_slice_price(mut self, v: f64) -> Self {
self.compute_slice_price = v;
self
}
pub fn storage_slice_price(mut self, v: f64) -> Self {
self.storage_slice_price = v;
self
}
pub fn network_slice_price(mut self, v: f64) -> Self {
self.network_slice_price = v;
self
}
pub fn status(mut self, v: ContractStatus) -> Self {
self.status = v;
self
}
pub fn start_date(mut self, v: u32) -> Self {
self.start_date = v;
self
}
pub fn end_date(mut self, v: u32) -> Self {
self.end_date = v;
self
}
pub fn signature_user(mut self, v: impl ToString) -> Self {
self.signature_user = v.to_string();
self
}
pub fn signature_hoster(mut self, v: impl ToString) -> Self {
self.signature_hoster = v.to_string();
self
}
pub fn billing_period(mut self, v: BillingPeriod) -> Self {
self.billing_period = v;
self
}
}
impl ComputeSliceProvisioned {
pub fn new() -> Self {
Self::default()
}
pub fn node_id(mut self, v: u32) -> Self {
self.node_id = v;
self
}
pub fn id(mut self, v: u16) -> Self {
self.id = v;
self
}
pub fn mem_gb(mut self, v: f64) -> Self {
self.mem_gb = v;
self
}
pub fn storage_gb(mut self, v: f64) -> Self {
self.storage_gb = v;
self
}
pub fn passmark(mut self, v: i32) -> Self {
self.passmark = v;
self
}
pub fn vcores(mut self, v: i32) -> Self {
self.vcores = v;
self
}
pub fn cpu_oversubscription(mut self, v: i32) -> Self {
self.cpu_oversubscription = v;
self
}
pub fn tags(mut self, v: impl ToString) -> Self {
self.tags = v.to_string();
self
}
}
impl StorageSliceProvisioned {
pub fn new() -> Self {
Self::default()
}
pub fn node_id(mut self, v: u32) -> Self {
self.node_id = v;
self
}
pub fn id(mut self, v: u16) -> Self {
self.id = v;
self
}
pub fn storage_size_gb(mut self, v: i32) -> Self {
self.storage_size_gb = v;
self
}
pub fn tags(mut self, v: impl ToString) -> Self {
self.tags = v.to_string();
self
}
}

View File

@@ -0,0 +1,18 @@
pub mod bid;
pub mod common;
pub mod contract;
pub mod node;
pub mod nodegroup;
pub mod reputation;
pub mod reservation;
pub use bid::{Bid, BidStatus, BillingPeriod};
pub use common::{PricingPolicy, SLAPolicy};
pub use contract::{Contract, ContractStatus, ComputeSliceProvisioned, StorageSliceProvisioned};
pub use node::{
CPUDevice, ComputeSlice, DeviceInfo, GPUDevice, MemoryDevice, NetworkDevice, Node,
NodeCapacity, StorageDevice, StorageSlice,
};
pub use nodegroup::NodeGroup;
pub use reputation::{NodeGroupReputation, NodeReputation};
pub use reservation::{Reservation, ReservationStatus};

View File

@@ -0,0 +1,279 @@
use crate::store::BaseData;
use rhai::{CustomType, TypeBuilder};
use serde::{Deserialize, Serialize};
use super::common::{PricingPolicy, SLAPolicy};
/// Storage device information
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
pub struct StorageDevice {
/// can be used in node
pub id: String,
/// Size of the storage device in gigabytes
pub size_gb: f64,
/// Description of the storage device
pub description: String,
}
/// Memory device information
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
pub struct MemoryDevice {
/// can be used in node
pub id: String,
/// Size of the memory device in gigabytes
pub size_gb: f64,
/// Description of the memory device
pub description: String,
}
/// CPU device information
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
pub struct CPUDevice {
/// can be used in node
pub id: String,
/// Number of CPU cores
pub cores: i32,
/// Passmark score
pub passmark: i32,
/// Description of the CPU
pub description: String,
/// Brand of the CPU
pub cpu_brand: String,
/// Version of the CPU
pub cpu_version: String,
}
/// GPU device information
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
pub struct GPUDevice {
/// can be used in node
pub id: String,
/// Number of GPU cores
pub cores: i32,
/// Size of the GPU memory in gigabytes
pub memory_gb: f64,
/// Description of the GPU
pub description: String,
pub gpu_brand: String,
pub gpu_version: String,
}
/// Network device information
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
pub struct NetworkDevice {
/// can be used in node
pub id: String,
/// Network speed in Mbps
pub speed_mbps: i32,
/// Description of the network device
pub description: String,
}
/// Aggregated device info for a node
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
pub struct DeviceInfo {
pub vendor: String,
pub storage: Vec<StorageDevice>,
pub memory: Vec<MemoryDevice>,
pub cpu: Vec<CPUDevice>,
pub gpu: Vec<GPUDevice>,
pub network: Vec<NetworkDevice>,
}
/// NodeCapacity represents the hardware capacity details of a node.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
pub struct NodeCapacity {
/// Total storage in gigabytes
pub storage_gb: f64,
/// Total memory in gigabytes
pub mem_gb: f64,
/// Total GPU memory in gigabytes
pub mem_gb_gpu: f64,
/// Passmark score for the node
pub passmark: i32,
/// Total virtual cores
pub vcores: i32,
}
// PricingPolicy and SLAPolicy moved to `common.rs` to be shared across models.
/// Compute slice (typically represents a base unit of compute)
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
pub struct ComputeSlice {
/// the id of the slice in the node
pub id: u16,
pub mem_gb: f64,
pub storage_gb: f64,
pub passmark: i32,
pub vcores: i32,
pub cpu_oversubscription: i32,
pub storage_oversubscription: i32,
/// nr of GPU's see node to know what GPU's are
pub gpus: u8,
}
impl ComputeSlice {
pub fn new() -> Self {
Self {
id: 0,
mem_gb: 0.0,
storage_gb: 0.0,
passmark: 0,
vcores: 0,
cpu_oversubscription: 0,
storage_oversubscription: 0,
gpus: 0,
}
}
pub fn id(mut self, id: u16) -> Self {
self.id = id;
self
}
pub fn mem_gb(mut self, v: f64) -> Self {
self.mem_gb = v;
self
}
pub fn storage_gb(mut self, v: f64) -> Self {
self.storage_gb = v;
self
}
pub fn passmark(mut self, v: i32) -> Self {
self.passmark = v;
self
}
pub fn vcores(mut self, v: i32) -> Self {
self.vcores = v;
self
}
pub fn cpu_oversubscription(mut self, v: i32) -> Self {
self.cpu_oversubscription = v;
self
}
pub fn storage_oversubscription(mut self, v: i32) -> Self {
self.storage_oversubscription = v;
self
}
pub fn gpus(mut self, v: u8) -> Self {
self.gpus = v;
self
}
}
/// Storage slice (typically 1GB of storage)
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
pub struct StorageSlice {
/// the id of the slice in the node, are tracked in the node itself
pub id: u16,
}
impl StorageSlice {
pub fn new() -> Self {
Self {
id: 0,
}
}
pub fn id(mut self, id: u16) -> Self {
self.id = id;
self
}
}
/// Grid4 Node model
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default, crate::DeriveObject)]
pub struct Node {
pub base_data: BaseData,
/// Link to node group
#[index]
pub nodegroupid: i32,
/// Uptime percentage 0..100
pub uptime: i32,
pub computeslices: Vec<ComputeSlice>,
pub storageslices: Vec<StorageSlice>,
pub devices: DeviceInfo,
/// 2 letter code as specified in lib/data/countries/data/countryInfo.txt
#[index]
pub country: String,
/// Hardware capacity details
pub capacity: NodeCapacity,
/// first time node was active
pub birthtime: u32,
/// node public key
#[index]
pub pubkey: String,
/// signature done on node to validate pubkey with privkey
pub signature_node: String,
/// signature as done by farmers to validate their identity
pub signature_farmer: String,
}
impl Node {
pub fn new() -> Self {
Self {
base_data: BaseData::new(),
nodegroupid: 0,
uptime: 0,
computeslices: Vec::new(),
storageslices: Vec::new(),
devices: DeviceInfo::default(),
country: String::new(),
capacity: NodeCapacity::default(),
birthtime: 0,
pubkey: String::new(),
signature_node: String::new(),
signature_farmer: String::new(),
}
}
pub fn nodegroupid(mut self, v: i32) -> Self {
self.nodegroupid = v;
self
}
pub fn uptime(mut self, v: i32) -> Self {
self.uptime = v;
self
}
pub fn add_compute_slice(mut self, s: ComputeSlice) -> Self {
self.computeslices.push(s);
self
}
pub fn add_storage_slice(mut self, s: StorageSlice) -> Self {
self.storageslices.push(s);
self
}
pub fn devices(mut self, d: DeviceInfo) -> Self {
self.devices = d;
self
}
pub fn country(mut self, c: impl ToString) -> Self {
self.country = c.to_string();
self
}
pub fn capacity(mut self, c: NodeCapacity) -> Self {
self.capacity = c;
self
}
pub fn birthtime(mut self, t: u32) -> Self {
self.birthtime = t;
self
}
pub fn pubkey(mut self, v: impl ToString) -> Self {
self.pubkey = v.to_string();
self
}
pub fn signature_node(mut self, v: impl ToString) -> Self {
self.signature_node = v.to_string();
self
}
pub fn signature_farmer(mut self, v: impl ToString) -> Self {
self.signature_farmer = v.to_string();
self
}
/// Placeholder for capacity recalculation out of the devices on the Node
pub fn check(self) -> Self {
// TODO: calculate NodeCapacity out of the devices on the Node
self
}
}

View File

@@ -0,0 +1,50 @@
use crate::store::BaseData;
use rhai::{CustomType, TypeBuilder};
use serde::{Deserialize, Serialize};
use super::common::{PricingPolicy, SLAPolicy};
/// Grid4 NodeGroup model (root object for farmer configuration)
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default, crate::DeriveObject)]
pub struct NodeGroup {
pub base_data: BaseData,
/// link back to farmer who owns the nodegroup, is a user?
#[index]
pub farmerid: u32,
/// only visible by farmer, in future encrypted, used to boot a node
pub secret: String,
pub description: String,
pub slapolicy: SLAPolicy,
pub pricingpolicy: PricingPolicy,
/// pricing in CC - cloud credit, per 2GB node slice
pub compute_slice_normalized_pricing_cc: f64,
/// pricing in CC - cloud credit, per 1GB storage slice
pub storage_slice_normalized_pricing_cc: f64,
/// signature as done by farmers to validate that they created this group
pub signature_farmer: String,
}
impl NodeGroup {
pub fn new() -> Self {
Self {
base_data: BaseData::new(),
farmerid: 0,
secret: String::new(),
description: String::new(),
slapolicy: SLAPolicy::default(),
pricingpolicy: PricingPolicy::new(),
compute_slice_normalized_pricing_cc: 0.0,
storage_slice_normalized_pricing_cc: 0.0,
signature_farmer: String::new(),
}
}
pub fn farmerid(mut self, v: u32) -> Self { self.farmerid = v; self }
pub fn secret(mut self, v: impl ToString) -> Self { self.secret = v.to_string(); self }
pub fn description(mut self, v: impl ToString) -> Self { self.description = v.to_string(); self }
pub fn slapolicy(mut self, v: SLAPolicy) -> Self { self.slapolicy = v; self }
pub fn pricingpolicy(mut self, v: PricingPolicy) -> Self { self.pricingpolicy = v; self }
pub fn compute_slice_normalized_pricing_cc(mut self, v: f64) -> Self { self.compute_slice_normalized_pricing_cc = v; self }
pub fn storage_slice_normalized_pricing_cc(mut self, v: f64) -> Self { self.storage_slice_normalized_pricing_cc = v; self }
pub fn signature_farmer(mut self, v: impl ToString) -> Self { self.signature_farmer = v.to_string(); self }
}

View File

@@ -0,0 +1,83 @@
use crate::store::BaseData;
use rhai::{CustomType, TypeBuilder};
use serde::{Deserialize, Serialize};
/// Node reputation information
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
pub struct NodeReputation {
pub node_id: u32,
/// between 0 and 100, earned over time
pub reputation: i32,
/// between 0 and 100, set by system, farmer has no ability to set this
pub uptime: i32,
}
/// NodeGroup reputation model
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default, crate::DeriveObject)]
pub struct NodeGroupReputation {
pub base_data: BaseData,
#[index]
pub nodegroup_id: u32,
/// between 0 and 100, earned over time
pub reputation: i32,
/// between 0 and 100, set by system, farmer has no ability to set this
pub uptime: i32,
pub nodes: Vec<NodeReputation>,
}
impl NodeGroupReputation {
pub fn new() -> Self {
Self {
base_data: BaseData::new(),
nodegroup_id: 0,
reputation: 50, // default as per spec
uptime: 0,
nodes: Vec::new(),
}
}
pub fn nodegroup_id(mut self, v: u32) -> Self {
self.nodegroup_id = v;
self
}
pub fn reputation(mut self, v: i32) -> Self {
self.reputation = v;
self
}
pub fn uptime(mut self, v: i32) -> Self {
self.uptime = v;
self
}
pub fn add_node_reputation(mut self, node_rep: NodeReputation) -> Self {
self.nodes.push(node_rep);
self
}
}
impl NodeReputation {
pub fn new() -> Self {
Self {
node_id: 0,
reputation: 50, // default as per spec
uptime: 0,
}
}
pub fn node_id(mut self, v: u32) -> Self {
self.node_id = v;
self
}
pub fn reputation(mut self, v: i32) -> Self {
self.reputation = v;
self
}
pub fn uptime(mut self, v: i32) -> Self {
self.uptime = v;
self
}
}

View File

@@ -0,0 +1,56 @@
use crate::store::BaseData;
use rhai::{CustomType, TypeBuilder};
use serde::{Deserialize, Serialize};
/// Reservation status as per V spec
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
pub enum ReservationStatus {
#[default]
Pending,
Confirmed,
Assigned,
Cancelled,
Done,
}
/// Grid4 Reservation model
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default, crate::DeriveObject)]
pub struct Reservation {
pub base_data: BaseData,
/// links back to customer for this capacity
#[index]
pub customer_id: u32,
pub compute_slices: Vec<u32>,
pub storage_slices: Vec<u32>,
pub status: ReservationStatus,
/// if obligation then will be charged and money needs to be in escrow, otherwise its an intent
pub obligation: bool,
/// epoch
pub start_date: u32,
pub end_date: u32,
}
impl Reservation {
pub fn new() -> Self {
Self {
base_data: BaseData::new(),
customer_id: 0,
compute_slices: Vec::new(),
storage_slices: Vec::new(),
status: ReservationStatus::Pending,
obligation: false,
start_date: 0,
end_date: 0,
}
}
pub fn customer_id(mut self, v: u32) -> Self { self.customer_id = v; self }
pub fn add_compute_slice(mut self, id: u32) -> Self { self.compute_slices.push(id); self }
pub fn compute_slices(mut self, v: Vec<u32>) -> Self { self.compute_slices = v; self }
pub fn add_storage_slice(mut self, id: u32) -> Self { self.storage_slices.push(id); self }
pub fn storage_slices(mut self, v: Vec<u32>) -> Self { self.storage_slices = v; self }
pub fn status(mut self, v: ReservationStatus) -> Self { self.status = v; self }
pub fn obligation(mut self, v: bool) -> Self { self.obligation = v; self }
pub fn start_date(mut self, v: u32) -> Self { self.start_date = v; self }
pub fn end_date(mut self, v: u32) -> Self { self.end_date = v; self }
}

View File

@@ -0,0 +1,194 @@
# Grid4 Data Model
This module defines data models for nodes, groups, and slices in a cloud/grid infrastructure. Each root object is marked with `@[heap]` and can be indexed for efficient querying.
## Root Objects Overview
| Object | Description | Index Fields |
| ----------- | --------------------------------------------- | ------------------------------ |
| `Node` | Represents a single node in the grid | `id`, `nodegroupid`, `country` |
| `NodeGroup` | Represents a group of nodes owned by a farmer | `id`, `farmerid` |
---
## Node
Represents a single node in the grid with slices, devices, and capacity.
| Field | Type | Description | Indexed |
| --------------- | ---------------- | -------------------------------------------- | ------- |
| `id` | `int` | Unique node ID | ✅ |
| `nodegroupid` | `int` | ID of the owning node group | ✅ |
| `uptime` | `int` | Uptime percentage (0-100) | ✅ |
| `computeslices` | `[]ComputeSlice` | List of compute slices | ❌ |
| `storageslices` | `[]StorageSlice` | List of storage slices | ❌ |
| `devices` | `DeviceInfo` | Hardware device info (storage, memory, etc.) | ❌ |
| `country` | `string` | 2-letter country code | ✅ |
| `capacity` | `NodeCapacity` | Aggregated hardware capacity | ❌ |
| `provisiontime` | `u32` | Provisioning time (simple/compatible format) | ✅ |
---
## NodeGroup
Represents a group of nodes owned by a farmer, with policies.
| Field | Type | Description | Indexed |
| ------------------------------------- | --------------- | ---------------------------------------------- | ------- |
| `id` | `u32` | Unique group ID | ✅ |
| `farmerid` | `u32` | Farmer/user ID | ✅ |
| `secret` | `string` | Encrypted secret for booting nodes | ❌ |
| `description` | `string` | Group description | ❌ |
| `slapolicy` | `SLAPolicy` | SLA policy details | ❌ |
| `pricingpolicy` | `PricingPolicy` | Pricing policy details | ❌ |
| `compute_slice_normalized_pricing_cc` | `f64` | Pricing per 2GB compute slice in cloud credits | ❌ |
| `storage_slice_normalized_pricing_cc` | `f64` | Pricing per 1GB storage slice in cloud credits | ❌ |
| `reputation` | `int` | Reputation (0-100) | ✅ |
| `uptime` | `int` | Uptime (0-100) | ✅ |
---
## ComputeSlice
Represents a compute slice (e.g., 1GB memory unit).
| Field | Type | Description |
| -------------------------- | --------------- | -------------------------------- |
| `nodeid` | `u32` | Owning node ID |
| `id` | `int` | Slice ID in node |
| `mem_gb` | `f64` | Memory in GB |
| `storage_gb` | `f64` | Storage in GB |
| `passmark` | `int` | Passmark score |
| `vcores` | `int` | Virtual cores |
| `cpu_oversubscription` | `int` | CPU oversubscription ratio |
| `storage_oversubscription` | `int` | Storage oversubscription ratio |
| `price_range` | `[]f64` | Price range [min, max] |
| `gpus` | `u8` | Number of GPUs |
| `price_cc` | `f64` | Price per slice in cloud credits |
| `pricing_policy` | `PricingPolicy` | Pricing policy |
| `sla_policy` | `SLAPolicy` | SLA policy |
---
## StorageSlice
Represents a 1GB storage slice.
| Field | Type | Description |
| ---------------- | --------------- | -------------------------------- |
| `nodeid` | `u32` | Owning node ID |
| `id` | `int` | Slice ID in node |
| `price_cc` | `f64` | Price per slice in cloud credits |
| `pricing_policy` | `PricingPolicy` | Pricing policy |
| `sla_policy` | `SLAPolicy` | SLA policy |
---
## DeviceInfo
Hardware device information for a node.
| Field | Type | Description |
| --------- | ----------------- | ----------------------- |
| `vendor` | `string` | Vendor of the node |
| `storage` | `[]StorageDevice` | List of storage devices |
| `memory` | `[]MemoryDevice` | List of memory devices |
| `cpu` | `[]CPUDevice` | List of CPU devices |
| `gpu` | `[]GPUDevice` | List of GPU devices |
| `network` | `[]NetworkDevice` | List of network devices |
---
## StorageDevice
| Field | Type | Description |
| ------------- | -------- | --------------------- |
| `id` | `string` | Unique ID for device |
| `size_gb` | `f64` | Size in GB |
| `description` | `string` | Description of device |
---
## MemoryDevice
| Field | Type | Description |
| ------------- | -------- | --------------------- |
| `id` | `string` | Unique ID for device |
| `size_gb` | `f64` | Size in GB |
| `description` | `string` | Description of device |
---
## CPUDevice
| Field | Type | Description |
| ------------- | -------- | ------------------------ |
| `id` | `string` | Unique ID for device |
| `cores` | `int` | Number of CPU cores |
| `passmark` | `int` | Passmark benchmark score |
| `description` | `string` | Description of device |
| `cpu_brand` | `string` | Brand of the CPU |
| `cpu_version` | `string` | Version of the CPU |
---
## GPUDevice
| Field | Type | Description |
| ------------- | -------- | --------------------- |
| `id` | `string` | Unique ID for device |
| `cores` | `int` | Number of GPU cores |
| `memory_gb` | `f64` | GPU memory in GB |
| `description` | `string` | Description of device |
| `gpu_brand` | `string` | Brand of the GPU |
| `gpu_version` | `string` | Version of the GPU |
---
## NetworkDevice
| Field | Type | Description |
| ------------- | -------- | --------------------- |
| `id` | `string` | Unique ID for device |
| `speed_mbps` | `int` | Network speed in Mbps |
| `description` | `string` | Description of device |
---
## NodeCapacity
Aggregated hardware capacity for a node.
| Field | Type | Description |
| ------------ | ----- | ---------------------- |
| `storage_gb` | `f64` | Total storage in GB |
| `mem_gb` | `f64` | Total memory in GB |
| `mem_gb_gpu` | `f64` | Total GPU memory in GB |
| `passmark` | `int` | Total passmark score |
| `vcores` | `int` | Total virtual cores |
---
## SLAPolicy
Service Level Agreement policy for slices or node groups.
| Field | Type | Description |
| -------------------- | ----- | --------------------------------------- |
| `sla_uptime` | `int` | Required uptime % (e.g., 90) |
| `sla_bandwidth_mbit` | `int` | Guaranteed bandwidth in Mbps (0 = none) |
| `sla_penalty` | `int` | Penalty % if SLA is breached (0-100) |
---
## PricingPolicy
Pricing policy for slices or node groups.
| Field | Type | Description |
| ---------------------------- | ------- | --------------------------------------------------------- |
| `marketplace_year_discounts` | `[]int` | Discounts for 1Y, 2Y, 3Y prepaid usage (e.g. [30,40,50]) |
| `volume_discounts` | `[]int` | Volume discounts based on purchase size (e.g. [10,20,30]) |

View File

@@ -0,0 +1,37 @@
module datamodel
// I can bid for infra, and optionally get accepted
@[heap]
pub struct Bid {
pub mut:
id u32
customer_id u32 // links back to customer for this capacity (user on ledger)
compute_slices_nr int // nr of slices I need in 1 machine
compute_slice_price f64 // price per 1 GB slice I want to accept
storage_slices_nr int
storage_slice_price f64 // price per 1 GB storage slice I want to accept
storage_slices_nr int
status BidStatus
obligation bool // if obligation then will be charged and money needs to be in escrow, otherwise its an intent
start_date u32 // epoch
end_date u32
signature_user string // signature as done by a user/consumer to validate their identity and intent
billing_period BillingPeriod
}
pub enum BidStatus {
pending
confirmed
assigned
cancelled
done
}
pub enum BillingPeriod {
hourly
monthly
yearly
biannually
triannually
}

View File

@@ -0,0 +1,52 @@
module datamodel
// I can bid for infra, and optionally get accepted
@[heap]
pub struct Contract {
pub mut:
id u32
customer_id u32 // links back to customer for this capacity (user on ledger)
compute_slices []ComputeSliceProvisioned
storage_slices []StorageSliceProvisioned
compute_slice_price f64 // price per 1 GB agreed upon
storage_slice_price f64 // price per 1 GB agreed upon
network_slice_price f64 // price per 1 GB agreed upon (transfer)
status ContractStatus
start_date u32 // epoch
end_date u32
signature_user string // signature as done by a user/consumer to validate their identity and intent
signature_hoster string // signature as done by the hoster
billing_period BillingPeriod
}
pub enum ConctractStatus {
active
cancelled
error
paused
}
// typically 1GB of memory, but can be adjusted based based on size of machine
pub struct ComputeSliceProvisioned {
pub mut:
node_id u32
id u16 // the id of the slice in the node
mem_gb f64
storage_gb f64
passmark int
vcores int
cpu_oversubscription int
tags string
}
// 1GB of storage
pub struct StorageSliceProvisioned {
pub mut:
node_id u32
id u16 // the id of the slice in the node, are tracked in the node itself
storage_size_gb int
tags string
}

View File

@@ -0,0 +1,104 @@
module datamodel
//ACCESS ONLY TF
@[heap]
pub struct Node {
pub mut:
id int
nodegroupid int
uptime int // 0..100
computeslices []ComputeSlice
storageslices []StorageSlice
devices DeviceInfo
country string // 2 letter code as specified in lib/data/countries/data/countryInfo.txt, use that library for validation
capacity NodeCapacity // Hardware capacity details
birthtime u32 // first time node was active
pubkey string
signature_node string // signature done on node to validate pubkey with privkey
signature_farmer string // signature as done by farmers to validate their identity
}
pub struct DeviceInfo {
pub mut:
vendor string
storage []StorageDevice
memory []MemoryDevice
cpu []CPUDevice
gpu []GPUDevice
network []NetworkDevice
}
pub struct StorageDevice {
pub mut:
id string // can be used in node
size_gb f64 // Size of the storage device in gigabytes
description string // Description of the storage device
}
pub struct MemoryDevice {
pub mut:
id string // can be used in node
size_gb f64 // Size of the memory device in gigabytes
description string // Description of the memory device
}
pub struct CPUDevice {
pub mut:
id string // can be used in node
cores int // Number of CPU cores
passmark int
description string // Description of the CPU
cpu_brand string // Brand of the CPU
cpu_version string // Version of the CPU
}
pub struct GPUDevice {
pub mut:
id string // can be used in node
cores int // Number of GPU cores
memory_gb f64 // Size of the GPU memory in gigabytes
description string // Description of the GPU
gpu_brand string
gpu_version string
}
pub struct NetworkDevice {
pub mut:
id string // can be used in node
speed_mbps int // Network speed in Mbps
description string // Description of the network device
}
// NodeCapacity represents the hardware capacity details of a node.
pub struct NodeCapacity {
pub mut:
storage_gb f64 // Total storage in gigabytes
mem_gb f64 // Total memory in gigabytes
mem_gb_gpu f64 // Total GPU memory in gigabytes
passmark int // Passmark score for the node
vcores int // Total virtual cores
}
// typically 1GB of memory, but can be adjusted based based on size of machine
pub struct ComputeSlice {
pub mut:
u16 int // the id of the slice in the node
mem_gb f64
storage_gb f64
passmark int
vcores int
cpu_oversubscription int
storage_oversubscription int
gpus u8 // nr of GPU's see node to know what GPU's are
}
// 1GB of storage
pub struct StorageSlice {
pub mut:
u16 int // the id of the slice in the node, are tracked in the node itself
}
fn (mut n Node) check() ! {
// todo calculate NodeCapacity out of the devices on the Node
}

View File

@@ -0,0 +1,33 @@
module datamodel
// is a root object, is the only obj farmer needs to configure in the UI, this defines how slices will be created
@[heap]
pub struct NodeGroup {
pub mut:
id u32
farmerid u32 // link back to farmer who owns the nodegroup, is a user?
secret string // only visible by farmer, in future encrypted, used to boot a node
description string
slapolicy SLAPolicy
pricingpolicy PricingPolicy
compute_slice_normalized_pricing_cc f64 // pricing in CC - cloud credit, per 2GB node slice
storage_slice_normalized_pricing_cc f64 // pricing in CC - cloud credit, per 1GB storage slice
signature_farmer string // signature as done by farmers to validate that they created this group
}
pub struct SLAPolicy {
pub mut:
sla_uptime int // should +90
sla_bandwidth_mbit int // minimal mbits we can expect avg over 1h per node, 0 means we don't guarantee
sla_penalty int // 0-100, percent of money given back in relation to month if sla breached, e.g. 200 means we return 2 months worth of rev if sla missed
}
pub struct PricingPolicy {
pub mut:
marketplace_year_discounts []int = [30, 40, 50] // e.g. 30,40,50 means if user has more CC in wallet than 1 year utilization on all his purchaes then this provider gives 30%, 2Y 40%, ...
// volume_discounts []int = [10, 20, 30] // e.g. 10,20,30
}

View File

@@ -0,0 +1,19 @@
@[heap]
pub struct NodeGroupReputation {
pub mut:
nodegroup_id u32
reputation int = 50 // between 0 and 100, earned over time
uptime int // between 0 and 100, set by system, farmer has no ability to set this
nodes []NodeReputation
}
pub struct NodeReputation {
pub mut:
node_id u32
reputation int = 50 // between 0 and 100, earned over time
uptime int // between 0 and 100, set by system, farmer has no ability to set this
}

View File

@@ -0,0 +1,311 @@
use crate::store::{BaseData, IndexKey, Object};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
/// Defines the supported DNS record types
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum NameType {
A,
AAAA,
CNAME,
MX,
TXT,
SRV,
PTR,
NS,
}
impl Default for NameType {
fn default() -> Self {
NameType::A
}
}
/// Category of the DNS record
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum NameCat {
IPv4,
IPv6,
Mycelium,
}
impl Default for NameCat {
fn default() -> Self {
NameCat::IPv4
}
}
/// Status of a DNS zone
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum DNSZoneStatus {
Active,
Suspended,
Archived,
}
impl Default for DNSZoneStatus {
fn default() -> Self {
DNSZoneStatus::Active
}
}
/// Represents a DNS record configuration
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct DNSRecord {
pub subdomain: String,
pub record_type: NameType,
pub value: String,
pub priority: u32,
pub ttl: u32,
pub is_active: bool,
pub cat: NameCat,
pub is_wildcard: bool,
}
impl DNSRecord {
pub fn new() -> Self {
Self {
subdomain: String::new(),
record_type: NameType::default(),
value: String::new(),
priority: 0,
ttl: 3600,
is_active: true,
cat: NameCat::default(),
is_wildcard: false,
}
}
pub fn subdomain(mut self, subdomain: impl ToString) -> Self {
self.subdomain = subdomain.to_string();
self
}
pub fn record_type(mut self, record_type: NameType) -> Self {
self.record_type = record_type;
self
}
pub fn value(mut self, value: impl ToString) -> Self {
self.value = value.to_string();
self
}
pub fn priority(mut self, priority: u32) -> Self {
self.priority = priority;
self
}
pub fn ttl(mut self, ttl: u32) -> Self {
self.ttl = ttl;
self
}
pub fn is_active(mut self, is_active: bool) -> Self {
self.is_active = is_active;
self
}
pub fn cat(mut self, cat: NameCat) -> Self {
self.cat = cat;
self
}
pub fn is_wildcard(mut self, is_wildcard: bool) -> Self {
self.is_wildcard = is_wildcard;
self
}
pub fn build(self) -> Self {
self
}
}
impl std::fmt::Display for DNSRecord {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}:{:?}", self.subdomain, self.record_type)
}
}
/// SOA (Start of Authority) record for a DNS zone
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct SOARecord {
pub zone_id: u32,
pub primary_ns: String,
pub admin_email: String,
pub serial: u64,
pub refresh: u32,
pub retry: u32,
pub expire: u32,
pub minimum_ttl: u32,
pub is_active: bool,
}
impl SOARecord {
pub fn new() -> Self {
Self {
zone_id: 0,
primary_ns: String::new(),
admin_email: String::new(),
serial: 0,
refresh: 3600,
retry: 600,
expire: 604800,
minimum_ttl: 3600,
is_active: true,
}
}
pub fn zone_id(mut self, zone_id: u32) -> Self {
self.zone_id = zone_id;
self
}
pub fn primary_ns(mut self, primary_ns: impl ToString) -> Self {
self.primary_ns = primary_ns.to_string();
self
}
pub fn admin_email(mut self, admin_email: impl ToString) -> Self {
self.admin_email = admin_email.to_string();
self
}
pub fn serial(mut self, serial: u64) -> Self {
self.serial = serial;
self
}
pub fn refresh(mut self, refresh: u32) -> Self {
self.refresh = refresh;
self
}
pub fn retry(mut self, retry: u32) -> Self {
self.retry = retry;
self
}
pub fn expire(mut self, expire: u32) -> Self {
self.expire = expire;
self
}
pub fn minimum_ttl(mut self, minimum_ttl: u32) -> Self {
self.minimum_ttl = minimum_ttl;
self
}
pub fn is_active(mut self, is_active: bool) -> Self {
self.is_active = is_active;
self
}
pub fn build(self) -> Self {
self
}
}
impl std::fmt::Display for SOARecord {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.primary_ns)
}
}
/// Represents a DNS zone with its configuration and records
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default, crate::DeriveObject)]
pub struct DNSZone {
/// Base model data
pub base_data: BaseData,
#[index]
pub domain: String,
#[index(path = "subdomain")]
#[index(path = "record_type")]
pub dnsrecords: Vec<DNSRecord>,
pub administrators: Vec<u32>,
pub status: DNSZoneStatus,
pub metadata: HashMap<String, String>,
#[index(path = "primary_ns")]
pub soarecord: Vec<SOARecord>,
}
impl DNSZone {
/// Create a new DNS zone instance
pub fn new(id: u32) -> Self {
let mut base_data = BaseData::new();
Self {
base_data,
domain: String::new(),
dnsrecords: Vec::new(),
administrators: Vec::new(),
status: DNSZoneStatus::default(),
metadata: HashMap::new(),
soarecord: Vec::new(),
}
}
/// Set the domain name (fluent)
pub fn domain(mut self, domain: impl ToString) -> Self {
self.domain = domain.to_string();
self
}
/// Add a DNS record (fluent)
pub fn add_dnsrecord(mut self, record: DNSRecord) -> Self {
self.dnsrecords.push(record);
self
}
/// Set all DNS records (fluent)
pub fn dnsrecords(mut self, dnsrecords: Vec<DNSRecord>) -> Self {
self.dnsrecords = dnsrecords;
self
}
/// Add an administrator (fluent)
pub fn add_administrator(mut self, admin_id: u32) -> Self {
self.administrators.push(admin_id);
self
}
/// Set all administrators (fluent)
pub fn administrators(mut self, administrators: Vec<u32>) -> Self {
self.administrators = administrators;
self
}
/// Set the zone status (fluent)
pub fn status(mut self, status: DNSZoneStatus) -> Self {
self.status = status;
self
}
/// Add metadata entry (fluent)
pub fn add_metadata(mut self, key: impl ToString, value: impl ToString) -> Self {
self.metadata.insert(key.to_string(), value.to_string());
self
}
/// Set all metadata (fluent)
pub fn metadata(mut self, metadata: HashMap<String, String>) -> Self {
self.metadata = metadata;
self
}
/// Add an SOA record (fluent)
pub fn add_soarecord(mut self, soa: SOARecord) -> Self {
self.soarecord.push(soa);
self
}
/// Set all SOA records (fluent)
pub fn soarecord(mut self, soarecord: Vec<SOARecord>) -> Self {
self.soarecord = soarecord;
self
}
/// Build the final DNS zone instance
pub fn build(self) -> Self {
self
}
}

View File

@@ -0,0 +1,227 @@
use crate::store::{BaseData, IndexKey, Object};
use serde::{Deserialize, Serialize};
/// Defines the lifecycle of a group
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum GroupStatus {
Active,
Inactive,
Suspended,
Archived,
}
impl Default for GroupStatus {
fn default() -> Self {
GroupStatus::Active
}
}
/// Visibility controls who can discover or view the group
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum Visibility {
Public, // Anyone can see and request to join
Private, // Only invited users can see the group
Unlisted, // Not visible in search; only accessible by direct link or DNS
}
impl Default for Visibility {
fn default() -> Self {
Visibility::Public
}
}
/// GroupConfig holds rules that govern group membership and behavior
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
pub struct GroupConfig {
pub max_members: u32,
pub allow_guests: bool,
pub auto_approve: bool,
pub require_invite: bool,
}
impl GroupConfig {
pub fn new() -> Self {
Self {
max_members: 0,
allow_guests: false,
auto_approve: false,
require_invite: false,
}
}
pub fn max_members(mut self, max_members: u32) -> Self {
self.max_members = max_members;
self
}
pub fn allow_guests(mut self, allow_guests: bool) -> Self {
self.allow_guests = allow_guests;
self
}
pub fn auto_approve(mut self, auto_approve: bool) -> Self {
self.auto_approve = auto_approve;
self
}
pub fn require_invite(mut self, require_invite: bool) -> Self {
self.require_invite = require_invite;
self
}
pub fn build(self) -> Self {
self
}
}
/// Represents a collaborative or access-controlled unit within the system
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default, crate::DeriveObject)]
pub struct Group {
/// Base model data
pub base_data: BaseData,
#[index]
pub name: String,
pub description: String,
pub dnsrecords: Vec<u32>,
pub administrators: Vec<u32>,
pub config: GroupConfig,
pub status: GroupStatus,
pub visibility: Visibility,
pub created: u64,
pub updated: u64,
}
impl Group {
/// Create a new group instance
pub fn new(id: u32) -> Self {
let mut base_data = BaseData::new();
Self {
base_data,
name: String::new(),
description: String::new(),
dnsrecords: Vec::new(),
administrators: Vec::new(),
config: GroupConfig::new(),
status: GroupStatus::default(),
visibility: Visibility::default(),
created: 0,
updated: 0,
}
}
/// Set the group name (fluent)
pub fn name(mut self, name: impl ToString) -> Self {
self.name = name.to_string();
self
}
/// Set the group description (fluent)
pub fn description(mut self, description: impl ToString) -> Self {
self.description = description.to_string();
self
}
/// Add a DNS record ID (fluent)
pub fn add_dnsrecord(mut self, dnsrecord_id: u32) -> Self {
self.dnsrecords.push(dnsrecord_id);
self
}
/// Set all DNS record IDs (fluent)
pub fn dnsrecords(mut self, dnsrecords: Vec<u32>) -> Self {
self.dnsrecords = dnsrecords;
self
}
/// Add an administrator user ID (fluent)
pub fn add_administrator(mut self, user_id: u32) -> Self {
self.administrators.push(user_id);
self
}
/// Set all administrator user IDs (fluent)
pub fn administrators(mut self, administrators: Vec<u32>) -> Self {
self.administrators = administrators;
self
}
/// Set the group configuration (fluent)
pub fn config(mut self, config: GroupConfig) -> Self {
self.config = config;
self
}
/// Set the group status (fluent)
pub fn status(mut self, status: GroupStatus) -> Self {
self.status = status;
self
}
/// Set the group visibility (fluent)
pub fn visibility(mut self, visibility: Visibility) -> Self {
self.visibility = visibility;
self
}
/// Set the created timestamp (fluent)
pub fn created(mut self, created: u64) -> Self {
self.created = created;
self
}
/// Set the updated timestamp (fluent)
pub fn updated(mut self, updated: u64) -> Self {
self.updated = updated;
self
}
/// Build the final group instance
pub fn build(self) -> Self {
self
}
}
/// Represents the membership relationship between users and groups
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default, crate::DeriveObject)]
pub struct UserGroupMembership {
/// Base model data
pub base_data: BaseData,
#[index]
pub user_id: u32,
pub group_ids: Vec<u32>,
}
impl UserGroupMembership {
/// Create a new user group membership instance
pub fn new(id: u32) -> Self {
let mut base_data = BaseData::new();
Self {
base_data,
user_id: 0,
group_ids: Vec::new(),
}
}
/// Set the user ID (fluent)
pub fn user_id(mut self, user_id: u32) -> Self {
self.user_id = user_id;
self
}
/// Add a group ID (fluent)
pub fn add_group_id(mut self, group_id: u32) -> Self {
self.group_ids.push(group_id);
self
}
/// Set all group IDs (fluent)
pub fn group_ids(mut self, group_ids: Vec<u32>) -> Self {
self.group_ids = group_ids;
self
}
/// Build the final membership instance
pub fn build(self) -> Self {
self
}
}

View File

@@ -0,0 +1,110 @@
use crate::store::{BaseData, IndexKey, Object};
use serde::{Deserialize, Serialize};
/// Defines the possible roles a member can have
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum MemberRole {
Owner,
Admin,
Moderator,
Member,
Guest,
}
impl Default for MemberRole {
fn default() -> Self {
MemberRole::Member
}
}
/// Represents the current status of membership
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum MemberStatus {
Active,
Pending,
Suspended,
Removed,
}
impl Default for MemberStatus {
fn default() -> Self {
MemberStatus::Pending
}
}
/// Represents a member within a circle
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default, crate::DeriveObject)]
pub struct Member {
/// Base model data
pub base_data: BaseData,
#[index]
pub user_id: u32,
pub role: MemberRole,
pub status: MemberStatus,
pub joined_at: u64,
pub invited_by: u32,
pub permissions: Vec<String>,
}
impl Member {
/// Create a new member instance
pub fn new(id: u32) -> Self {
let mut base_data = BaseData::new();
Self {
base_data,
user_id: 0,
role: MemberRole::default(),
status: MemberStatus::default(),
joined_at: 0,
invited_by: 0,
permissions: Vec::new(),
}
}
/// Set the user ID (fluent)
pub fn user_id(mut self, user_id: u32) -> Self {
self.user_id = user_id;
self
}
/// Set the member role (fluent)
pub fn role(mut self, role: MemberRole) -> Self {
self.role = role;
self
}
/// Set the member status (fluent)
pub fn status(mut self, status: MemberStatus) -> Self {
self.status = status;
self
}
/// Set the joined timestamp (fluent)
pub fn joined_at(mut self, joined_at: u64) -> Self {
self.joined_at = joined_at;
self
}
/// Set who invited this member (fluent)
pub fn invited_by(mut self, invited_by: u32) -> Self {
self.invited_by = invited_by;
self
}
/// Add a permission (fluent)
pub fn add_permission(mut self, permission: impl ToString) -> Self {
self.permissions.push(permission.to_string());
self
}
/// Set all permissions (fluent)
pub fn permissions(mut self, permissions: Vec<String>) -> Self {
self.permissions = permissions;
self
}
/// Build the final member instance
pub fn build(self) -> Self {
self
}
}

View File

@@ -0,0 +1,10 @@
// Export all heroledger model modules
pub mod dnsrecord;
pub mod group;
pub mod membership;
pub mod money;
pub mod rhai;
pub mod secretbox;
pub mod signature;
pub mod user;
pub mod user_kvs;

View File

@@ -0,0 +1,498 @@
use crate::store::{BaseData, IndexKey, Object};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
/// Represents the status of an account
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum AccountStatus {
Active,
Inactive,
Suspended,
Archived,
}
impl Default for AccountStatus {
fn default() -> Self {
AccountStatus::Active
}
}
/// Represents the type of transaction
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum TransactionType {
Transfer,
Clawback,
Freeze,
Unfreeze,
Issue,
Burn,
}
impl Default for TransactionType {
fn default() -> Self {
TransactionType::Transfer
}
}
/// Represents a signature for transactions
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Signature {
pub signer_id: u32,
pub signature: String,
pub timestamp: u64,
}
impl Signature {
pub fn new() -> Self {
Self {
signer_id: 0,
signature: String::new(),
timestamp: 0,
}
}
pub fn signer_id(mut self, signer_id: u32) -> Self {
self.signer_id = signer_id;
self
}
pub fn signature(mut self, signature: impl ToString) -> Self {
self.signature = signature.to_string();
self
}
pub fn timestamp(mut self, timestamp: u64) -> Self {
self.timestamp = timestamp;
self
}
pub fn build(self) -> Self {
self
}
}
/// Policy item for account operations
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
pub struct AccountPolicyItem {
pub signers: Vec<u32>,
pub min_signatures: u32,
pub enabled: bool,
pub threshold: f64,
pub recipient: u32,
}
impl AccountPolicyItem {
pub fn new() -> Self {
Self {
signers: Vec::new(),
min_signatures: 0,
enabled: false,
threshold: 0.0,
recipient: 0,
}
}
pub fn add_signer(mut self, signer_id: u32) -> Self {
self.signers.push(signer_id);
self
}
pub fn signers(mut self, signers: Vec<u32>) -> Self {
self.signers = signers;
self
}
pub fn min_signatures(mut self, min_signatures: u32) -> Self {
self.min_signatures = min_signatures;
self
}
pub fn enabled(mut self, enabled: bool) -> Self {
self.enabled = enabled;
self
}
pub fn threshold(mut self, threshold: f64) -> Self {
self.threshold = threshold;
self
}
pub fn recipient(mut self, recipient: u32) -> Self {
self.recipient = recipient;
self
}
pub fn build(self) -> Self {
self
}
}
/// Represents an account in the financial system
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default, crate::DeriveObject)]
pub struct Account {
/// Base model data
pub base_data: BaseData,
pub owner_id: u32,
#[index]
pub address: String,
pub balance: f64,
pub currency: String,
pub assetid: u32,
pub last_activity: u64,
pub administrators: Vec<u32>,
pub accountpolicy: u32,
}
impl Account {
/// Create a new account instance
pub fn new(id: u32) -> Self {
let mut base_data = BaseData::new();
Self {
base_data,
owner_id: 0,
address: String::new(),
balance: 0.0,
currency: String::new(),
assetid: 0,
last_activity: 0,
administrators: Vec::new(),
accountpolicy: 0,
}
}
/// Set the owner ID (fluent)
pub fn owner_id(mut self, owner_id: u32) -> Self {
self.owner_id = owner_id;
self
}
/// Set the blockchain address (fluent)
pub fn address(mut self, address: impl ToString) -> Self {
self.address = address.to_string();
self
}
/// Set the balance (fluent)
pub fn balance(mut self, balance: f64) -> Self {
self.balance = balance;
self
}
/// Set the currency (fluent)
pub fn currency(mut self, currency: impl ToString) -> Self {
self.currency = currency.to_string();
self
}
/// Set the asset ID (fluent)
pub fn assetid(mut self, assetid: u32) -> Self {
self.assetid = assetid;
self
}
/// Set the last activity timestamp (fluent)
pub fn last_activity(mut self, last_activity: u64) -> Self {
self.last_activity = last_activity;
self
}
/// Add an administrator (fluent)
pub fn add_administrator(mut self, admin_id: u32) -> Self {
self.administrators.push(admin_id);
self
}
/// Set all administrators (fluent)
pub fn administrators(mut self, administrators: Vec<u32>) -> Self {
self.administrators = administrators;
self
}
/// Set the account policy ID (fluent)
pub fn accountpolicy(mut self, accountpolicy: u32) -> Self {
self.accountpolicy = accountpolicy;
self
}
/// Build the final account instance
pub fn build(self) -> Self {
self
}
}
/// Represents an asset in the financial system
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default, crate::DeriveObject)]
pub struct Asset {
/// Base model data
pub base_data: BaseData,
#[index]
pub address: String,
pub assetid: u32,
pub asset_type: String,
pub issuer: u32,
pub supply: f64,
pub decimals: u8,
pub is_frozen: bool,
pub metadata: HashMap<String, String>,
pub administrators: Vec<u32>,
pub min_signatures: u32,
}
impl Asset {
/// Create a new asset instance
pub fn new(id: u32) -> Self {
let mut base_data = BaseData::new();
Self {
base_data,
address: String::new(),
assetid: 0,
asset_type: String::new(),
issuer: 0,
supply: 0.0,
decimals: 0,
is_frozen: false,
metadata: HashMap::new(),
administrators: Vec::new(),
min_signatures: 0,
}
}
/// Set the blockchain address (fluent)
pub fn address(mut self, address: impl ToString) -> Self {
self.address = address.to_string();
self
}
/// Set the asset ID (fluent)
pub fn assetid(mut self, assetid: u32) -> Self {
self.assetid = assetid;
self
}
/// Set the asset type (fluent)
pub fn asset_type(mut self, asset_type: impl ToString) -> Self {
self.asset_type = asset_type.to_string();
self
}
/// Set the issuer (fluent)
pub fn issuer(mut self, issuer: u32) -> Self {
self.issuer = issuer;
self
}
/// Set the supply (fluent)
pub fn supply(mut self, supply: f64) -> Self {
self.supply = supply;
self
}
/// Set the decimals (fluent)
pub fn decimals(mut self, decimals: u8) -> Self {
self.decimals = decimals;
self
}
/// Set the frozen status (fluent)
pub fn is_frozen(mut self, is_frozen: bool) -> Self {
self.is_frozen = is_frozen;
self
}
/// Add metadata entry (fluent)
pub fn add_metadata(mut self, key: impl ToString, value: impl ToString) -> Self {
self.metadata.insert(key.to_string(), value.to_string());
self
}
/// Set all metadata (fluent)
pub fn metadata(mut self, metadata: HashMap<String, String>) -> Self {
self.metadata = metadata;
self
}
/// Add an administrator (fluent)
pub fn add_administrator(mut self, admin_id: u32) -> Self {
self.administrators.push(admin_id);
self
}
/// Set all administrators (fluent)
pub fn administrators(mut self, administrators: Vec<u32>) -> Self {
self.administrators = administrators;
self
}
/// Set minimum signatures required (fluent)
pub fn min_signatures(mut self, min_signatures: u32) -> Self {
self.min_signatures = min_signatures;
self
}
/// Build the final asset instance
pub fn build(self) -> Self {
self
}
}
/// Represents account policies for various operations
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default, crate::DeriveObject)]
pub struct AccountPolicy {
/// Base model data
pub base_data: BaseData,
pub transferpolicy: AccountPolicyItem,
pub adminpolicy: AccountPolicyItem,
pub clawbackpolicy: AccountPolicyItem,
pub freezepolicy: AccountPolicyItem,
}
impl AccountPolicy {
/// Create a new account policy instance
pub fn new(id: u32) -> Self {
let mut base_data = BaseData::new();
Self {
base_data,
transferpolicy: AccountPolicyItem::new(),
adminpolicy: AccountPolicyItem::new(),
clawbackpolicy: AccountPolicyItem::new(),
freezepolicy: AccountPolicyItem::new(),
}
}
/// Set the transfer policy (fluent)
pub fn transferpolicy(mut self, transferpolicy: AccountPolicyItem) -> Self {
self.transferpolicy = transferpolicy;
self
}
/// Set the admin policy (fluent)
pub fn adminpolicy(mut self, adminpolicy: AccountPolicyItem) -> Self {
self.adminpolicy = adminpolicy;
self
}
/// Set the clawback policy (fluent)
pub fn clawbackpolicy(mut self, clawbackpolicy: AccountPolicyItem) -> Self {
self.clawbackpolicy = clawbackpolicy;
self
}
/// Set the freeze policy (fluent)
pub fn freezepolicy(mut self, freezepolicy: AccountPolicyItem) -> Self {
self.freezepolicy = freezepolicy;
self
}
/// Build the final account policy instance
pub fn build(self) -> Self {
self
}
}
/// Represents a financial transaction
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default, crate::DeriveObject)]
pub struct Transaction {
/// Base model data
pub base_data: BaseData,
pub txid: u32,
pub source: u32,
pub destination: u32,
pub assetid: u32,
pub amount: f64,
pub timestamp: u64,
pub status: String,
pub memo: String,
pub tx_type: TransactionType,
pub signatures: Vec<Signature>,
}
impl Transaction {
/// Create a new transaction instance
pub fn new(id: u32) -> Self {
let mut base_data = BaseData::new();
Self {
base_data,
txid: 0,
source: 0,
destination: 0,
assetid: 0,
amount: 0.0,
timestamp: 0,
status: String::new(),
memo: String::new(),
tx_type: TransactionType::default(),
signatures: Vec::new(),
}
}
/// Set the transaction ID (fluent)
pub fn txid(mut self, txid: u32) -> Self {
self.txid = txid;
self
}
/// Set the source account (fluent)
pub fn source(mut self, source: u32) -> Self {
self.source = source;
self
}
/// Set the destination account (fluent)
pub fn destination(mut self, destination: u32) -> Self {
self.destination = destination;
self
}
/// Set the asset ID (fluent)
pub fn assetid(mut self, assetid: u32) -> Self {
self.assetid = assetid;
self
}
/// Set the amount (fluent)
pub fn amount(mut self, amount: f64) -> Self {
self.amount = amount;
self
}
/// Set the timestamp (fluent)
pub fn timestamp(mut self, timestamp: u64) -> Self {
self.timestamp = timestamp;
self
}
/// Set the status (fluent)
pub fn status(mut self, status: impl ToString) -> Self {
self.status = status.to_string();
self
}
/// Set the memo (fluent)
pub fn memo(mut self, memo: impl ToString) -> Self {
self.memo = memo.to_string();
self
}
/// Set the transaction type (fluent)
pub fn tx_type(mut self, tx_type: TransactionType) -> Self {
self.tx_type = tx_type;
self
}
/// Add a signature (fluent)
pub fn add_signature(mut self, signature: Signature) -> Self {
self.signatures.push(signature);
self
}
/// Set all signatures (fluent)
pub fn signatures(mut self, signatures: Vec<Signature>) -> Self {
self.signatures = signatures;
self
}
/// Build the final transaction instance
pub fn build(self) -> Self {
self
}
}

View File

@@ -0,0 +1,364 @@
use ::rhai::plugin::*;
use ::rhai::{Dynamic, Engine, EvalAltResult, Module, CustomType, TypeBuilder};
use std::mem;
use crate::objects::heroledger::{
dnsrecord::DNSZone,
group::{Group, Visibility},
money::Account,
user::{User, UserStatus},
};
// ============================================================================
// User Module
// ============================================================================
type RhaiUser = User;
#[export_module]
mod rhai_user_module {
use crate::objects::heroledger::user::User;
use super::RhaiUser;
#[rhai_fn(name = "new_user", return_raw)]
pub fn new_user() -> Result<RhaiUser, Box<EvalAltResult>> {
Ok(User::new(0))
}
#[rhai_fn(name = "username", return_raw)]
pub fn set_username(
user: &mut RhaiUser,
username: String,
) -> Result<RhaiUser, Box<EvalAltResult>> {
let owned = std::mem::take(user);
*user = owned.username(username);
Ok(user.clone())
}
#[rhai_fn(name = "add_email", return_raw)]
pub fn add_email(user: &mut RhaiUser, email: String) -> Result<RhaiUser, Box<EvalAltResult>> {
let owned = std::mem::take(user);
*user = owned.add_email(email);
Ok(user.clone())
}
#[rhai_fn(name = "pubkey", return_raw)]
pub fn set_pubkey(user: &mut RhaiUser, pubkey: String) -> Result<RhaiUser, Box<EvalAltResult>> {
let owned = std::mem::take(user);
*user = owned.pubkey(pubkey);
Ok(user.clone())
}
#[rhai_fn(name = "status", return_raw)]
pub fn set_status(user: &mut RhaiUser, status: String) -> Result<RhaiUser, Box<EvalAltResult>> {
let status_enum = match status.as_str() {
"Active" => UserStatus::Active,
"Inactive" => UserStatus::Inactive,
"Suspended" => UserStatus::Suspended,
"Archived" => UserStatus::Archived,
_ => return Err(format!("Invalid user status: {}", status).into()),
};
let owned = std::mem::take(user);
*user = owned.status(status_enum);
Ok(user.clone())
}
#[rhai_fn(name = "save_user", return_raw)]
pub fn save_user(user: &mut RhaiUser) -> Result<RhaiUser, Box<EvalAltResult>> {
// This would integrate with the database save functionality
// For now, just return the user as-is
Ok(user.clone())
}
// Getters
#[rhai_fn(name = "get_id")]
pub fn get_id(user: &mut RhaiUser) -> u32 {
user.base_data.id
}
#[rhai_fn(name = "get_username")]
pub fn get_username(user: &mut RhaiUser) -> String {
user.username.clone()
}
#[rhai_fn(name = "get_email")]
pub fn get_email(user: &mut RhaiUser) -> String {
if let Some(first_email) = user.email.first() {
first_email.clone()
} else {
String::new()
}
}
#[rhai_fn(name = "get_pubkey")]
pub fn get_pubkey(user: &mut RhaiUser) -> String {
user.pubkey.clone()
}
}
// ============================================================================
// Group Module
// ============================================================================
type RhaiGroup = Group;
#[export_module]
mod rhai_group_module {
use super::RhaiGroup;
#[rhai_fn(name = "new_group", return_raw)]
pub fn new_group() -> Result<RhaiGroup, Box<EvalAltResult>> {
Ok(Group::new(0))
}
#[rhai_fn(name = "name", return_raw)]
pub fn set_name(group: &mut RhaiGroup, name: String) -> Result<RhaiGroup, Box<EvalAltResult>> {
let owned = std::mem::take(group);
*group = owned.name(name);
Ok(group.clone())
}
#[rhai_fn(name = "description", return_raw)]
pub fn set_description(
group: &mut RhaiGroup,
description: String,
) -> Result<RhaiGroup, Box<EvalAltResult>> {
let owned = std::mem::take(group);
*group = owned.description(description);
Ok(group.clone())
}
#[rhai_fn(name = "visibility", return_raw)]
pub fn set_visibility(
group: &mut RhaiGroup,
visibility: String,
) -> Result<RhaiGroup, Box<EvalAltResult>> {
let visibility_enum = match visibility.as_str() {
"Public" => Visibility::Public,
"Private" => Visibility::Private,
_ => return Err(format!("Invalid visibility: {}", visibility).into()),
};
let owned = std::mem::take(group);
*group = owned.visibility(visibility_enum);
Ok(group.clone())
}
#[rhai_fn(name = "save_group", return_raw)]
pub fn save_group(group: &mut RhaiGroup) -> Result<RhaiGroup, Box<EvalAltResult>> {
Ok(group.clone())
}
// Getters
#[rhai_fn(name = "get_id")]
pub fn get_id(group: &mut RhaiGroup) -> u32 {
group.base_data.id
}
#[rhai_fn(name = "get_name")]
pub fn get_name(group: &mut RhaiGroup) -> String {
group.name.clone()
}
#[rhai_fn(name = "get_description")]
pub fn get_description(group: &mut RhaiGroup) -> String {
group.description.clone()
}
}
// ============================================================================
// Account Module (from money.rs)
// ============================================================================
type RhaiAccount = Account;
#[export_module]
mod rhai_account_module {
use super::RhaiAccount;
#[rhai_fn(name = "new_account", return_raw)]
pub fn new_account() -> Result<RhaiAccount, Box<EvalAltResult>> {
Ok(Account::new(0))
}
#[rhai_fn(name = "owner_id", return_raw)]
pub fn set_owner_id(
account: &mut RhaiAccount,
owner_id: i64,
) -> Result<RhaiAccount, Box<EvalAltResult>> {
let owned = std::mem::take(account);
*account = owned.owner_id(owner_id as u32);
Ok(account.clone())
}
#[rhai_fn(name = "address", return_raw)]
pub fn set_address(
account: &mut RhaiAccount,
address: String,
) -> Result<RhaiAccount, Box<EvalAltResult>> {
let owned = std::mem::take(account);
*account = owned.address(address);
Ok(account.clone())
}
#[rhai_fn(name = "currency", return_raw)]
pub fn set_currency(
account: &mut RhaiAccount,
currency: String,
) -> Result<RhaiAccount, Box<EvalAltResult>> {
let owned = std::mem::take(account);
*account = owned.currency(currency);
Ok(account.clone())
}
#[rhai_fn(name = "save_account", return_raw)]
pub fn save_account(account: &mut RhaiAccount) -> Result<RhaiAccount, Box<EvalAltResult>> {
Ok(account.clone())
}
// Getters
#[rhai_fn(name = "get_id")]
pub fn get_id(account: &mut RhaiAccount) -> u32 {
account.base_data.id
}
#[rhai_fn(name = "get_address")]
pub fn get_address(account: &mut RhaiAccount) -> String {
account.address.clone()
}
#[rhai_fn(name = "get_currency")]
pub fn get_currency(account: &mut RhaiAccount) -> String {
account.currency.clone()
}
}
// ============================================================================
// DNS Zone Module
// ============================================================================
type RhaiDNSZone = DNSZone;
#[export_module]
mod rhai_dns_zone_module {
use super::RhaiDNSZone;
#[rhai_fn(name = "new_dns_zone", return_raw)]
pub fn new_dns_zone() -> Result<RhaiDNSZone, Box<EvalAltResult>> {
Ok(DNSZone::new(0))
}
#[rhai_fn(name = "domain", return_raw)]
pub fn set_domain(
zone: &mut RhaiDNSZone,
domain: String,
) -> Result<RhaiDNSZone, Box<EvalAltResult>> {
let owned = std::mem::take(zone);
*zone = owned.domain(domain);
Ok(zone.clone())
}
#[rhai_fn(name = "save_dns_zone", return_raw)]
pub fn save_dns_zone(zone: &mut RhaiDNSZone) -> Result<RhaiDNSZone, Box<EvalAltResult>> {
Ok(zone.clone())
}
// Getters
#[rhai_fn(name = "get_id")]
pub fn get_id(zone: &mut RhaiDNSZone) -> u32 {
zone.base_data.id
}
#[rhai_fn(name = "get_domain")]
pub fn get_domain(zone: &mut RhaiDNSZone) -> String {
zone.domain.clone()
}
}
// ============================================================================
// Registration Functions
// ============================================================================
// Registration functions
/// Register heroledger modules into a Rhai Module (for use in packages)
/// This flattens all functions into the parent module
pub fn register_heroledger_modules(parent_module: &mut Module) {
// Register custom types
parent_module.set_custom_type::<User>("User");
parent_module.set_custom_type::<Group>("Group");
parent_module.set_custom_type::<Account>("Account");
parent_module.set_custom_type::<DNSZone>("DNSZone");
// Merge user functions into parent module
let user_module = exported_module!(rhai_user_module);
parent_module.merge(&user_module);
// Merge group functions into parent module
let group_module = exported_module!(rhai_group_module);
parent_module.merge(&group_module);
// Merge account functions into parent module
let account_module = exported_module!(rhai_account_module);
parent_module.merge(&account_module);
// Merge dnszone functions into parent module
let dnszone_module = exported_module!(rhai_dns_zone_module);
parent_module.merge(&dnszone_module);
}
/// Register heroledger modules into a Rhai Engine (for standalone use)
pub fn register_user_functions(engine: &mut Engine) {
let module = exported_module!(rhai_user_module);
engine.register_static_module("user", module.into());
}
pub fn register_group_functions(engine: &mut Engine) {
let module = exported_module!(rhai_group_module);
engine.register_static_module("group", module.into());
}
pub fn register_account_functions(engine: &mut Engine) {
let module = exported_module!(rhai_account_module);
engine.register_static_module("account", module.into());
}
pub fn register_dnszone_functions(engine: &mut Engine) {
let module = exported_module!(rhai_dns_zone_module);
engine.register_static_module("dnszone", module.into());
}
/// Register all heroledger Rhai modules with the engine
pub fn register_heroledger_rhai_modules(engine: &mut Engine) {
register_user_functions(engine);
register_group_functions(engine);
register_account_functions(engine);
register_dnszone_functions(engine);
}
// ============================================================================
// CustomType Implementations (for type registration in Rhai)
// ============================================================================
impl CustomType for User {
fn build(mut builder: TypeBuilder<Self>) {
builder.with_name("User");
}
}
impl CustomType for Group {
fn build(mut builder: TypeBuilder<Self>) {
builder.with_name("Group");
}
}
impl CustomType for Account {
fn build(mut builder: TypeBuilder<Self>) {
builder.with_name("Account");
}
}
impl CustomType for DNSZone {
fn build(mut builder: TypeBuilder<Self>) {
builder.with_name("DNSZone");
}
}

View File

@@ -0,0 +1,137 @@
use crate::store::{BaseData, IndexKey, Object};
use serde::{Deserialize, Serialize};
/// Category of the secret box
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum SecretBoxCategory {
Profile,
}
impl Default for SecretBoxCategory {
fn default() -> Self {
SecretBoxCategory::Profile
}
}
/// Status of a notary
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum NotaryStatus {
Active,
Inactive,
Suspended,
Archived,
Error,
}
impl Default for NotaryStatus {
fn default() -> Self {
NotaryStatus::Active
}
}
/// Represents an encrypted secret box for storing sensitive data
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct SecretBox {
pub notary_id: u32,
pub value: String,
pub version: u16,
pub timestamp: u64,
pub cat: SecretBoxCategory,
}
impl SecretBox {
pub fn new() -> Self {
Self {
notary_id: 0,
value: String::new(),
version: 1,
timestamp: 0,
cat: SecretBoxCategory::default(),
}
}
pub fn notary_id(mut self, notary_id: u32) -> Self {
self.notary_id = notary_id;
self
}
pub fn value(mut self, value: impl ToString) -> Self {
self.value = value.to_string();
self
}
pub fn version(mut self, version: u16) -> Self {
self.version = version;
self
}
pub fn timestamp(mut self, timestamp: u64) -> Self {
self.timestamp = timestamp;
self
}
pub fn cat(mut self, cat: SecretBoxCategory) -> Self {
self.cat = cat;
self
}
pub fn build(self) -> Self {
self
}
}
/// Represents a notary who can decrypt secret boxes
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default, crate::DeriveObject)]
pub struct Notary {
/// Base model data
pub base_data: BaseData,
#[index]
pub userid: u32,
pub status: NotaryStatus,
pub myceliumaddress: String,
#[index]
pub pubkey: String,
}
impl Notary {
/// Create a new notary instance
pub fn new(id: u32) -> Self {
let mut base_data = BaseData::new();
Self {
base_data,
userid: 0,
status: NotaryStatus::default(),
myceliumaddress: String::new(),
pubkey: String::new(),
}
}
/// Set the user ID (fluent)
pub fn userid(mut self, userid: u32) -> Self {
self.userid = userid;
self
}
/// Set the notary status (fluent)
pub fn status(mut self, status: NotaryStatus) -> Self {
self.status = status;
self
}
/// Set the mycelium address (fluent)
pub fn myceliumaddress(mut self, myceliumaddress: impl ToString) -> Self {
self.myceliumaddress = myceliumaddress.to_string();
self
}
/// Set the public key (fluent)
pub fn pubkey(mut self, pubkey: impl ToString) -> Self {
self.pubkey = pubkey.to_string();
self
}
/// Build the final notary instance
pub fn build(self) -> Self {
self
}
}

View File

@@ -0,0 +1,115 @@
use crate::store::{BaseData, IndexKey, Object};
use serde::{Deserialize, Serialize};
/// Status of a signature
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum SignatureStatus {
Active,
Inactive,
Pending,
Revoked,
}
impl Default for SignatureStatus {
fn default() -> Self {
SignatureStatus::Pending
}
}
/// Type of object being signed
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum ObjectType {
Account,
DNSRecord,
Membership,
User,
Transaction,
KYC,
}
impl Default for ObjectType {
fn default() -> Self {
ObjectType::User
}
}
/// Represents a cryptographic signature for various objects
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default, crate::DeriveObject)]
pub struct Signature {
/// Base model data
pub base_data: BaseData,
#[index]
pub signature_id: u32,
#[index]
pub user_id: u32,
pub value: String,
#[index]
pub objectid: u32,
pub objecttype: ObjectType,
pub status: SignatureStatus,
pub timestamp: u64,
}
impl Signature {
/// Create a new signature instance
pub fn new(id: u32) -> Self {
let mut base_data = BaseData::new();
Self {
base_data,
signature_id: 0,
user_id: 0,
value: String::new(),
objectid: 0,
objecttype: ObjectType::default(),
status: SignatureStatus::default(),
timestamp: 0,
}
}
/// Set the signature ID (fluent)
pub fn signature_id(mut self, signature_id: u32) -> Self {
self.signature_id = signature_id;
self
}
/// Set the user ID (fluent)
pub fn user_id(mut self, user_id: u32) -> Self {
self.user_id = user_id;
self
}
/// Set the signature value (fluent)
pub fn value(mut self, value: impl ToString) -> Self {
self.value = value.to_string();
self
}
/// Set the object ID (fluent)
pub fn objectid(mut self, objectid: u32) -> Self {
self.objectid = objectid;
self
}
/// Set the object type (fluent)
pub fn objecttype(mut self, objecttype: ObjectType) -> Self {
self.objecttype = objecttype;
self
}
/// Set the signature status (fluent)
pub fn status(mut self, status: SignatureStatus) -> Self {
self.status = status;
self
}
/// Set the timestamp (fluent)
pub fn timestamp(mut self, timestamp: u64) -> Self {
self.timestamp = timestamp;
self
}
/// Build the final signature instance
pub fn build(self) -> Self {
self
}
}

View File

@@ -0,0 +1,365 @@
use crate::store::{BaseData, IndexKey, Object};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
/// Represents the status of a user in the system
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum UserStatus {
Active,
Inactive,
Suspended,
Archived,
}
impl Default for UserStatus {
fn default() -> Self {
UserStatus::Active
}
}
/// Represents the KYC status of a user
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum KYCStatus {
Pending,
Approved,
Rejected,
}
impl Default for KYCStatus {
fn default() -> Self {
KYCStatus::Pending
}
}
/// User profile information
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct UserProfile {
pub user_id: u32,
pub full_name: String,
pub bio: String,
pub profile_pic: String,
pub links: HashMap<String, String>,
pub metadata: HashMap<String, String>,
}
impl UserProfile {
pub fn new() -> Self {
Self {
user_id: 0,
full_name: String::new(),
bio: String::new(),
profile_pic: String::new(),
links: HashMap::new(),
metadata: HashMap::new(),
}
}
pub fn user_id(mut self, user_id: u32) -> Self {
self.user_id = user_id;
self
}
pub fn full_name(mut self, full_name: impl ToString) -> Self {
self.full_name = full_name.to_string();
self
}
pub fn bio(mut self, bio: impl ToString) -> Self {
self.bio = bio.to_string();
self
}
pub fn profile_pic(mut self, profile_pic: impl ToString) -> Self {
self.profile_pic = profile_pic.to_string();
self
}
pub fn add_link(mut self, key: impl ToString, value: impl ToString) -> Self {
self.links.insert(key.to_string(), value.to_string());
self
}
pub fn links(mut self, links: HashMap<String, String>) -> Self {
self.links = links;
self
}
pub fn add_metadata(mut self, key: impl ToString, value: impl ToString) -> Self {
self.metadata.insert(key.to_string(), value.to_string());
self
}
pub fn metadata(mut self, metadata: HashMap<String, String>) -> Self {
self.metadata = metadata;
self
}
pub fn build(self) -> Self {
self
}
}
/// KYC (Know Your Customer) information for a user
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct KYCInfo {
pub user_id: u32,
pub full_name: String,
pub date_of_birth: u64,
pub address: String,
pub phone_number: String,
pub id_number: String,
pub id_type: String,
pub id_expiry: u64,
pub kyc_status: KYCStatus,
pub kyc_verified: bool,
pub kyc_verified_by: u32,
pub kyc_verified_at: u64,
pub kyc_rejected_reason: String,
pub kyc_signature: u32,
pub metadata: HashMap<String, String>,
}
impl KYCInfo {
pub fn new() -> Self {
Self {
user_id: 0,
full_name: String::new(),
date_of_birth: 0,
address: String::new(),
phone_number: String::new(),
id_number: String::new(),
id_type: String::new(),
id_expiry: 0,
kyc_status: KYCStatus::default(),
kyc_verified: false,
kyc_verified_by: 0,
kyc_verified_at: 0,
kyc_rejected_reason: String::new(),
kyc_signature: 0,
metadata: HashMap::new(),
}
}
pub fn user_id(mut self, user_id: u32) -> Self {
self.user_id = user_id;
self
}
pub fn full_name(mut self, full_name: impl ToString) -> Self {
self.full_name = full_name.to_string();
self
}
pub fn date_of_birth(mut self, date_of_birth: u64) -> Self {
self.date_of_birth = date_of_birth;
self
}
pub fn address(mut self, address: impl ToString) -> Self {
self.address = address.to_string();
self
}
pub fn phone_number(mut self, phone_number: impl ToString) -> Self {
self.phone_number = phone_number.to_string();
self
}
pub fn id_number(mut self, id_number: impl ToString) -> Self {
self.id_number = id_number.to_string();
self
}
pub fn id_type(mut self, id_type: impl ToString) -> Self {
self.id_type = id_type.to_string();
self
}
pub fn id_expiry(mut self, id_expiry: u64) -> Self {
self.id_expiry = id_expiry;
self
}
pub fn kyc_status(mut self, kyc_status: KYCStatus) -> Self {
self.kyc_status = kyc_status;
self
}
pub fn kyc_verified(mut self, kyc_verified: bool) -> Self {
self.kyc_verified = kyc_verified;
self
}
pub fn kyc_verified_by(mut self, kyc_verified_by: u32) -> Self {
self.kyc_verified_by = kyc_verified_by;
self
}
pub fn kyc_verified_at(mut self, kyc_verified_at: u64) -> Self {
self.kyc_verified_at = kyc_verified_at;
self
}
pub fn kyc_rejected_reason(mut self, kyc_rejected_reason: impl ToString) -> Self {
self.kyc_rejected_reason = kyc_rejected_reason.to_string();
self
}
pub fn kyc_signature(mut self, kyc_signature: u32) -> Self {
self.kyc_signature = kyc_signature;
self
}
pub fn add_metadata(mut self, key: impl ToString, value: impl ToString) -> Self {
self.metadata.insert(key.to_string(), value.to_string());
self
}
pub fn metadata(mut self, metadata: HashMap<String, String>) -> Self {
self.metadata = metadata;
self
}
pub fn build(self) -> Self {
self
}
}
/// Represents a secret box for storing encrypted data
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct SecretBox {
pub data: Vec<u8>,
pub nonce: Vec<u8>,
}
impl SecretBox {
pub fn new() -> Self {
Self {
data: Vec::new(),
nonce: Vec::new(),
}
}
pub fn data(mut self, data: Vec<u8>) -> Self {
self.data = data;
self
}
pub fn nonce(mut self, nonce: Vec<u8>) -> Self {
self.nonce = nonce;
self
}
pub fn build(self) -> Self {
self
}
}
/// Represents a user in the heroledger system
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, crate::DeriveObject)]
pub struct User {
/// Base model data
pub base_data: BaseData,
#[index]
pub username: String,
#[index]
pub pubkey: String,
pub email: Vec<String>,
pub status: UserStatus,
pub userprofile: Vec<SecretBox>,
pub kyc: Vec<SecretBox>,
}
impl Default for User {
fn default() -> Self {
Self {
base_data: BaseData::new(),
username: String::new(),
pubkey: String::new(),
email: Vec::new(),
status: UserStatus::default(),
userprofile: Vec::new(),
kyc: Vec::new(),
}
}
}
impl User {
/// Create a new user instance
pub fn new(id: u32) -> Self {
let mut base_data = BaseData::new();
Self {
base_data,
username: String::new(),
pubkey: String::new(),
email: Vec::new(),
status: UserStatus::default(),
userprofile: Vec::new(),
kyc: Vec::new(),
}
}
/// Get the user ID
pub fn id(&self) -> u32 {
self.base_data.id
}
/// Set the username (fluent)
pub fn username(mut self, username: impl ToString) -> Self {
self.username = username.to_string();
self
}
/// Set the public key (fluent)
pub fn pubkey(mut self, pubkey: impl ToString) -> Self {
self.pubkey = pubkey.to_string();
self
}
/// Add an email address (fluent)
pub fn add_email(mut self, email: impl ToString) -> Self {
self.email.push(email.to_string());
self
}
/// Set all email addresses (fluent)
pub fn email(mut self, email: Vec<String>) -> Self {
self.email = email;
self
}
/// Set the user status (fluent)
pub fn status(mut self, status: UserStatus) -> Self {
self.status = status;
self
}
/// Add a user profile secret box (fluent)
pub fn add_userprofile(mut self, profile: SecretBox) -> Self {
self.userprofile.push(profile);
self
}
/// Set all user profile secret boxes (fluent)
pub fn userprofile(mut self, userprofile: Vec<SecretBox>) -> Self {
self.userprofile = userprofile;
self
}
/// Add a KYC secret box (fluent)
pub fn add_kyc(mut self, kyc: SecretBox) -> Self {
self.kyc.push(kyc);
self
}
/// Set all KYC secret boxes (fluent)
pub fn kyc(mut self, kyc: Vec<SecretBox>) -> Self {
self.kyc = kyc;
self
}
/// Build the final user instance
pub fn build(self) -> Self {
self
}
}

View File

@@ -0,0 +1,111 @@
use super::secretbox::SecretBox;
use crate::store::{BaseData, IndexKey, Object};
use serde::{Deserialize, Serialize};
/// Represents a per-user key-value store
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default, crate::DeriveObject)]
pub struct UserKVS {
/// Base model data
pub base_data: BaseData,
#[index]
pub userid: u32,
pub name: String,
}
impl UserKVS {
/// Create a new user KVS instance
pub fn new(id: u32) -> Self {
let mut base_data = BaseData::new();
Self {
base_data,
userid: 0,
name: String::new(),
}
}
/// Set the user ID (fluent)
pub fn userid(mut self, userid: u32) -> Self {
self.userid = userid;
self
}
/// Set the KVS name (fluent)
pub fn name(mut self, name: impl ToString) -> Self {
self.name = name.to_string();
self
}
/// Build the final user KVS instance
pub fn build(self) -> Self {
self
}
}
/// Represents an item in a user's key-value store
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default, crate::DeriveObject)]
pub struct UserKVSItem {
/// Base model data
pub base_data: BaseData,
#[index]
pub userkvs_id: u32,
pub key: String,
pub value: String,
pub secretbox: Vec<SecretBox>,
pub timestamp: u64,
}
impl UserKVSItem {
/// Create a new user KVS item instance
pub fn new(id: u32) -> Self {
let mut base_data = BaseData::new();
Self {
base_data,
userkvs_id: 0,
key: String::new(),
value: String::new(),
secretbox: Vec::new(),
timestamp: 0,
}
}
/// Set the user KVS ID (fluent)
pub fn userkvs_id(mut self, userkvs_id: u32) -> Self {
self.userkvs_id = userkvs_id;
self
}
/// Set the key (fluent)
pub fn key(mut self, key: impl ToString) -> Self {
self.key = key.to_string();
self
}
/// Set the value (fluent)
pub fn value(mut self, value: impl ToString) -> Self {
self.value = value.to_string();
self
}
/// Add a secret box (fluent)
pub fn add_secretbox(mut self, secretbox: SecretBox) -> Self {
self.secretbox.push(secretbox);
self
}
/// Set all secret boxes (fluent)
pub fn secretbox(mut self, secretbox: Vec<SecretBox>) -> Self {
self.secretbox = secretbox;
self
}
/// Set the timestamp (fluent)
pub fn timestamp(mut self, timestamp: u64) -> Self {
self.timestamp = timestamp;
self
}
/// Build the final user KVS item instance
pub fn build(self) -> Self {
self
}
}

View File

@@ -0,0 +1,238 @@
/// KYC Client
///
/// Actual API client for making KYC provider API calls.
/// Currently implements Idenfy API but designed to be extensible for other providers.
use serde::{Deserialize, Serialize};
use super::{KycInfo, KycSession, session::SessionStatus};
/// KYC Client for making API calls to KYC providers
#[derive(Debug, Clone)]
pub struct KycClient {
/// Provider name (e.g., "idenfy", "sumsub", "onfido")
pub provider: String,
/// API key
pub api_key: String,
/// API secret
pub api_secret: String,
/// Base URL for API (optional, uses provider default if not set)
pub base_url: Option<String>,
}
/// Idenfy-specific API request/response structures
#[derive(Debug, Serialize, Deserialize)]
pub struct IdenfyTokenRequest {
#[serde(rename = "clientId")]
pub client_id: String,
#[serde(rename = "firstName")]
pub first_name: String,
#[serde(rename = "lastName")]
pub last_name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub email: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub phone: Option<String>,
#[serde(rename = "dateOfBirth", skip_serializing_if = "Option::is_none")]
pub date_of_birth: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub nationality: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub address: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub city: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub country: Option<String>,
#[serde(rename = "zipCode", skip_serializing_if = "Option::is_none")]
pub zip_code: Option<String>,
#[serde(rename = "successUrl", skip_serializing_if = "Option::is_none")]
pub success_url: Option<String>,
#[serde(rename = "errorUrl", skip_serializing_if = "Option::is_none")]
pub error_url: Option<String>,
#[serde(rename = "callbackUrl", skip_serializing_if = "Option::is_none")]
pub callback_url: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub locale: Option<String>,
}
#[derive(Debug, Deserialize)]
pub struct IdenfyTokenResponse {
#[serde(rename = "authToken")]
pub auth_token: String,
#[serde(rename = "scanRef")]
pub scan_ref: String,
#[serde(rename = "clientId")]
pub client_id: String,
}
#[derive(Debug, Deserialize)]
pub struct IdenfyVerificationStatus {
pub status: String,
#[serde(rename = "scanRef")]
pub scan_ref: String,
#[serde(rename = "clientId")]
pub client_id: String,
}
impl KycClient {
/// Create a new KYC client
pub fn new(provider: String, api_key: String, api_secret: String) -> Self {
Self {
provider,
api_key,
api_secret,
base_url: None,
}
}
/// Create an Idenfy client
pub fn idenfy(api_key: String, api_secret: String) -> Self {
Self {
provider: "idenfy".to_string(),
api_key,
api_secret,
base_url: Some("https://ivs.idenfy.com/api/v2".to_string()),
}
}
/// Set custom base URL
pub fn with_base_url(mut self, base_url: String) -> Self {
self.base_url = Some(base_url);
self
}
/// Get the base URL for the provider
fn get_base_url(&self) -> String {
if let Some(url) = &self.base_url {
return url.clone();
}
match self.provider.as_str() {
"idenfy" => "https://ivs.idenfy.com/api/v2".to_string(),
"sumsub" => "https://api.sumsub.com".to_string(),
"onfido" => "https://api.onfido.com/v3".to_string(),
_ => panic!("Unknown provider: {}", self.provider),
}
}
/// Create a verification session (Idenfy implementation)
pub async fn create_verification_session(
&self,
kyc_info: &KycInfo,
session: &mut KycSession,
) -> Result<String, Box<dyn std::error::Error>> {
match self.provider.as_str() {
"idenfy" => self.create_idenfy_session(kyc_info, session).await,
_ => Err(format!("Provider {} not yet implemented", self.provider).into()),
}
}
/// Create an Idenfy verification session
async fn create_idenfy_session(
&self,
kyc_info: &KycInfo,
session: &mut KycSession,
) -> Result<String, Box<dyn std::error::Error>> {
let url = format!("{}/token", self.get_base_url());
let request = IdenfyTokenRequest {
client_id: kyc_info.client_id.clone(),
first_name: kyc_info.first_name.clone(),
last_name: kyc_info.last_name.clone(),
email: kyc_info.email.clone(),
phone: kyc_info.phone.clone(),
date_of_birth: kyc_info.date_of_birth.clone(),
nationality: kyc_info.nationality.clone(),
address: kyc_info.address.clone(),
city: kyc_info.city.clone(),
country: kyc_info.country.clone(),
zip_code: kyc_info.postal_code.clone(),
success_url: session.success_url.clone(),
error_url: session.error_url.clone(),
callback_url: session.callback_url.clone(),
locale: session.locale.clone(),
};
let client = reqwest::Client::new();
let response = client
.post(&url)
.basic_auth(&self.api_key, Some(&self.api_secret))
.json(&request)
.send()
.await?;
if !response.status().is_success() {
let error_text = response.text().await?;
return Err(format!("Idenfy API error: {}", error_text).into());
}
let token_response: IdenfyTokenResponse = response.json().await?;
// Update session with token and URL
session.set_session_token(token_response.auth_token.clone());
// Construct verification URL
let verification_url = format!(
"https://ivs.idenfy.com/api/v2/redirect?authToken={}",
token_response.auth_token
);
session.set_verification_url(verification_url.clone());
session.set_status(SessionStatus::Active);
Ok(verification_url)
}
/// Get verification status (Idenfy implementation)
pub async fn get_verification_status(
&self,
scan_ref: &str,
) -> Result<IdenfyVerificationStatus, Box<dyn std::error::Error>> {
match self.provider.as_str() {
"idenfy" => self.get_idenfy_status(scan_ref).await,
_ => Err(format!("Provider {} not yet implemented", self.provider).into()),
}
}
/// Get Idenfy verification status
async fn get_idenfy_status(
&self,
scan_ref: &str,
) -> Result<IdenfyVerificationStatus, Box<dyn std::error::Error>> {
let url = format!("{}/status/{}", self.get_base_url(), scan_ref);
let client = reqwest::Client::new();
let response = client
.get(&url)
.basic_auth(&self.api_key, Some(&self.api_secret))
.send()
.await?;
if !response.status().is_success() {
let error_text = response.text().await?;
return Err(format!("Idenfy API error: {}", error_text).into());
}
let status: IdenfyVerificationStatus = response.json().await?;
Ok(status)
}
}

View File

@@ -0,0 +1,319 @@
/// KYC Info Object
///
/// Represents customer/person information for KYC verification.
/// Designed to be provider-agnostic but follows Idenfy API patterns.
use crate::store::{BaseData, Object, Storable};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, Default, crate::DeriveObject)]
pub struct KycInfo {
#[serde(flatten)]
pub base_data: BaseData,
/// External client ID (from your system) - links to User
pub client_id: String,
/// Full name (or separate first/last)
pub full_name: String,
/// First name
pub first_name: String,
/// Last name
pub last_name: String,
/// Email address
pub email: Option<String>,
/// Phone number
pub phone: Option<String>,
/// Date of birth (YYYY-MM-DD string or unix timestamp)
pub date_of_birth: Option<String>,
/// Date of birth as unix timestamp
pub date_of_birth_timestamp: Option<u64>,
/// Nationality (ISO 3166-1 alpha-2 code)
pub nationality: Option<String>,
/// Address
pub address: Option<String>,
/// City
pub city: Option<String>,
/// Country (ISO 3166-1 alpha-2 code)
pub country: Option<String>,
/// Postal code
pub postal_code: Option<String>,
/// ID document number
pub id_number: Option<String>,
/// ID document type (passport, drivers_license, national_id, etc.)
pub id_type: Option<String>,
/// ID document expiry (unix timestamp)
pub id_expiry: Option<u64>,
/// KYC provider (e.g., "idenfy", "sumsub", "onfido")
pub provider: String,
/// Provider-specific client ID (assigned by KYC provider)
pub provider_client_id: Option<String>,
/// Current verification status
pub verification_status: VerificationStatus,
/// Whether KYC is verified
pub kyc_verified: bool,
/// User ID who verified this KYC
pub kyc_verified_by: Option<u32>,
/// Timestamp when KYC was verified
pub kyc_verified_at: Option<u64>,
/// Reason for rejection if denied
pub kyc_rejected_reason: Option<String>,
/// Signature ID for verification record
pub kyc_signature: Option<u32>,
/// Additional metadata
#[serde(default)]
pub metadata: std::collections::HashMap<String, String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "UPPERCASE")]
pub enum VerificationStatus {
/// Not yet started
Pending,
/// Verification in progress
Processing,
/// Successfully verified
Approved,
/// Verification failed
Denied,
/// Verification expired
Expired,
/// Requires manual review
Review,
}
impl Default for VerificationStatus {
fn default() -> Self {
VerificationStatus::Pending
}
}
impl KycInfo {
/// Create a new KYC info object
pub fn new(id: u32) -> Self {
let mut base_data = BaseData::new();
base_data.id = id;
Self {
base_data,
client_id: String::new(),
full_name: String::new(),
first_name: String::new(),
last_name: String::new(),
email: None,
phone: None,
date_of_birth: None,
date_of_birth_timestamp: None,
nationality: None,
address: None,
city: None,
country: None,
postal_code: None,
id_number: None,
id_type: None,
id_expiry: None,
provider: "idenfy".to_string(), // Default to Idenfy
provider_client_id: None,
verification_status: VerificationStatus::default(),
kyc_verified: false,
kyc_verified_by: None,
kyc_verified_at: None,
kyc_rejected_reason: None,
kyc_signature: None,
metadata: std::collections::HashMap::new(),
}
}
/// Builder: Set client ID
pub fn client_id(mut self, client_id: String) -> Self {
self.client_id = client_id;
self.base_data.update_modified();
self
}
/// Builder: Set full name
pub fn full_name(mut self, full_name: String) -> Self {
self.full_name = full_name.clone();
// Try to split into first/last if not already set
if self.first_name.is_empty() && self.last_name.is_empty() {
let parts: Vec<&str> = full_name.split_whitespace().collect();
if parts.len() >= 2 {
self.first_name = parts[0].to_string();
self.last_name = parts[1..].join(" ");
} else if parts.len() == 1 {
self.first_name = parts[0].to_string();
}
}
self.base_data.update_modified();
self
}
/// Builder: Set first name
pub fn first_name(mut self, first_name: String) -> Self {
self.first_name = first_name.clone();
// Update full_name if last_name exists
if !self.last_name.is_empty() {
self.full_name = format!("{} {}", first_name, self.last_name);
} else {
self.full_name = first_name;
}
self.base_data.update_modified();
self
}
/// Builder: Set last name
pub fn last_name(mut self, last_name: String) -> Self {
self.last_name = last_name.clone();
// Update full_name if first_name exists
if !self.first_name.is_empty() {
self.full_name = format!("{} {}", self.first_name, last_name);
} else {
self.full_name = last_name;
}
self.base_data.update_modified();
self
}
/// Builder: Set email
pub fn email(mut self, email: String) -> Self {
self.email = Some(email);
self.base_data.update_modified();
self
}
/// Builder: Set phone
pub fn phone(mut self, phone: String) -> Self {
self.phone = Some(phone);
self.base_data.update_modified();
self
}
/// Builder: Set date of birth
pub fn date_of_birth(mut self, dob: String) -> Self {
self.date_of_birth = Some(dob);
self.base_data.update_modified();
self
}
/// Builder: Set nationality
pub fn nationality(mut self, nationality: String) -> Self {
self.nationality = Some(nationality);
self.base_data.update_modified();
self
}
/// Builder: Set address
pub fn address(mut self, address: String) -> Self {
self.address = Some(address);
self.base_data.update_modified();
self
}
/// Builder: Set city
pub fn city(mut self, city: String) -> Self {
self.city = Some(city);
self.base_data.update_modified();
self
}
/// Builder: Set country
pub fn country(mut self, country: String) -> Self {
self.country = Some(country);
self.base_data.update_modified();
self
}
/// Builder: Set postal code
pub fn postal_code(mut self, postal_code: String) -> Self {
self.postal_code = Some(postal_code);
self.base_data.update_modified();
self
}
/// Builder: Set ID number
pub fn id_number(mut self, id_number: String) -> Self {
self.id_number = Some(id_number);
self.base_data.update_modified();
self
}
/// Builder: Set ID type
pub fn id_type(mut self, id_type: String) -> Self {
self.id_type = Some(id_type);
self.base_data.update_modified();
self
}
/// Builder: Set ID expiry
pub fn id_expiry(mut self, id_expiry: u64) -> Self {
self.id_expiry = Some(id_expiry);
self.base_data.update_modified();
self
}
/// Builder: Set KYC provider
pub fn provider(mut self, provider: String) -> Self {
self.provider = provider;
self.base_data.update_modified();
self
}
/// Set provider client ID (assigned by KYC provider)
pub fn set_provider_client_id(&mut self, provider_client_id: String) {
self.provider_client_id = Some(provider_client_id);
self.base_data.update_modified();
}
/// Set verification status
pub fn set_verification_status(&mut self, status: VerificationStatus) {
self.verification_status = status;
self.base_data.update_modified();
}
/// Set KYC verified
pub fn set_kyc_verified(&mut self, verified: bool, verified_by: Option<u32>) {
self.kyc_verified = verified;
self.kyc_verified_by = verified_by;
self.kyc_verified_at = Some(std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs());
self.base_data.update_modified();
}
/// Set KYC rejected
pub fn set_kyc_rejected(&mut self, reason: String) {
self.kyc_verified = false;
self.kyc_rejected_reason = Some(reason);
self.verification_status = VerificationStatus::Denied;
self.base_data.update_modified();
}
/// Add metadata
pub fn add_metadata(&mut self, key: String, value: String) {
self.metadata.insert(key, value);
self.base_data.update_modified();
}
}

View File

@@ -0,0 +1,13 @@
/// KYC (Know Your Customer) Module
///
/// Provides generic KYC client and session management.
/// Designed to work with multiple KYC providers (Idenfy, Sumsub, Onfido, etc.)
pub mod info;
pub mod client;
pub mod session;
pub mod rhai;
pub use info::{KycInfo, VerificationStatus};
pub use client::KycClient;
pub use session::{KycSession, SessionStatus, SessionResult};

View File

@@ -0,0 +1,376 @@
/// Rhai bindings for KYC objects
use ::rhai::plugin::*;
use ::rhai::{CustomType, Dynamic, Engine, EvalAltResult, Module, TypeBuilder};
use super::info::{KycInfo, VerificationStatus};
use super::session::{KycSession, SessionStatus};
use super::client::KycClient;
// ============================================================================
// KYC Info Module
// ============================================================================
type RhaiKycInfo = KycInfo;
#[export_module]
mod rhai_kyc_info_module {
use super::RhaiKycInfo;
#[rhai_fn(name = "new_kyc_info", return_raw)]
pub fn new_kyc_info() -> Result<RhaiKycInfo, Box<EvalAltResult>> {
Ok(KycInfo::new(0))
}
#[rhai_fn(name = "client_id", return_raw)]
pub fn set_client_id(
info: &mut RhaiKycInfo,
client_id: String,
) -> Result<RhaiKycInfo, Box<EvalAltResult>> {
let owned = std::mem::take(info);
*info = owned.client_id(client_id);
Ok(info.clone())
}
#[rhai_fn(name = "first_name", return_raw)]
pub fn set_first_name(
info: &mut RhaiKycInfo,
first_name: String,
) -> Result<RhaiKycInfo, Box<EvalAltResult>> {
let owned = std::mem::take(info);
*info = owned.first_name(first_name);
Ok(info.clone())
}
#[rhai_fn(name = "last_name", return_raw)]
pub fn set_last_name(
info: &mut RhaiKycInfo,
last_name: String,
) -> Result<RhaiKycInfo, Box<EvalAltResult>> {
let owned = std::mem::take(info);
*info = owned.last_name(last_name);
Ok(info.clone())
}
#[rhai_fn(name = "email", return_raw)]
pub fn set_email(
info: &mut RhaiKycInfo,
email: String,
) -> Result<RhaiKycInfo, Box<EvalAltResult>> {
let owned = std::mem::take(info);
*info = owned.email(email);
Ok(info.clone())
}
#[rhai_fn(name = "phone", return_raw)]
pub fn set_phone(
info: &mut RhaiKycInfo,
phone: String,
) -> Result<RhaiKycInfo, Box<EvalAltResult>> {
let owned = std::mem::take(info);
*info = owned.phone(phone);
Ok(info.clone())
}
#[rhai_fn(name = "date_of_birth", return_raw)]
pub fn set_date_of_birth(
info: &mut RhaiKycInfo,
dob: String,
) -> Result<RhaiKycInfo, Box<EvalAltResult>> {
let owned = std::mem::take(info);
*info = owned.date_of_birth(dob);
Ok(info.clone())
}
#[rhai_fn(name = "nationality", return_raw)]
pub fn set_nationality(
info: &mut RhaiKycInfo,
nationality: String,
) -> Result<RhaiKycInfo, Box<EvalAltResult>> {
let owned = std::mem::take(info);
*info = owned.nationality(nationality);
Ok(info.clone())
}
#[rhai_fn(name = "address", return_raw)]
pub fn set_address(
info: &mut RhaiKycInfo,
address: String,
) -> Result<RhaiKycInfo, Box<EvalAltResult>> {
let owned = std::mem::take(info);
*info = owned.address(address);
Ok(info.clone())
}
#[rhai_fn(name = "city", return_raw)]
pub fn set_city(
info: &mut RhaiKycInfo,
city: String,
) -> Result<RhaiKycInfo, Box<EvalAltResult>> {
let owned = std::mem::take(info);
*info = owned.city(city);
Ok(info.clone())
}
#[rhai_fn(name = "country", return_raw)]
pub fn set_country(
info: &mut RhaiKycInfo,
country: String,
) -> Result<RhaiKycInfo, Box<EvalAltResult>> {
let owned = std::mem::take(info);
*info = owned.country(country);
Ok(info.clone())
}
#[rhai_fn(name = "postal_code", return_raw)]
pub fn set_postal_code(
info: &mut RhaiKycInfo,
postal_code: String,
) -> Result<RhaiKycInfo, Box<EvalAltResult>> {
let owned = std::mem::take(info);
*info = owned.postal_code(postal_code);
Ok(info.clone())
}
#[rhai_fn(name = "provider", return_raw)]
pub fn set_provider(
info: &mut RhaiKycInfo,
provider: String,
) -> Result<RhaiKycInfo, Box<EvalAltResult>> {
let owned = std::mem::take(info);
*info = owned.provider(provider);
Ok(info.clone())
}
#[rhai_fn(name = "document_type", return_raw)]
pub fn set_document_type(
info: &mut RhaiKycInfo,
doc_type: String,
) -> Result<RhaiKycInfo, Box<EvalAltResult>> {
// Store in provider field for now (or add to KycInfo struct)
let provider = info.provider.clone();
let owned = std::mem::take(info);
*info = owned.provider(format!("{}|doc_type:{}", provider, doc_type));
Ok(info.clone())
}
#[rhai_fn(name = "document_number", return_raw)]
pub fn set_document_number(
info: &mut RhaiKycInfo,
doc_number: String,
) -> Result<RhaiKycInfo, Box<EvalAltResult>> {
// Store in provider field for now (or add to KycInfo struct)
let provider = info.provider.clone();
let owned = std::mem::take(info);
*info = owned.provider(format!("{}|doc_num:{}", provider, doc_number));
Ok(info.clone())
}
#[rhai_fn(name = "verified", return_raw)]
pub fn set_verified(
info: &mut RhaiKycInfo,
_verified: bool,
) -> Result<RhaiKycInfo, Box<EvalAltResult>> {
// Mark as verified in provider field
let provider = info.provider.clone();
let owned = std::mem::take(info);
*info = owned.provider(format!("{}|verified", provider));
Ok(info.clone())
}
// Getters
#[rhai_fn(name = "get_id")]
pub fn get_id(info: &mut RhaiKycInfo) -> u32 {
info.base_data.id
}
#[rhai_fn(name = "get_client_id")]
pub fn get_client_id(info: &mut RhaiKycInfo) -> String {
info.client_id.clone()
}
#[rhai_fn(name = "get_first_name")]
pub fn get_first_name(info: &mut RhaiKycInfo) -> String {
info.first_name.clone()
}
#[rhai_fn(name = "get_last_name")]
pub fn get_last_name(info: &mut RhaiKycInfo) -> String {
info.last_name.clone()
}
#[rhai_fn(name = "get_email")]
pub fn get_email(info: &mut RhaiKycInfo) -> String {
info.email.clone().unwrap_or_default()
}
#[rhai_fn(name = "get_provider")]
pub fn get_provider(info: &mut RhaiKycInfo) -> String {
info.provider.clone()
}
}
// ============================================================================
// KYC Session Module
// ============================================================================
type RhaiKycSession = KycSession;
#[export_module]
mod rhai_kyc_session_module {
use super::RhaiKycSession;
#[rhai_fn(name = "new_kyc_session", return_raw)]
pub fn new_kyc_session(
client_id: String,
provider: String,
) -> Result<RhaiKycSession, Box<EvalAltResult>> {
Ok(KycSession::new(0, client_id, provider))
}
#[rhai_fn(name = "callback_url", return_raw)]
pub fn set_callback_url(
session: &mut RhaiKycSession,
url: String,
) -> Result<RhaiKycSession, Box<EvalAltResult>> {
let owned = std::mem::take(session);
*session = owned.callback_url(url);
Ok(session.clone())
}
#[rhai_fn(name = "success_url", return_raw)]
pub fn set_success_url(
session: &mut RhaiKycSession,
url: String,
) -> Result<RhaiKycSession, Box<EvalAltResult>> {
let owned = std::mem::take(session);
*session = owned.success_url(url);
Ok(session.clone())
}
#[rhai_fn(name = "error_url", return_raw)]
pub fn set_error_url(
session: &mut RhaiKycSession,
url: String,
) -> Result<RhaiKycSession, Box<EvalAltResult>> {
let owned = std::mem::take(session);
*session = owned.error_url(url);
Ok(session.clone())
}
#[rhai_fn(name = "locale", return_raw)]
pub fn set_locale(
session: &mut RhaiKycSession,
locale: String,
) -> Result<RhaiKycSession, Box<EvalAltResult>> {
let owned = std::mem::take(session);
*session = owned.locale(locale);
Ok(session.clone())
}
// Getters
#[rhai_fn(name = "get_id")]
pub fn get_id(session: &mut RhaiKycSession) -> u32 {
session.base_data.id
}
#[rhai_fn(name = "get_client_id")]
pub fn get_client_id(session: &mut RhaiKycSession) -> String {
session.client_id.clone()
}
#[rhai_fn(name = "get_provider")]
pub fn get_provider(session: &mut RhaiKycSession) -> String {
session.provider.clone()
}
#[rhai_fn(name = "get_verification_url")]
pub fn get_verification_url(session: &mut RhaiKycSession) -> String {
session.verification_url.clone().unwrap_or_default()
}
}
// ============================================================================
// KYC Client Module
// ============================================================================
type RhaiKycClient = KycClient;
#[export_module]
mod rhai_kyc_client_module {
use super::RhaiKycClient;
use super::RhaiKycInfo;
use super::RhaiKycSession;
use ::rhai::EvalAltResult;
#[rhai_fn(name = "new_kyc_client_idenfy", return_raw)]
pub fn new_idenfy_client(
api_key: String,
api_secret: String,
) -> Result<RhaiKycClient, Box<EvalAltResult>> {
Ok(KycClient::idenfy(api_key, api_secret))
}
#[rhai_fn(name = "create_verification_session", return_raw)]
pub fn create_verification_session(
client: &mut RhaiKycClient,
kyc_info: RhaiKycInfo,
session: RhaiKycSession,
) -> Result<String, Box<EvalAltResult>> {
// Need to use tokio runtime for async call
let rt = tokio::runtime::Runtime::new()
.map_err(|e| format!("Failed to create runtime: {}", e))?;
let mut session_mut = session.clone();
let url = rt.block_on(client.create_verification_session(&kyc_info, &mut session_mut))
.map_err(|e| format!("Failed to create verification session: {}", e))?;
Ok(url)
}
}
// ============================================================================
// Registration Functions
// ============================================================================
/// Register KYC modules into a Rhai Module (for use in packages)
pub fn register_kyc_modules(parent_module: &mut Module) {
// Register custom types
parent_module.set_custom_type::<KycInfo>("KycInfo");
parent_module.set_custom_type::<KycSession>("KycSession");
parent_module.set_custom_type::<KycClient>("KycClient");
// Merge KYC info functions
let info_module = exported_module!(rhai_kyc_info_module);
parent_module.merge(&info_module);
// Merge KYC session functions
let session_module = exported_module!(rhai_kyc_session_module);
parent_module.merge(&session_module);
// Merge KYC client functions
let client_module = exported_module!(rhai_kyc_client_module);
parent_module.merge(&client_module);
}
// ============================================================================
// CustomType Implementations
// ============================================================================
impl CustomType for KycInfo {
fn build(mut builder: TypeBuilder<Self>) {
builder.with_name("KycInfo");
}
}
impl CustomType for KycSession {
fn build(mut builder: TypeBuilder<Self>) {
builder.with_name("KycSession");
}
}
impl CustomType for KycClient {
fn build(mut builder: TypeBuilder<Self>) {
builder.with_name("KycClient");
}
}

View File

@@ -0,0 +1,186 @@
/// KYC Verification Session
///
/// Represents a verification session for a KYC client.
/// Follows Idenfy API patterns but is provider-agnostic.
use crate::store::{BaseData, Object, Storable};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, Default, crate::DeriveObject)]
pub struct KycSession {
#[serde(flatten)]
pub base_data: BaseData,
/// Reference to the KYC client
pub client_id: String,
/// KYC provider
pub provider: String,
/// Session token/ID from provider
pub session_token: Option<String>,
/// Verification URL for the client
pub verification_url: Option<String>,
/// Session status
pub status: SessionStatus,
/// Session expiration timestamp
pub expires_at: Option<i64>,
/// Callback URL for webhook notifications
pub callback_url: Option<String>,
/// Success redirect URL
pub success_url: Option<String>,
/// Error redirect URL
pub error_url: Option<String>,
/// Locale (e.g., "en", "de", "fr")
pub locale: Option<String>,
/// Provider-specific configuration
#[serde(default)]
pub provider_config: std::collections::HashMap<String, String>,
/// Session result data
pub result: Option<SessionResult>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
#[serde(rename_all = "UPPERCASE")]
pub enum SessionStatus {
/// Session created but not started
#[default]
Created,
/// Client is currently verifying
Active,
/// Session completed successfully
Completed,
/// Session failed
Failed,
/// Session expired
Expired,
/// Session cancelled
Cancelled,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SessionResult {
/// Overall verification status
pub status: String,
/// Verification score (0-100)
pub score: Option<f64>,
/// Reason for denial (if denied)
pub denial_reason: Option<String>,
/// Document type verified
pub document_type: Option<String>,
/// Document number
pub document_number: Option<String>,
/// Document issuing country
pub document_country: Option<String>,
/// Face match result
pub face_match: Option<bool>,
/// Liveness check result
pub liveness_check: Option<bool>,
/// Additional provider-specific data
#[serde(default)]
pub provider_data: std::collections::HashMap<String, serde_json::Value>,
}
impl KycSession {
/// Create a new KYC session
pub fn new(id: u32, client_id: String, provider: String) -> Self {
let mut base_data = BaseData::new();
base_data.id = id;
Self {
base_data,
client_id,
provider,
session_token: None,
verification_url: None,
status: SessionStatus::Created,
expires_at: None,
callback_url: None,
success_url: None,
error_url: None,
locale: None,
provider_config: std::collections::HashMap::new(),
result: None,
}
}
/// Builder: Set callback URL
pub fn callback_url(mut self, url: String) -> Self {
self.callback_url = Some(url);
self.base_data.update_modified();
self
}
/// Builder: Set success URL
pub fn success_url(mut self, url: String) -> Self {
self.success_url = Some(url);
self.base_data.update_modified();
self
}
/// Builder: Set error URL
pub fn error_url(mut self, url: String) -> Self {
self.error_url = Some(url);
self.base_data.update_modified();
self
}
/// Builder: Set locale
pub fn locale(mut self, locale: String) -> Self {
self.locale = Some(locale);
self.base_data.update_modified();
self
}
/// Set session token from provider
pub fn set_session_token(&mut self, token: String) {
self.session_token = Some(token);
self.base_data.update_modified();
}
/// Set verification URL
pub fn set_verification_url(&mut self, url: String) {
self.verification_url = Some(url);
self.base_data.update_modified();
}
/// Set session status
pub fn set_status(&mut self, status: SessionStatus) {
self.status = status;
self.base_data.update_modified();
}
/// Set expiration timestamp
pub fn set_expires_at(&mut self, timestamp: i64) {
self.expires_at = Some(timestamp);
self.base_data.update_modified();
}
/// Set session result
pub fn set_result(&mut self, result: SessionResult) {
self.result = Some(result);
self.base_data.update_modified();
}
/// Add provider-specific configuration
pub fn add_provider_config(&mut self, key: String, value: String) {
self.provider_config.insert(key, value);
self.base_data.update_modified();
}
}

View File

@@ -0,0 +1,129 @@
/// Legal Contract Object
///
/// Simple contract object with signatures for legal agreements
use crate::store::{BaseData, Object};
use serde::{Deserialize, Serialize};
/// Contract status
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum ContractStatus {
Draft,
Active,
Completed,
Cancelled,
}
impl Default for ContractStatus {
fn default() -> Self {
ContractStatus::Draft
}
}
/// Legal contract with signatures
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, crate::DeriveObject)]
pub struct Contract {
/// Base data for object storage
pub base_data: BaseData,
/// Contract title
pub title: String,
/// Contract content/terms
pub content: String,
/// Contract status
pub status: ContractStatus,
/// List of signature IDs (references to Signature objects)
pub signatures: Vec<u32>,
/// Creator user ID
pub creator_id: u32,
/// Expiry timestamp (optional)
pub expires_at: Option<u64>,
}
impl Contract {
/// Create a new contract
pub fn new(id: u32) -> Self {
let base_data = BaseData::with_id(id, String::new());
Self {
base_data,
title: String::new(),
content: String::new(),
status: ContractStatus::default(),
signatures: Vec::new(),
creator_id: 0,
expires_at: None,
}
}
/// Set the title (fluent)
pub fn title(mut self, title: impl ToString) -> Self {
self.title = title.to_string();
self
}
/// Set the content (fluent)
pub fn content(mut self, content: impl ToString) -> Self {
self.content = content.to_string();
self
}
/// Set the status (fluent)
pub fn status(mut self, status: ContractStatus) -> Self {
self.status = status;
self
}
/// Set the creator ID (fluent)
pub fn creator_id(mut self, creator_id: u32) -> Self {
self.creator_id = creator_id;
self
}
/// Set the expiry timestamp (fluent)
pub fn expires_at(mut self, expires_at: u64) -> Self {
self.expires_at = Some(expires_at);
self
}
/// Add a signature (fluent)
pub fn add_signature(mut self, signature_id: u32) -> Self {
if !self.signatures.contains(&signature_id) {
self.signatures.push(signature_id);
}
self
}
/// Remove a signature (fluent)
pub fn remove_signature(mut self, signature_id: u32) -> Self {
self.signatures.retain(|&id| id != signature_id);
self
}
/// Check if all required signatures are present
pub fn is_fully_signed(&self, required_count: usize) -> bool {
self.signatures.len() >= required_count
}
/// Activate the contract
pub fn activate(mut self) -> Self {
self.status = ContractStatus::Active;
self
}
/// Complete the contract
pub fn complete(mut self) -> Self {
self.status = ContractStatus::Completed;
self
}
/// Cancel the contract
pub fn cancel(mut self) -> Self {
self.status = ContractStatus::Cancelled;
self
}
}

View File

@@ -0,0 +1,7 @@
/// Legal module for contracts and legal documents
pub mod contract;
pub mod rhai;
pub use contract::{Contract, ContractStatus};
pub use rhai::register_legal_modules;

View File

@@ -0,0 +1,150 @@
/// Rhai bindings for Legal objects (Contract)
use ::rhai::plugin::*;
use ::rhai::{CustomType, Dynamic, Engine, EvalAltResult, Module, TypeBuilder};
use super::{Contract, ContractStatus};
/// Register legal modules with the Rhai engine
pub fn register_legal_modules(parent_module: &mut Module) {
// Register custom types
parent_module.set_custom_type::<Contract>("Contract");
parent_module.set_custom_type::<ContractStatus>("ContractStatus");
// Merge contract functions
let contract_module = exported_module!(rhai_contract_module);
parent_module.merge(&contract_module);
}
// ============================================================================
// Contract Module
// ============================================================================
type RhaiContract = Contract;
type RhaiContractStatus = ContractStatus;
#[export_module]
mod rhai_contract_module {
use super::{RhaiContract, RhaiContractStatus};
use super::super::{Contract, ContractStatus};
use ::rhai::EvalAltResult;
// Contract constructor
#[rhai_fn(name = "new_contract", return_raw)]
pub fn new_contract(id: i64) -> Result<RhaiContract, Box<EvalAltResult>> {
Ok(Contract::new(id as u32))
}
// Builder methods
#[rhai_fn(name = "title", return_raw)]
pub fn set_title(
contract: RhaiContract,
title: String,
) -> Result<RhaiContract, Box<EvalAltResult>> {
Ok(contract.title(title))
}
#[rhai_fn(name = "content", return_raw)]
pub fn set_content(
contract: RhaiContract,
content: String,
) -> Result<RhaiContract, Box<EvalAltResult>> {
Ok(contract.content(content))
}
#[rhai_fn(name = "creator_id", return_raw)]
pub fn set_creator_id(
contract: RhaiContract,
creator_id: i64,
) -> Result<RhaiContract, Box<EvalAltResult>> {
Ok(contract.creator_id(creator_id as u32))
}
#[rhai_fn(name = "expires_at", return_raw)]
pub fn set_expires_at(
contract: RhaiContract,
expires_at: i64,
) -> Result<RhaiContract, Box<EvalAltResult>> {
Ok(contract.expires_at(expires_at as u64))
}
#[rhai_fn(name = "add_signature", return_raw)]
pub fn add_signature(
contract: RhaiContract,
signature_id: i64,
) -> Result<RhaiContract, Box<EvalAltResult>> {
Ok(contract.add_signature(signature_id as u32))
}
#[rhai_fn(name = "remove_signature", return_raw)]
pub fn remove_signature(
contract: RhaiContract,
signature_id: i64,
) -> Result<RhaiContract, Box<EvalAltResult>> {
Ok(contract.remove_signature(signature_id as u32))
}
// State management methods
#[rhai_fn(name = "activate", return_raw)]
pub fn activate(contract: RhaiContract) -> Result<RhaiContract, Box<EvalAltResult>> {
Ok(contract.activate())
}
#[rhai_fn(name = "complete", return_raw)]
pub fn complete(contract: RhaiContract) -> Result<RhaiContract, Box<EvalAltResult>> {
Ok(contract.complete())
}
#[rhai_fn(name = "cancel", return_raw)]
pub fn cancel(contract: RhaiContract) -> Result<RhaiContract, Box<EvalAltResult>> {
Ok(contract.cancel())
}
// Query methods
#[rhai_fn(name = "is_fully_signed", pure)]
pub fn is_fully_signed(contract: &mut RhaiContract, required_count: i64) -> bool {
contract.is_fully_signed(required_count as usize)
}
// Getters
#[rhai_fn(name = "title", pure)]
pub fn get_title(contract: &mut RhaiContract) -> String {
contract.title.clone()
}
#[rhai_fn(name = "content", pure)]
pub fn get_content(contract: &mut RhaiContract) -> String {
contract.content.clone()
}
#[rhai_fn(name = "status", pure)]
pub fn get_status(contract: &mut RhaiContract) -> String {
format!("{:?}", contract.status)
}
#[rhai_fn(name = "creator_id", pure)]
pub fn get_creator_id(contract: &mut RhaiContract) -> i64 {
contract.creator_id as i64
}
#[rhai_fn(name = "signature_count", pure)]
pub fn get_signature_count(contract: &mut RhaiContract) -> i64 {
contract.signatures.len() as i64
}
}
// ============================================================================
// CustomType Implementations
// ============================================================================
impl CustomType for Contract {
fn build(mut builder: TypeBuilder<Self>) {
builder.with_name("Contract");
}
}
impl CustomType for ContractStatus {
fn build(mut builder: TypeBuilder<Self>) {
builder.with_name("ContractStatus");
}
}

View File

@@ -0,0 +1,19 @@
pub mod accounting;
pub mod communication;
pub mod event;
pub mod flow;
pub mod grid4;
pub mod heroledger;
pub mod kyc;
pub mod legal;
pub mod money;
pub mod note;
pub mod supervisor;
pub use note::Note;
pub use event::Event;
pub use kyc::{KycInfo, KycSession};
pub use flow::{FlowTemplate, FlowInstance};
pub use communication::{Verification, EmailClient};
pub use money::{Account, Asset, Transaction, PaymentClient};
pub use legal::{Contract, ContractStatus};

View File

@@ -0,0 +1,10 @@
/// Money Module
///
/// Financial objects including accounts, assets, transactions, and payment providers.
pub mod models;
pub mod rhai;
pub mod payments;
pub use models::{Account, Asset, Transaction, AccountStatus, TransactionType, Signature, AccountPolicyItem};
pub use payments::{PaymentClient, PaymentRequest, PaymentResponse, PaymentStatus};

View File

@@ -0,0 +1,498 @@
use crate::store::{BaseData, IndexKey, Object};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
/// Represents the status of an account
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum AccountStatus {
Active,
Inactive,
Suspended,
Archived,
}
impl Default for AccountStatus {
fn default() -> Self {
AccountStatus::Active
}
}
/// Represents the type of transaction
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum TransactionType {
Transfer,
Clawback,
Freeze,
Unfreeze,
Issue,
Burn,
}
impl Default for TransactionType {
fn default() -> Self {
TransactionType::Transfer
}
}
/// Represents a signature for transactions
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Signature {
pub signer_id: u32,
pub signature: String,
pub timestamp: u64,
}
impl Signature {
pub fn new() -> Self {
Self {
signer_id: 0,
signature: String::new(),
timestamp: 0,
}
}
pub fn signer_id(mut self, signer_id: u32) -> Self {
self.signer_id = signer_id;
self
}
pub fn signature(mut self, signature: impl ToString) -> Self {
self.signature = signature.to_string();
self
}
pub fn timestamp(mut self, timestamp: u64) -> Self {
self.timestamp = timestamp;
self
}
pub fn build(self) -> Self {
self
}
}
/// Policy item for account operations
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
pub struct AccountPolicyItem {
pub signers: Vec<u32>,
pub min_signatures: u32,
pub enabled: bool,
pub threshold: f64,
pub recipient: u32,
}
impl AccountPolicyItem {
pub fn new() -> Self {
Self {
signers: Vec::new(),
min_signatures: 0,
enabled: false,
threshold: 0.0,
recipient: 0,
}
}
pub fn add_signer(mut self, signer_id: u32) -> Self {
self.signers.push(signer_id);
self
}
pub fn signers(mut self, signers: Vec<u32>) -> Self {
self.signers = signers;
self
}
pub fn min_signatures(mut self, min_signatures: u32) -> Self {
self.min_signatures = min_signatures;
self
}
pub fn enabled(mut self, enabled: bool) -> Self {
self.enabled = enabled;
self
}
pub fn threshold(mut self, threshold: f64) -> Self {
self.threshold = threshold;
self
}
pub fn recipient(mut self, recipient: u32) -> Self {
self.recipient = recipient;
self
}
pub fn build(self) -> Self {
self
}
}
/// Represents an account in the financial system
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default, crate::DeriveObject)]
pub struct Account {
/// Base model data
pub base_data: BaseData,
pub owner_id: u32,
#[index]
pub address: String,
pub balance: f64,
pub currency: String,
pub assetid: u32,
pub last_activity: u64,
pub administrators: Vec<u32>,
pub accountpolicy: u32,
}
impl Account {
/// Create a new account instance
pub fn new(id: u32) -> Self {
let mut base_data = BaseData::new();
Self {
base_data,
owner_id: 0,
address: String::new(),
balance: 0.0,
currency: String::new(),
assetid: 0,
last_activity: 0,
administrators: Vec::new(),
accountpolicy: 0,
}
}
/// Set the owner ID (fluent)
pub fn owner_id(mut self, owner_id: u32) -> Self {
self.owner_id = owner_id;
self
}
/// Set the blockchain address (fluent)
pub fn address(mut self, address: impl ToString) -> Self {
self.address = address.to_string();
self
}
/// Set the balance (fluent)
pub fn balance(mut self, balance: f64) -> Self {
self.balance = balance;
self
}
/// Set the currency (fluent)
pub fn currency(mut self, currency: impl ToString) -> Self {
self.currency = currency.to_string();
self
}
/// Set the asset ID (fluent)
pub fn assetid(mut self, assetid: u32) -> Self {
self.assetid = assetid;
self
}
/// Set the last activity timestamp (fluent)
pub fn last_activity(mut self, last_activity: u64) -> Self {
self.last_activity = last_activity;
self
}
/// Add an administrator (fluent)
pub fn add_administrator(mut self, admin_id: u32) -> Self {
self.administrators.push(admin_id);
self
}
/// Set all administrators (fluent)
pub fn administrators(mut self, administrators: Vec<u32>) -> Self {
self.administrators = administrators;
self
}
/// Set the account policy ID (fluent)
pub fn accountpolicy(mut self, accountpolicy: u32) -> Self {
self.accountpolicy = accountpolicy;
self
}
/// Build the final account instance
pub fn build(self) -> Self {
self
}
}
/// Represents an asset in the financial system
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default, crate::DeriveObject)]
pub struct Asset {
/// Base model data
pub base_data: BaseData,
#[index]
pub address: String,
pub assetid: u32,
pub asset_type: String,
pub issuer: u32,
pub supply: f64,
pub decimals: u8,
pub is_frozen: bool,
pub metadata: HashMap<String, String>,
pub administrators: Vec<u32>,
pub min_signatures: u32,
}
impl Asset {
/// Create a new asset instance
pub fn new(id: u32) -> Self {
let mut base_data = BaseData::new();
Self {
base_data,
address: String::new(),
assetid: 0,
asset_type: String::new(),
issuer: 0,
supply: 0.0,
decimals: 0,
is_frozen: false,
metadata: HashMap::new(),
administrators: Vec::new(),
min_signatures: 0,
}
}
/// Set the blockchain address (fluent)
pub fn address(mut self, address: impl ToString) -> Self {
self.address = address.to_string();
self
}
/// Set the asset ID (fluent)
pub fn assetid(mut self, assetid: u32) -> Self {
self.assetid = assetid;
self
}
/// Set the asset type (fluent)
pub fn asset_type(mut self, asset_type: impl ToString) -> Self {
self.asset_type = asset_type.to_string();
self
}
/// Set the issuer (fluent)
pub fn issuer(mut self, issuer: u32) -> Self {
self.issuer = issuer;
self
}
/// Set the supply (fluent)
pub fn supply(mut self, supply: f64) -> Self {
self.supply = supply;
self
}
/// Set the decimals (fluent)
pub fn decimals(mut self, decimals: u8) -> Self {
self.decimals = decimals;
self
}
/// Set the frozen status (fluent)
pub fn is_frozen(mut self, is_frozen: bool) -> Self {
self.is_frozen = is_frozen;
self
}
/// Add metadata entry (fluent)
pub fn add_metadata(mut self, key: impl ToString, value: impl ToString) -> Self {
self.metadata.insert(key.to_string(), value.to_string());
self
}
/// Set all metadata (fluent)
pub fn metadata(mut self, metadata: HashMap<String, String>) -> Self {
self.metadata = metadata;
self
}
/// Add an administrator (fluent)
pub fn add_administrator(mut self, admin_id: u32) -> Self {
self.administrators.push(admin_id);
self
}
/// Set all administrators (fluent)
pub fn administrators(mut self, administrators: Vec<u32>) -> Self {
self.administrators = administrators;
self
}
/// Set minimum signatures required (fluent)
pub fn min_signatures(mut self, min_signatures: u32) -> Self {
self.min_signatures = min_signatures;
self
}
/// Build the final asset instance
pub fn build(self) -> Self {
self
}
}
/// Represents account policies for various operations
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default, crate::DeriveObject)]
pub struct AccountPolicy {
/// Base model data
pub base_data: BaseData,
pub transferpolicy: AccountPolicyItem,
pub adminpolicy: AccountPolicyItem,
pub clawbackpolicy: AccountPolicyItem,
pub freezepolicy: AccountPolicyItem,
}
impl AccountPolicy {
/// Create a new account policy instance
pub fn new(id: u32) -> Self {
let mut base_data = BaseData::new();
Self {
base_data,
transferpolicy: AccountPolicyItem::new(),
adminpolicy: AccountPolicyItem::new(),
clawbackpolicy: AccountPolicyItem::new(),
freezepolicy: AccountPolicyItem::new(),
}
}
/// Set the transfer policy (fluent)
pub fn transferpolicy(mut self, transferpolicy: AccountPolicyItem) -> Self {
self.transferpolicy = transferpolicy;
self
}
/// Set the admin policy (fluent)
pub fn adminpolicy(mut self, adminpolicy: AccountPolicyItem) -> Self {
self.adminpolicy = adminpolicy;
self
}
/// Set the clawback policy (fluent)
pub fn clawbackpolicy(mut self, clawbackpolicy: AccountPolicyItem) -> Self {
self.clawbackpolicy = clawbackpolicy;
self
}
/// Set the freeze policy (fluent)
pub fn freezepolicy(mut self, freezepolicy: AccountPolicyItem) -> Self {
self.freezepolicy = freezepolicy;
self
}
/// Build the final account policy instance
pub fn build(self) -> Self {
self
}
}
/// Represents a financial transaction
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default, crate::DeriveObject)]
pub struct Transaction {
/// Base model data
pub base_data: BaseData,
pub txid: u32,
pub source: u32,
pub destination: u32,
pub assetid: u32,
pub amount: f64,
pub timestamp: u64,
pub status: String,
pub memo: String,
pub tx_type: TransactionType,
pub signatures: Vec<Signature>,
}
impl Transaction {
/// Create a new transaction instance
pub fn new(id: u32) -> Self {
let mut base_data = BaseData::new();
Self {
base_data,
txid: 0,
source: 0,
destination: 0,
assetid: 0,
amount: 0.0,
timestamp: 0,
status: String::new(),
memo: String::new(),
tx_type: TransactionType::default(),
signatures: Vec::new(),
}
}
/// Set the transaction ID (fluent)
pub fn txid(mut self, txid: u32) -> Self {
self.txid = txid;
self
}
/// Set the source account (fluent)
pub fn source(mut self, source: u32) -> Self {
self.source = source;
self
}
/// Set the destination account (fluent)
pub fn destination(mut self, destination: u32) -> Self {
self.destination = destination;
self
}
/// Set the asset ID (fluent)
pub fn assetid(mut self, assetid: u32) -> Self {
self.assetid = assetid;
self
}
/// Set the amount (fluent)
pub fn amount(mut self, amount: f64) -> Self {
self.amount = amount;
self
}
/// Set the timestamp (fluent)
pub fn timestamp(mut self, timestamp: u64) -> Self {
self.timestamp = timestamp;
self
}
/// Set the status (fluent)
pub fn status(mut self, status: impl ToString) -> Self {
self.status = status.to_string();
self
}
/// Set the memo (fluent)
pub fn memo(mut self, memo: impl ToString) -> Self {
self.memo = memo.to_string();
self
}
/// Set the transaction type (fluent)
pub fn tx_type(mut self, tx_type: TransactionType) -> Self {
self.tx_type = tx_type;
self
}
/// Add a signature (fluent)
pub fn add_signature(mut self, signature: Signature) -> Self {
self.signatures.push(signature);
self
}
/// Set all signatures (fluent)
pub fn signatures(mut self, signatures: Vec<Signature>) -> Self {
self.signatures = signatures;
self
}
/// Build the final transaction instance
pub fn build(self) -> Self {
self
}
}

View File

@@ -0,0 +1,457 @@
/// Payment Provider Client
///
/// Generic payment provider API client supporting multiple payment gateways.
/// Currently implements Pesapal API but designed to be extensible for other providers.
use serde::{Deserialize, Serialize};
use crate::store::{BaseData, IndexKey, Object};
// Helper to run async code synchronously
fn run_async<F, T>(future: F) -> T
where
F: std::future::Future<Output = T> + Send + 'static,
T: Send + 'static,
{
// Try to use current runtime handle if available
if tokio::runtime::Handle::try_current().is_ok() {
// We're in a runtime, spawn a blocking thread with its own runtime
std::thread::scope(|s| {
s.spawn(|| {
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(future)
}).join().unwrap()
})
} else {
// No runtime, create one
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(future)
}
}
/// Payment Provider Client for making API calls to payment gateways
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PaymentClient {
/// Base data for object storage
pub base_data: BaseData,
/// Provider name (e.g., "pesapal", "stripe", "paypal", "flutterwave")
pub provider: String,
/// Consumer key / API key
pub consumer_key: String,
/// Consumer secret / API secret
pub consumer_secret: String,
/// Base URL for API (optional, uses provider default if not set)
pub base_url: Option<String>,
/// Sandbox mode (for testing)
pub sandbox: bool,
}
/// Payment request details
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PaymentRequest {
/// Unique merchant reference
pub merchant_reference: String,
/// Amount to charge
pub amount: f64,
/// Currency code (e.g., "USD", "KES", "UGX")
pub currency: String,
/// Description of the payment
pub description: String,
/// Callback URL for payment notifications
pub callback_url: String,
/// Redirect URL after payment (optional)
pub redirect_url: Option<String>,
/// Cancel URL (optional)
pub cancel_url: Option<String>,
/// Customer email
pub customer_email: Option<String>,
/// Customer phone
pub customer_phone: Option<String>,
/// Customer first name
pub customer_first_name: Option<String>,
/// Customer last name
pub customer_last_name: Option<String>,
}
/// Payment response from provider
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PaymentResponse {
/// Payment link URL
pub payment_url: String,
/// Order tracking ID from provider
pub order_tracking_id: String,
/// Merchant reference
pub merchant_reference: String,
/// Status message
pub status: String,
}
/// Payment status query result
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PaymentStatus {
/// Order tracking ID
pub order_tracking_id: String,
/// Merchant reference
pub merchant_reference: String,
/// Payment status (e.g., "PENDING", "COMPLETED", "FAILED")
pub status: String,
/// Amount
pub amount: f64,
/// Currency
pub currency: String,
/// Payment method used
pub payment_method: Option<String>,
/// Transaction ID
pub transaction_id: Option<String>,
}
// Pesapal-specific structures
#[derive(Debug, Serialize)]
struct PesapalAuthRequest {
consumer_key: String,
consumer_secret: String,
}
#[derive(Debug, Deserialize)]
struct PesapalAuthResponse {
token: String,
#[serde(rename = "expiryDate")]
expiry_date: Option<serde_json::Value>,
error: Option<String>,
status: Option<String>,
message: Option<String>,
}
#[derive(Debug, Serialize)]
struct PesapalSubmitOrderRequest {
id: String,
currency: String,
amount: f64,
description: String,
callback_url: String,
redirect_mode: String,
notification_id: String,
billing_address: Option<PesapalBillingAddress>,
}
#[derive(Debug, Serialize)]
struct PesapalBillingAddress {
email_address: Option<String>,
phone_number: Option<String>,
first_name: Option<String>,
last_name: Option<String>,
}
#[derive(Debug, Deserialize)]
struct PesapalSubmitOrderResponse {
order_tracking_id: Option<String>,
merchant_reference: Option<String>,
redirect_url: Option<String>,
error: Option<serde_json::Value>,
status: Option<String>,
}
#[derive(Debug, Deserialize)]
struct PesapalTransactionStatusResponse {
payment_method: Option<String>,
amount: f64,
created_date: String,
confirmation_code: Option<String>,
payment_status_description: String,
description: String,
message: String,
payment_account: Option<String>,
call_back_url: String,
status_code: i32,
merchant_reference: String,
payment_status_code: String,
currency: String,
error: Option<String>,
status: String,
}
impl PaymentClient {
/// Create a new payment client
pub fn new(id: u32, provider: String, consumer_key: String, consumer_secret: String) -> Self {
let base_data = BaseData::with_id(id, String::new());
Self {
base_data,
provider,
consumer_key,
consumer_secret,
base_url: None,
sandbox: false,
}
}
/// Create a Pesapal client
pub fn pesapal(id: u32, consumer_key: String, consumer_secret: String) -> Self {
let base_data = BaseData::with_id(id, String::new());
Self {
base_data,
provider: "pesapal".to_string(),
consumer_key,
consumer_secret,
base_url: Some("https://pay.pesapal.com/v3".to_string()),
sandbox: false,
}
}
/// Create a Pesapal sandbox client
pub fn pesapal_sandbox(id: u32, consumer_key: String, consumer_secret: String) -> Self {
let base_data = BaseData::with_id(id, String::new());
Self {
base_data,
provider: "pesapal".to_string(),
consumer_key,
consumer_secret,
base_url: Some("https://cybqa.pesapal.com/pesapalv3".to_string()),
sandbox: true,
}
}
/// Set custom base URL
pub fn with_base_url(mut self, base_url: String) -> Self {
self.base_url = Some(base_url);
self
}
/// Enable sandbox mode
pub fn with_sandbox(mut self, sandbox: bool) -> Self {
self.sandbox = sandbox;
self
}
/// Get the base URL for the provider
fn get_base_url(&self) -> String {
if let Some(url) = &self.base_url {
return url.clone();
}
match self.provider.as_str() {
"pesapal" => {
if self.sandbox {
"https://cybqa.pesapal.com/pesapalv3".to_string()
} else {
"https://pay.pesapal.com/v3".to_string()
}
}
"stripe" => "https://api.stripe.com/v1".to_string(),
"paypal" => "https://api.paypal.com/v2".to_string(),
"flutterwave" => "https://api.flutterwave.com/v3".to_string(),
_ => panic!("Unknown provider: {}", self.provider),
}
}
/// Create a payment link
pub fn create_payment_link(
&self,
request: &PaymentRequest,
) -> Result<PaymentResponse, String> {
match self.provider.as_str() {
"pesapal" => self.create_pesapal_payment(request),
_ => Err(format!("Provider {} not yet implemented", self.provider)),
}
}
/// Get payment status
pub fn get_payment_status(
&self,
order_tracking_id: &str,
) -> Result<PaymentStatus, String> {
match self.provider.as_str() {
"pesapal" => self.get_pesapal_status(order_tracking_id),
_ => Err(format!("Provider {} not yet implemented", self.provider)),
}
}
/// Authenticate with Pesapal and get access token
fn pesapal_authenticate(&self) -> Result<String, String> {
let url = format!("{}/api/Auth/RequestToken", self.get_base_url());
let auth_request = PesapalAuthRequest {
consumer_key: self.consumer_key.clone(),
consumer_secret: self.consumer_secret.clone(),
};
run_async(async move {
let client = reqwest::Client::new();
let response = client
.post(&url)
.header("Content-Type", "application/json")
.header("Accept", "application/json")
.json(&auth_request)
.send()
.await
.map_err(|e| format!("Failed to send auth request: {}", e))?;
if !response.status().is_success() {
let status = response.status();
let error_text = response.text().await.unwrap_or_default();
return Err(format!("Pesapal auth failed ({}): {}", status, error_text));
}
// Debug: print raw response
let response_text = response.text().await
.map_err(|e| format!("Failed to read auth response: {}", e))?;
println!("=== PESAPAL AUTH RESPONSE ===");
println!("{}", response_text);
println!("==============================");
let auth_response: PesapalAuthResponse = serde_json::from_str(&response_text)
.map_err(|e| format!("Failed to parse auth response: {}", e))?;
if let Some(error) = auth_response.error {
return Err(format!("Pesapal auth error: {}", error));
}
Ok(auth_response.token)
})
}
/// Create a Pesapal payment
fn create_pesapal_payment(
&self,
request: &PaymentRequest,
) -> Result<PaymentResponse, String> {
// Get auth token
let token = self.pesapal_authenticate()?;
let url = format!("{}/api/Transactions/SubmitOrderRequest", self.get_base_url());
let pesapal_request = PesapalSubmitOrderRequest {
id: request.merchant_reference.clone(),
currency: request.currency.clone(),
amount: request.amount,
description: request.description.clone(),
callback_url: request.callback_url.clone(),
redirect_mode: String::new(),
notification_id: String::new(),
billing_address: Some(PesapalBillingAddress {
email_address: request.customer_email.clone(),
phone_number: request.customer_phone.clone(),
first_name: request.customer_first_name.clone(),
last_name: request.customer_last_name.clone(),
}),
};
run_async(async move {
let client = reqwest::Client::new();
let response = client
.post(&url)
.header("Content-Type", "application/json")
.header("Accept", "application/json")
.bearer_auth(&token)
.json(&pesapal_request)
.send()
.await
.map_err(|e| format!("Failed to send payment request: {}", e))?;
if !response.status().is_success() {
let status = response.status();
let error_text = response.text().await.unwrap_or_default();
return Err(format!("Pesapal payment request failed ({}): {}", status, error_text));
}
// Debug: print raw response
let response_text = response.text().await
.map_err(|e| format!("Failed to read payment response: {}", e))?;
println!("=== PESAPAL PAYMENT RESPONSE ===");
println!("{}", response_text);
println!("=================================");
let pesapal_response: PesapalSubmitOrderResponse = serde_json::from_str(&response_text)
.map_err(|e| format!("Failed to parse payment response: {}", e))?;
if let Some(error) = pesapal_response.error {
return Err(format!("Pesapal payment error: {}", error));
}
Ok(PaymentResponse {
payment_url: pesapal_response.redirect_url.unwrap_or_default(),
order_tracking_id: pesapal_response.order_tracking_id.unwrap_or_default(),
merchant_reference: pesapal_response.merchant_reference.unwrap_or_default(),
status: pesapal_response.status.unwrap_or_default(),
})
})
}
/// Get Pesapal payment status
fn get_pesapal_status(
&self,
order_tracking_id: &str,
) -> Result<PaymentStatus, String> {
let token = self.pesapal_authenticate()?;
let order_tracking_id = order_tracking_id.to_string();
let url = format!(
"{}/api/Transactions/GetTransactionStatus?orderTrackingId={}",
self.get_base_url(),
order_tracking_id
);
run_async(async move {
let client = reqwest::Client::new();
let response = client
.get(&url)
.header("Accept", "application/json")
.bearer_auth(&token)
.send()
.await
.map_err(|e| format!("Failed to send status request: {}", e))?;
if !response.status().is_success() {
let status = response.status();
let error_text = response.text().await.unwrap_or_default();
return Err(format!("Pesapal status request failed ({}): {}", status, error_text));
}
// Debug: print raw response
let response_text = response.text().await
.map_err(|e| format!("Failed to read status response: {}", e))?;
println!("=== PESAPAL STATUS RESPONSE ===");
println!("{}", response_text);
println!("================================");
let status_response: PesapalTransactionStatusResponse = serde_json::from_str(&response_text)
.map_err(|e| format!("Failed to parse status response: {}", e))?;
if let Some(error) = status_response.error {
return Err(format!("Pesapal status error: {}", error));
}
Ok(PaymentStatus {
order_tracking_id: order_tracking_id.to_string(),
merchant_reference: status_response.merchant_reference,
status: status_response.payment_status_description,
amount: status_response.amount,
currency: status_response.currency,
payment_method: status_response.payment_method,
transaction_id: status_response.confirmation_code,
})
})
}
}

View File

@@ -0,0 +1,630 @@
/// Rhai bindings for Money objects (Account, Asset, Transaction, PaymentClient)
use ::rhai::plugin::*;
use ::rhai::{CustomType, Dynamic, Engine, EvalAltResult, Module, TypeBuilder};
use super::models::{Account, Asset, Transaction};
use super::payments::{PaymentClient, PaymentRequest, PaymentResponse, PaymentStatus};
// ============================================================================
// Account Module
// ============================================================================
type RhaiAccount = Account;
#[export_module]
mod rhai_account_module {
use super::RhaiAccount;
#[rhai_fn(name = "new_account", return_raw)]
pub fn new_account() -> Result<RhaiAccount, Box<EvalAltResult>> {
Ok(Account::new(0))
}
#[rhai_fn(name = "owner_id", return_raw)]
pub fn set_owner_id(
account: &mut RhaiAccount,
owner_id: i64,
) -> Result<RhaiAccount, Box<EvalAltResult>> {
let owned = std::mem::take(account);
*account = owned.owner_id(owner_id as u32);
Ok(account.clone())
}
#[rhai_fn(name = "address", return_raw)]
pub fn set_address(
account: &mut RhaiAccount,
address: String,
) -> Result<RhaiAccount, Box<EvalAltResult>> {
let owned = std::mem::take(account);
*account = owned.address(address);
Ok(account.clone())
}
#[rhai_fn(name = "balance", return_raw)]
pub fn set_balance(
account: &mut RhaiAccount,
balance: f64,
) -> Result<RhaiAccount, Box<EvalAltResult>> {
let owned = std::mem::take(account);
*account = owned.balance(balance);
Ok(account.clone())
}
#[rhai_fn(name = "currency", return_raw)]
pub fn set_currency(
account: &mut RhaiAccount,
currency: String,
) -> Result<RhaiAccount, Box<EvalAltResult>> {
let owned = std::mem::take(account);
*account = owned.currency(currency);
Ok(account.clone())
}
#[rhai_fn(name = "assetid", return_raw)]
pub fn set_assetid(
account: &mut RhaiAccount,
assetid: i64,
) -> Result<RhaiAccount, Box<EvalAltResult>> {
let owned = std::mem::take(account);
*account = owned.assetid(assetid as u32);
Ok(account.clone())
}
// Getters
#[rhai_fn(name = "get_id")]
pub fn get_id(account: &mut RhaiAccount) -> i64 {
account.base_data.id as i64
}
#[rhai_fn(name = "get_owner_id")]
pub fn get_owner_id(account: &mut RhaiAccount) -> i64 {
account.owner_id as i64
}
#[rhai_fn(name = "get_address")]
pub fn get_address(account: &mut RhaiAccount) -> String {
account.address.clone()
}
#[rhai_fn(name = "get_balance")]
pub fn get_balance(account: &mut RhaiAccount) -> f64 {
account.balance
}
#[rhai_fn(name = "get_currency")]
pub fn get_currency(account: &mut RhaiAccount) -> String {
account.currency.clone()
}
}
// ============================================================================
// Asset Module
// ============================================================================
type RhaiAsset = Asset;
#[export_module]
mod rhai_asset_module {
use super::RhaiAsset;
#[rhai_fn(name = "new_asset", return_raw)]
pub fn new_asset() -> Result<RhaiAsset, Box<EvalAltResult>> {
Ok(Asset::new(0))
}
#[rhai_fn(name = "address", return_raw)]
pub fn set_address(
asset: &mut RhaiAsset,
address: String,
) -> Result<RhaiAsset, Box<EvalAltResult>> {
let owned = std::mem::take(asset);
*asset = owned.address(address);
Ok(asset.clone())
}
#[rhai_fn(name = "asset_type", return_raw)]
pub fn set_asset_type(
asset: &mut RhaiAsset,
asset_type: String,
) -> Result<RhaiAsset, Box<EvalAltResult>> {
let owned = std::mem::take(asset);
*asset = owned.asset_type(asset_type);
Ok(asset.clone())
}
#[rhai_fn(name = "issuer", return_raw)]
pub fn set_issuer(
asset: &mut RhaiAsset,
issuer: i64,
) -> Result<RhaiAsset, Box<EvalAltResult>> {
let owned = std::mem::take(asset);
*asset = owned.issuer(issuer as u32);
Ok(asset.clone())
}
#[rhai_fn(name = "supply", return_raw)]
pub fn set_supply(
asset: &mut RhaiAsset,
supply: f64,
) -> Result<RhaiAsset, Box<EvalAltResult>> {
let owned = std::mem::take(asset);
*asset = owned.supply(supply);
Ok(asset.clone())
}
// Getters
#[rhai_fn(name = "get_id")]
pub fn get_id(asset: &mut RhaiAsset) -> i64 {
asset.base_data.id as i64
}
#[rhai_fn(name = "get_address")]
pub fn get_address(asset: &mut RhaiAsset) -> String {
asset.address.clone()
}
#[rhai_fn(name = "get_asset_type")]
pub fn get_asset_type(asset: &mut RhaiAsset) -> String {
asset.asset_type.clone()
}
#[rhai_fn(name = "get_supply")]
pub fn get_supply(asset: &mut RhaiAsset) -> f64 {
asset.supply
}
}
// ============================================================================
// Transaction Module
// ============================================================================
type RhaiTransaction = Transaction;
#[export_module]
mod rhai_transaction_module {
use super::RhaiTransaction;
#[rhai_fn(name = "new_transaction", return_raw)]
pub fn new_transaction() -> Result<RhaiTransaction, Box<EvalAltResult>> {
Ok(Transaction::new(0))
}
#[rhai_fn(name = "source", return_raw)]
pub fn set_source(
tx: &mut RhaiTransaction,
source: i64,
) -> Result<RhaiTransaction, Box<EvalAltResult>> {
let owned = std::mem::take(tx);
*tx = owned.source(source as u32);
Ok(tx.clone())
}
#[rhai_fn(name = "destination", return_raw)]
pub fn set_destination(
tx: &mut RhaiTransaction,
destination: i64,
) -> Result<RhaiTransaction, Box<EvalAltResult>> {
let owned = std::mem::take(tx);
*tx = owned.destination(destination as u32);
Ok(tx.clone())
}
#[rhai_fn(name = "amount", return_raw)]
pub fn set_amount(
tx: &mut RhaiTransaction,
amount: f64,
) -> Result<RhaiTransaction, Box<EvalAltResult>> {
let owned = std::mem::take(tx);
*tx = owned.amount(amount);
Ok(tx.clone())
}
#[rhai_fn(name = "assetid", return_raw)]
pub fn set_assetid(
tx: &mut RhaiTransaction,
assetid: i64,
) -> Result<RhaiTransaction, Box<EvalAltResult>> {
let owned = std::mem::take(tx);
*tx = owned.assetid(assetid as u32);
Ok(tx.clone())
}
// Getters
#[rhai_fn(name = "get_id")]
pub fn get_id(tx: &mut RhaiTransaction) -> i64 {
tx.base_data.id as i64
}
#[rhai_fn(name = "get_source")]
pub fn get_source(tx: &mut RhaiTransaction) -> i64 {
tx.source as i64
}
#[rhai_fn(name = "get_destination")]
pub fn get_destination(tx: &mut RhaiTransaction) -> i64 {
tx.destination as i64
}
#[rhai_fn(name = "get_amount")]
pub fn get_amount(tx: &mut RhaiTransaction) -> f64 {
tx.amount
}
#[rhai_fn(name = "get_assetid")]
pub fn get_assetid(tx: &mut RhaiTransaction) -> i64 {
tx.assetid as i64
}
}
// ============================================================================
// Registration Functions
// ============================================================================
/// Register money modules with the Rhai engine
pub fn register_money_modules(parent_module: &mut Module) {
// Register custom types
parent_module.set_custom_type::<Account>("Account");
parent_module.set_custom_type::<Asset>("Asset");
parent_module.set_custom_type::<Transaction>("Transaction");
parent_module.set_custom_type::<PaymentClient>("PaymentClient");
parent_module.set_custom_type::<PaymentRequest>("PaymentRequest");
parent_module.set_custom_type::<PaymentResponse>("PaymentResponse");
parent_module.set_custom_type::<PaymentStatus>("PaymentStatus");
// Merge account functions
let account_module = exported_module!(rhai_account_module);
parent_module.merge(&account_module);
// Merge asset functions
let asset_module = exported_module!(rhai_asset_module);
parent_module.merge(&asset_module);
// Merge transaction functions
let transaction_module = exported_module!(rhai_transaction_module);
parent_module.merge(&transaction_module);
// Merge payment client functions
let payment_module = exported_module!(rhai_payment_module);
parent_module.merge(&payment_module);
// Merge ethereum wallet functions
let eth_module = exported_module!(rhai_ethereum_module);
parent_module.merge(&eth_module);
}
// ============================================================================
// Payment Provider Module
// ============================================================================
type RhaiPaymentClient = PaymentClient;
type RhaiPaymentRequest = PaymentRequest;
type RhaiPaymentResponse = PaymentResponse;
type RhaiPaymentStatus = PaymentStatus;
#[export_module]
mod rhai_payment_module {
use super::{RhaiPaymentClient, RhaiPaymentRequest, RhaiPaymentResponse, RhaiPaymentStatus};
use super::super::payments::{PaymentClient, PaymentRequest, PaymentResponse, PaymentStatus};
use ::rhai::EvalAltResult;
// PaymentClient constructors
#[rhai_fn(name = "new_payment_client_pesapal", return_raw)]
pub fn new_pesapal_client(
id: i64,
consumer_key: String,
consumer_secret: String,
) -> Result<RhaiPaymentClient, Box<EvalAltResult>> {
Ok(PaymentClient::pesapal(id as u32, consumer_key, consumer_secret))
}
#[rhai_fn(name = "new_payment_client_pesapal_sandbox", return_raw)]
pub fn new_pesapal_sandbox_client(
id: i64,
consumer_key: String,
consumer_secret: String,
) -> Result<RhaiPaymentClient, Box<EvalAltResult>> {
Ok(PaymentClient::pesapal_sandbox(id as u32, consumer_key, consumer_secret))
}
// PaymentRequest constructor and builder methods
#[rhai_fn(name = "new_payment_request", return_raw)]
pub fn new_payment_request() -> Result<RhaiPaymentRequest, Box<EvalAltResult>> {
Ok(PaymentRequest {
merchant_reference: String::new(),
amount: 0.0,
currency: String::from("USD"),
description: String::new(),
callback_url: String::new(),
redirect_url: None,
cancel_url: None,
customer_email: None,
customer_phone: None,
customer_first_name: None,
customer_last_name: None,
})
}
#[rhai_fn(name = "amount", return_raw)]
pub fn set_amount(
request: &mut RhaiPaymentRequest,
amount: f64,
) -> Result<RhaiPaymentRequest, Box<EvalAltResult>> {
request.amount = amount;
Ok(request.clone())
}
#[rhai_fn(name = "currency", return_raw)]
pub fn set_currency(
request: &mut RhaiPaymentRequest,
currency: String,
) -> Result<RhaiPaymentRequest, Box<EvalAltResult>> {
request.currency = currency;
Ok(request.clone())
}
#[rhai_fn(name = "description", return_raw)]
pub fn set_description(
request: &mut RhaiPaymentRequest,
description: String,
) -> Result<RhaiPaymentRequest, Box<EvalAltResult>> {
request.description = description;
Ok(request.clone())
}
#[rhai_fn(name = "callback_url", return_raw)]
pub fn set_callback_url(
request: &mut RhaiPaymentRequest,
url: String,
) -> Result<RhaiPaymentRequest, Box<EvalAltResult>> {
request.callback_url = url;
Ok(request.clone())
}
#[rhai_fn(name = "merchant_reference", return_raw)]
pub fn set_merchant_reference(
request: &mut RhaiPaymentRequest,
reference: String,
) -> Result<RhaiPaymentRequest, Box<EvalAltResult>> {
request.merchant_reference = reference;
Ok(request.clone())
}
#[rhai_fn(name = "customer_email", return_raw)]
pub fn set_customer_email(
request: &mut RhaiPaymentRequest,
email: String,
) -> Result<RhaiPaymentRequest, Box<EvalAltResult>> {
request.customer_email = Some(email);
Ok(request.clone())
}
#[rhai_fn(name = "customer_phone", return_raw)]
pub fn set_customer_phone(
request: &mut RhaiPaymentRequest,
phone: String,
) -> Result<RhaiPaymentRequest, Box<EvalAltResult>> {
request.customer_phone = Some(phone);
Ok(request.clone())
}
#[rhai_fn(name = "customer_name", return_raw)]
pub fn set_customer_name(
request: &mut RhaiPaymentRequest,
first_name: String,
last_name: String,
) -> Result<RhaiPaymentRequest, Box<EvalAltResult>> {
request.customer_first_name = Some(first_name);
request.customer_last_name = Some(last_name);
Ok(request.clone())
}
#[rhai_fn(name = "redirect_url", return_raw)]
pub fn set_redirect_url(
request: &mut RhaiPaymentRequest,
url: String,
) -> Result<RhaiPaymentRequest, Box<EvalAltResult>> {
request.redirect_url = Some(url);
Ok(request.clone())
}
// PaymentClient methods
#[rhai_fn(name = "create_payment_link", return_raw)]
pub fn create_payment_link(
client: &mut RhaiPaymentClient,
request: RhaiPaymentRequest,
) -> Result<RhaiPaymentResponse, Box<EvalAltResult>> {
client.create_payment_link(&request)
.map_err(|e| e.into())
}
#[rhai_fn(name = "get_payment_status", return_raw)]
pub fn get_payment_status(
client: &mut RhaiPaymentClient,
order_tracking_id: String,
) -> Result<RhaiPaymentStatus, Box<EvalAltResult>> {
client.get_payment_status(&order_tracking_id)
.map_err(|e| e.into())
}
// PaymentResponse getters
#[rhai_fn(name = "get_payment_url", pure)]
pub fn get_payment_url(response: &mut RhaiPaymentResponse) -> String {
response.payment_url.clone()
}
#[rhai_fn(name = "get_order_tracking_id", pure)]
pub fn get_order_tracking_id(response: &mut RhaiPaymentResponse) -> String {
response.order_tracking_id.clone()
}
#[rhai_fn(name = "get_merchant_reference", pure)]
pub fn get_merchant_reference(response: &mut RhaiPaymentResponse) -> String {
response.merchant_reference.clone()
}
#[rhai_fn(name = "get_status", pure)]
pub fn get_response_status(response: &mut RhaiPaymentResponse) -> String {
response.status.clone()
}
// PaymentStatus getters
#[rhai_fn(name = "get_status", pure)]
pub fn get_payment_status_value(status: &mut RhaiPaymentStatus) -> String {
status.status.clone()
}
#[rhai_fn(name = "get_amount", pure)]
pub fn get_amount(status: &mut RhaiPaymentStatus) -> f64 {
status.amount
}
#[rhai_fn(name = "get_currency", pure)]
pub fn get_currency(status: &mut RhaiPaymentStatus) -> String {
status.currency.clone()
}
#[rhai_fn(name = "get_payment_method", pure)]
pub fn get_payment_method(status: &mut RhaiPaymentStatus) -> String {
status.payment_method.clone().unwrap_or_default()
}
#[rhai_fn(name = "get_transaction_id", pure)]
pub fn get_transaction_id(status: &mut RhaiPaymentStatus) -> String {
status.transaction_id.clone().unwrap_or_default()
}
}
// ============================================================================
// CustomType Implementations
// ============================================================================
impl CustomType for Account {
fn build(mut builder: TypeBuilder<Self>) {
builder.with_name("Account");
}
}
impl CustomType for Asset {
fn build(mut builder: TypeBuilder<Self>) {
builder.with_name("Asset");
}
}
impl CustomType for Transaction {
fn build(mut builder: TypeBuilder<Self>) {
builder.with_name("Transaction");
}
}
impl CustomType for PaymentClient {
fn build(mut builder: TypeBuilder<Self>) {
builder.with_name("PaymentClient");
}
}
impl CustomType for PaymentRequest {
fn build(mut builder: TypeBuilder<Self>) {
builder.with_name("PaymentRequest");
}
}
impl CustomType for PaymentResponse {
fn build(mut builder: TypeBuilder<Self>) {
builder.with_name("PaymentResponse");
}
}
impl CustomType for PaymentStatus {
fn build(mut builder: TypeBuilder<Self>) {
builder.with_name("PaymentStatus");
}
}
// ============================================================================
// Ethereum Wallet Module (Stub Implementation)
// ============================================================================
/// Simple Ethereum wallet representation
#[derive(Debug, Clone, Default)]
pub struct EthereumWallet {
pub owner_id: u32,
pub address: String,
pub network: String,
}
impl EthereumWallet {
pub fn new() -> Self {
// Generate a mock Ethereum address (in production, use ethers-rs or similar)
use std::time::{SystemTime, UNIX_EPOCH};
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_nanos();
let mock_address = format!("0x{:040x}", timestamp as u128);
Self {
owner_id: 0,
address: mock_address,
network: String::from("mainnet"),
}
}
pub fn owner_id(mut self, id: u32) -> Self {
self.owner_id = id;
self
}
pub fn network(mut self, network: impl ToString) -> Self {
self.network = network.to_string();
self
}
}
type RhaiEthereumWallet = EthereumWallet;
#[export_module]
mod rhai_ethereum_module {
use super::RhaiEthereumWallet;
use ::rhai::EvalAltResult;
#[rhai_fn(name = "new_ethereum_wallet", return_raw)]
pub fn new_ethereum_wallet() -> Result<RhaiEthereumWallet, Box<EvalAltResult>> {
Ok(EthereumWallet::new())
}
#[rhai_fn(name = "owner_id", return_raw)]
pub fn set_owner_id(
wallet: &mut RhaiEthereumWallet,
owner_id: i64,
) -> Result<RhaiEthereumWallet, Box<EvalAltResult>> {
let owned = std::mem::take(wallet);
*wallet = owned.owner_id(owner_id as u32);
Ok(wallet.clone())
}
#[rhai_fn(name = "network", return_raw)]
pub fn set_network(
wallet: &mut RhaiEthereumWallet,
network: String,
) -> Result<RhaiEthereumWallet, Box<EvalAltResult>> {
let owned = std::mem::take(wallet);
*wallet = owned.network(network);
Ok(wallet.clone())
}
#[rhai_fn(name = "get_address")]
pub fn get_address(wallet: &mut RhaiEthereumWallet) -> String {
wallet.address.clone()
}
#[rhai_fn(name = "get_network")]
pub fn get_network(wallet: &mut RhaiEthereumWallet) -> String {
wallet.network.clone()
}
}
impl CustomType for EthereumWallet {
fn build(mut builder: TypeBuilder<Self>) {
builder.with_name("EthereumWallet");
}
}

View File

@@ -0,0 +1,78 @@
use crate::store::BaseData;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
pub mod rhai;
/// A simple note object
#[derive(Debug, Clone, Serialize, Deserialize, crate::DeriveObject)]
pub struct Note {
/// Base data
pub base_data: BaseData,
/// Title of the note
#[index]
pub title: Option<String>,
/// Content of the note (searchable but not indexed)
pub content: Option<String>,
/// Tags for categorization
#[index]
pub tags: BTreeMap<String, String>,
}
impl Note {
/// Create a new note
pub fn new(ns: String) -> Self {
Self {
base_data: BaseData::with_ns(ns),
title: None,
content: None,
tags: BTreeMap::new(),
}
}
/// Create a note with specific ID
pub fn with_id(id: String, ns: String) -> Self {
let id_u32 = id.parse::<u32>().unwrap_or(0);
Self {
base_data: BaseData::with_id(id_u32, ns),
title: None,
content: None,
tags: BTreeMap::new(),
}
}
/// Set the title
pub fn set_title(mut self, title: impl ToString) -> Self {
self.title = Some(title.to_string());
self.base_data.update_modified();
self
}
/// Set the content
pub fn set_content(mut self, content: impl ToString) -> Self {
let content_str = content.to_string();
self.base_data.set_size(Some(content_str.len() as u64));
self.content = Some(content_str);
self.base_data.update_modified();
self
}
/// Add a tag
pub fn add_tag(mut self, key: impl ToString, value: impl ToString) -> Self {
self.tags.insert(key.to_string(), value.to_string());
self.base_data.update_modified();
self
}
/// Set MIME type
pub fn set_mime(mut self, mime: impl ToString) -> Self {
self.base_data.set_mime(Some(mime.to_string()));
self
}
}
// Object trait implementation is auto-generated by #[derive(DeriveObject)]
// The derive macro generates: object_type(), base_data(), base_data_mut(), index_keys(), indexed_fields()

View File

@@ -0,0 +1,107 @@
use crate::objects::Note;
use rhai::{CustomType, Engine, TypeBuilder, Module, FuncRegistration};
impl CustomType for Note {
fn build(mut builder: TypeBuilder<Self>) {
builder
.with_name("Note")
.with_fn("new", |ns: String| Note::new(ns))
.with_fn("set_title", |note: &mut Note, title: String| {
note.title = Some(title);
note.base_data.update_modified();
})
.with_fn("set_content", |note: &mut Note, content: String| {
let size = content.len() as u64;
note.content = Some(content);
note.base_data.set_size(Some(size));
note.base_data.update_modified();
})
.with_fn("add_tag", |note: &mut Note, key: String, value: String| {
note.tags.insert(key, value);
note.base_data.update_modified();
})
.with_fn("set_mime", |note: &mut Note, mime: String| {
note.base_data.set_mime(Some(mime));
})
.with_fn("get_id", |note: &mut Note| note.base_data.id.clone())
.with_fn("get_title", |note: &mut Note| note.title.clone().unwrap_or_default())
.with_fn("get_content", |note: &mut Note| note.content.clone().unwrap_or_default())
.with_fn("to_json", |note: &mut Note| {
serde_json::to_string_pretty(note).unwrap_or_default()
});
}
}
/// Register Note API in Rhai engine
pub fn register_note_api(engine: &mut Engine) {
engine.build_type::<Note>();
// Register builder-style constructor
engine.register_fn("note", |ns: String| Note::new(ns));
// Register chainable methods that return Self
engine.register_fn("title", |mut note: Note, title: String| {
note.title = Some(title);
note.base_data.update_modified();
note
});
engine.register_fn("content", |mut note: Note, content: String| {
let size = content.len() as u64;
note.content = Some(content);
note.base_data.set_size(Some(size));
note.base_data.update_modified();
note
});
engine.register_fn("tag", |mut note: Note, key: String, value: String| {
note.tags.insert(key, value);
note.base_data.update_modified();
note
});
engine.register_fn("mime", |mut note: Note, mime: String| {
note.base_data.set_mime(Some(mime));
note
});
}
/// Register Note functions into a module (for use in packages)
pub fn register_note_functions(module: &mut Module) {
// Register Note type
module.set_custom_type::<Note>("Note");
// Register builder-style constructor
FuncRegistration::new("note")
.set_into_module(module, |ns: String| Note::new(ns));
// Register chainable methods that return Self
FuncRegistration::new("title")
.set_into_module(module, |mut note: Note, title: String| {
note.title = Some(title);
note.base_data.update_modified();
note
});
FuncRegistration::new("content")
.set_into_module(module, |mut note: Note, content: String| {
let size = content.len() as u64;
note.content = Some(content);
note.base_data.set_size(Some(size));
note.base_data.update_modified();
note
});
FuncRegistration::new("tag")
.set_into_module(module, |mut note: Note, key: String, value: String| {
note.tags.insert(key, value);
note.base_data.update_modified();
note
});
FuncRegistration::new("mime")
.set_into_module(module, |mut note: Note, mime: String| {
note.base_data.set_mime(Some(mime));
note
});
}

View File

@@ -0,0 +1,183 @@
use crate::store::BaseData;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
pub mod rhai;
/// API Key scopes for authorization
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum ApiKeyScope {
Admin,
User,
Registrar,
}
/// API Key for supervisor authentication
#[derive(Debug, Clone, Serialize, Deserialize, crate::DeriveObject)]
pub struct ApiKey {
/// Base data
pub base_data: BaseData,
/// The actual key value (hashed in production)
#[index]
pub key: String,
/// Human-readable name for the key
#[index]
pub name: String,
/// Scope/permission level
#[index]
pub scope: ApiKeyScope,
/// Optional expiration timestamp
pub expires_at: Option<String>,
/// Metadata
pub metadata: BTreeMap<String, String>,
}
impl ApiKey {
/// Create a new API key
pub fn new(ns: String, key: String, name: String, scope: ApiKeyScope) -> Self {
Self {
base_data: BaseData::with_ns(ns),
key,
name,
scope,
expires_at: None,
metadata: BTreeMap::new(),
}
}
/// Set expiration
pub fn set_expires_at(mut self, expires_at: impl ToString) -> Self {
self.expires_at = Some(expires_at.to_string());
self.base_data.update_modified();
self
}
/// Add metadata
pub fn add_metadata(mut self, key: impl ToString, value: impl ToString) -> Self {
self.metadata.insert(key.to_string(), value.to_string());
self.base_data.update_modified();
self
}
}
/// Runner metadata for supervisor
#[derive(Debug, Clone, Serialize, Deserialize, crate::DeriveObject)]
pub struct Runner {
/// Base data
pub base_data: BaseData,
/// Runner ID (same as base_data.id but as string)
#[index]
pub runner_id: String,
/// Runner name
#[index]
pub name: String,
/// Queue name
pub queue: String,
/// Registered by (API key name)
#[index]
pub registered_by: String,
/// Metadata
pub metadata: BTreeMap<String, String>,
}
impl Runner {
/// Create a new runner
pub fn new(ns: String, runner_id: String, name: String, queue: String, registered_by: String) -> Self {
Self {
base_data: BaseData::with_ns(ns),
runner_id,
name,
queue,
registered_by,
metadata: BTreeMap::new(),
}
}
/// Add metadata
pub fn add_metadata(mut self, key: impl ToString, value: impl ToString) -> Self {
self.metadata.insert(key.to_string(), value.to_string());
self.base_data.update_modified();
self
}
}
/// Job metadata for supervisor
#[derive(Debug, Clone, Serialize, Deserialize, crate::DeriveObject)]
pub struct JobMetadata {
/// Base data
pub base_data: BaseData,
/// Job ID
#[index]
pub job_id: String,
/// Runner name
#[index]
pub runner: String,
/// Created by (API key name)
#[index]
pub created_by: String,
/// Job status
#[index]
pub status: String,
/// Job payload (Rhai script or data)
pub payload: String,
/// Result (if completed)
pub result: Option<String>,
/// Metadata
pub metadata: BTreeMap<String, String>,
}
impl JobMetadata {
/// Create new job metadata
pub fn new(ns: String, job_id: String, runner: String, created_by: String, payload: String) -> Self {
Self {
base_data: BaseData::with_ns(ns),
job_id,
runner,
created_by,
status: "created".to_string(),
payload,
result: None,
metadata: BTreeMap::new(),
}
}
/// Set status
pub fn set_status(mut self, status: impl ToString) -> Self {
self.status = status.to_string();
self.base_data.update_modified();
self
}
/// Set result
pub fn set_result(mut self, result: impl ToString) -> Self {
self.result = Some(result.to_string());
self.base_data.update_modified();
self
}
/// Add metadata
pub fn add_metadata(mut self, key: impl ToString, value: impl ToString) -> Self {
self.metadata.insert(key.to_string(), value.to_string());
self.base_data.update_modified();
self
}
}
// Object trait implementations are auto-generated by #[derive(DeriveObject)]

View File

@@ -0,0 +1,238 @@
/// Rhai bindings for Supervisor objects (ApiKey, Runner, JobMetadata)
use ::rhai::plugin::*;
use ::rhai::{CustomType, Dynamic, Engine, EvalAltResult, Module, TypeBuilder};
use super::{ApiKey, ApiKeyScope, Runner, JobMetadata};
/// Register supervisor modules with the Rhai engine
pub fn register_supervisor_modules(parent_module: &mut Module) {
// Register custom types
parent_module.set_custom_type::<ApiKey>("ApiKey");
parent_module.set_custom_type::<Runner>("Runner");
parent_module.set_custom_type::<JobMetadata>("JobMetadata");
// Merge function modules
let api_key_module = exported_module!(rhai_api_key_module);
parent_module.merge(&api_key_module);
let runner_module = exported_module!(rhai_runner_module);
parent_module.merge(&runner_module);
let job_module = exported_module!(rhai_job_metadata_module);
parent_module.merge(&job_module);
}
// ============================================================================
// ApiKey Module
// ============================================================================
type RhaiApiKey = ApiKey;
#[export_module]
mod rhai_api_key_module {
use super::RhaiApiKey;
use super::super::{ApiKey, ApiKeyScope};
use ::rhai::EvalAltResult;
// ApiKey constructor
#[rhai_fn(name = "new_api_key", return_raw)]
pub fn new_api_key(
ns: String,
key: String,
name: String,
scope: String,
) -> Result<RhaiApiKey, Box<EvalAltResult>> {
let scope_enum = match scope.as_str() {
"admin" => ApiKeyScope::Admin,
"user" => ApiKeyScope::User,
"registrar" => ApiKeyScope::Registrar,
_ => return Err(format!("Invalid scope: {}", scope).into()),
};
Ok(ApiKey::new(ns, key, name, scope_enum))
}
// Builder methods
#[rhai_fn(name = "expires_at", return_raw)]
pub fn set_expires_at(
api_key: RhaiApiKey,
expires_at: String,
) -> Result<RhaiApiKey, Box<EvalAltResult>> {
Ok(api_key.set_expires_at(expires_at))
}
#[rhai_fn(name = "add_metadata", return_raw)]
pub fn add_metadata(
api_key: RhaiApiKey,
key: String,
value: String,
) -> Result<RhaiApiKey, Box<EvalAltResult>> {
Ok(api_key.add_metadata(key, value))
}
// Getters
#[rhai_fn(name = "key", pure)]
pub fn get_key(api_key: &mut RhaiApiKey) -> String {
api_key.key.clone()
}
#[rhai_fn(name = "name", pure)]
pub fn get_name(api_key: &mut RhaiApiKey) -> String {
api_key.name.clone()
}
#[rhai_fn(name = "scope", pure)]
pub fn get_scope(api_key: &mut RhaiApiKey) -> String {
format!("{:?}", api_key.scope)
}
}
// ============================================================================
// Runner Module
// ============================================================================
type RhaiRunner = Runner;
#[export_module]
mod rhai_runner_module {
use super::RhaiRunner;
use super::super::Runner;
use ::rhai::EvalAltResult;
// Runner constructor
#[rhai_fn(name = "new_runner", return_raw)]
pub fn new_runner(
ns: String,
runner_id: String,
name: String,
queue: String,
registered_by: String,
) -> Result<RhaiRunner, Box<EvalAltResult>> {
Ok(Runner::new(ns, runner_id, name, queue, registered_by))
}
// Builder methods
#[rhai_fn(name = "add_metadata", return_raw)]
pub fn add_metadata(
runner: RhaiRunner,
key: String,
value: String,
) -> Result<RhaiRunner, Box<EvalAltResult>> {
Ok(runner.add_metadata(key, value))
}
// Getters
#[rhai_fn(name = "runner_id", pure)]
pub fn get_runner_id(runner: &mut RhaiRunner) -> String {
runner.runner_id.clone()
}
#[rhai_fn(name = "name", pure)]
pub fn get_name(runner: &mut RhaiRunner) -> String {
runner.name.clone()
}
#[rhai_fn(name = "queue", pure)]
pub fn get_queue(runner: &mut RhaiRunner) -> String {
runner.queue.clone()
}
#[rhai_fn(name = "registered_by", pure)]
pub fn get_registered_by(runner: &mut RhaiRunner) -> String {
runner.registered_by.clone()
}
}
// ============================================================================
// JobMetadata Module
// ============================================================================
type RhaiJobMetadata = JobMetadata;
#[export_module]
mod rhai_job_metadata_module {
use super::RhaiJobMetadata;
use super::super::JobMetadata;
use ::rhai::EvalAltResult;
// JobMetadata constructor
#[rhai_fn(name = "new_job_metadata", return_raw)]
pub fn new_job_metadata(
ns: String,
job_id: String,
runner: String,
created_by: String,
payload: String,
) -> Result<RhaiJobMetadata, Box<EvalAltResult>> {
Ok(JobMetadata::new(ns, job_id, runner, created_by, payload))
}
// Builder methods
#[rhai_fn(name = "set_status", return_raw)]
pub fn set_status(
job: RhaiJobMetadata,
status: String,
) -> Result<RhaiJobMetadata, Box<EvalAltResult>> {
Ok(job.set_status(status))
}
#[rhai_fn(name = "set_result", return_raw)]
pub fn set_result(
job: RhaiJobMetadata,
result: String,
) -> Result<RhaiJobMetadata, Box<EvalAltResult>> {
Ok(job.set_result(result))
}
#[rhai_fn(name = "add_metadata", return_raw)]
pub fn add_metadata(
job: RhaiJobMetadata,
key: String,
value: String,
) -> Result<RhaiJobMetadata, Box<EvalAltResult>> {
Ok(job.add_metadata(key, value))
}
// Getters
#[rhai_fn(name = "job_id", pure)]
pub fn get_job_id(job: &mut RhaiJobMetadata) -> String {
job.job_id.clone()
}
#[rhai_fn(name = "runner", pure)]
pub fn get_runner(job: &mut RhaiJobMetadata) -> String {
job.runner.clone()
}
#[rhai_fn(name = "status", pure)]
pub fn get_status(job: &mut RhaiJobMetadata) -> String {
job.status.clone()
}
#[rhai_fn(name = "created_by", pure)]
pub fn get_created_by(job: &mut RhaiJobMetadata) -> String {
job.created_by.clone()
}
}
// ============================================================================
// CustomType Implementations
// ============================================================================
impl CustomType for ApiKey {
fn build(mut builder: TypeBuilder<Self>) {
builder.with_name("ApiKey");
}
}
impl CustomType for Runner {
fn build(mut builder: TypeBuilder<Self>) {
builder.with_name("Runner");
}
}
impl CustomType for JobMetadata {
fn build(mut builder: TypeBuilder<Self>) {
builder.with_name("JobMetadata");
}
}

View File

@@ -0,0 +1,5 @@
pub mod query;
pub mod search;
pub use query::RetrievalQuery;
pub use search::SearchEngine;

View File

@@ -0,0 +1,74 @@
/// Retrieval query structure
#[derive(Clone, Debug)]
pub struct RetrievalQuery {
/// Optional text query for keyword substring matching
pub text: Option<String>,
/// Namespace to search in
pub ns: String,
/// Field filters (key=value pairs)
pub filters: Vec<(String, String)>,
/// Maximum number of results to return
pub top_k: usize,
}
impl RetrievalQuery {
/// Create a new retrieval query
pub fn new(ns: String) -> Self {
Self {
text: None,
ns,
filters: Vec::new(),
top_k: 10,
}
}
/// Set the text query
pub fn with_text(mut self, text: String) -> Self {
self.text = Some(text);
self
}
/// Add a filter
pub fn with_filter(mut self, key: String, value: String) -> Self {
self.filters.push((key, value));
self
}
/// Set the maximum number of results
pub fn with_top_k(mut self, top_k: usize) -> Self {
self.top_k = top_k;
self
}
}
/// Search result
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
pub struct SearchResult {
/// Object ID
pub id: String,
/// Match score (0.0 to 1.0)
pub score: f32,
/// Matched text snippet (if applicable)
#[serde(skip_serializing_if = "Option::is_none")]
pub snippet: Option<String>,
}
impl SearchResult {
pub fn new(id: String, score: f32) -> Self {
Self {
id,
score,
snippet: None,
}
}
pub fn with_snippet(mut self, snippet: String) -> Self {
self.snippet = Some(snippet);
self
}
}

View File

@@ -0,0 +1,150 @@
use crate::error::Result;
use crate::index::FieldIndex;
use crate::retrieve::query::{RetrievalQuery, SearchResult};
use crate::store::{HeroDbClient, OsirisObject};
/// Search engine for OSIRIS
pub struct SearchEngine {
client: HeroDbClient,
index: FieldIndex,
}
impl SearchEngine {
/// Create a new search engine
pub fn new(client: HeroDbClient) -> Self {
let index = FieldIndex::new(client.clone());
Self { client, index }
}
/// Execute a search query
pub async fn search(&self, query: &RetrievalQuery) -> Result<Vec<SearchResult>> {
// Step 1: Get candidate IDs from field filters
let candidate_ids = if query.filters.is_empty() {
self.index.get_all_ids().await?
} else {
self.index.get_ids_by_filters(&query.filters).await?
};
// Step 2: If text query is provided, filter by substring match
let mut results = Vec::new();
if let Some(text_query) = &query.text {
let text_query_lower = text_query.to_lowercase();
for id in candidate_ids {
// Fetch the object
if let Ok(obj) = self.client.get_object(&id).await {
// Check if text matches
let score = self.compute_text_score(&obj, &text_query_lower);
if score > 0.0 {
let snippet = self.extract_snippet(&obj, &text_query_lower);
results.push(SearchResult::new(id, score).with_snippet(snippet));
}
}
}
} else {
// No text query, return all candidates with score 1.0
for id in candidate_ids {
results.push(SearchResult::new(id, 1.0));
}
}
// Step 3: Sort by score (descending) and limit
results.sort_by(|a, b| b.score.partial_cmp(&a.score).unwrap());
results.truncate(query.top_k);
Ok(results)
}
/// Compute text match score (simple substring matching)
fn compute_text_score(&self, obj: &OsirisObject, query: &str) -> f32 {
let mut score = 0.0;
// Check title
if let Some(title) = &obj.meta.title {
if title.to_lowercase().contains(query) {
score += 0.5;
}
}
// Check text content
if let Some(text) = &obj.text {
if text.to_lowercase().contains(query) {
score += 0.5;
// Bonus for multiple occurrences
let count = text.to_lowercase().matches(query).count();
score += (count as f32 - 1.0) * 0.1;
}
}
// Check tags
for (key, value) in &obj.meta.tags {
if key.to_lowercase().contains(query) || value.to_lowercase().contains(query) {
score += 0.2;
}
}
score.min(1.0)
}
/// Extract a snippet around the matched text
fn extract_snippet(&self, obj: &OsirisObject, query: &str) -> String {
const SNIPPET_LENGTH: usize = 100;
// Try to find snippet in text
if let Some(text) = &obj.text {
let text_lower = text.to_lowercase();
if let Some(pos) = text_lower.find(query) {
let start = pos.saturating_sub(SNIPPET_LENGTH / 2);
let end = (pos + query.len() + SNIPPET_LENGTH / 2).min(text.len());
let mut snippet = text[start..end].to_string();
if start > 0 {
snippet = format!("...{}", snippet);
}
if end < text.len() {
snippet = format!("{}...", snippet);
}
return snippet;
}
}
// Fallback to title or first N chars
if let Some(title) = &obj.meta.title {
return title.clone();
}
if let Some(text) = &obj.text {
let end = SNIPPET_LENGTH.min(text.len());
let mut snippet = text[..end].to_string();
if end < text.len() {
snippet = format!("{}...", snippet);
}
return snippet;
}
String::from("[No content]")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
#[ignore]
async fn test_search() {
let client = HeroDbClient::new("redis://localhost:6379", 1).unwrap();
let engine = SearchEngine::new(client);
let query = RetrievalQuery::new("test".to_string())
.with_text("rust".to_string())
.with_top_k(10);
let results = engine.search(&query).await.unwrap();
assert!(results.len() <= 10);
}
}

View File

@@ -0,0 +1,91 @@
use serde::{Deserialize, Serialize};
use time::OffsetDateTime;
/// Base data that all OSIRIS objects must include
/// Similar to heromodels BaseModelData but adapted for OSIRIS
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
pub struct BaseData {
/// Unique ID (auto-generated or user-assigned)
pub id: u32,
/// Namespace this object belongs to
pub ns: String,
/// Unix timestamp for creation time
#[serde(with = "time::serde::timestamp")]
pub created_at: OffsetDateTime,
/// Unix timestamp for last modification time
#[serde(with = "time::serde::timestamp")]
pub modified_at: OffsetDateTime,
/// Optional MIME type
pub mime: Option<String>,
/// Content size in bytes
pub size: Option<u64>,
}
impl BaseData {
/// Create new base data with ID 0 (no namespace required)
pub fn new() -> Self {
let now = OffsetDateTime::now_utc();
Self {
id: 0,
ns: String::new(),
created_at: now,
modified_at: now,
mime: None,
size: None,
}
}
/// Create new base data with namespace
pub fn with_ns(ns: impl ToString) -> Self {
let now = OffsetDateTime::now_utc();
Self {
id: 0,
ns: ns.to_string(),
created_at: now,
modified_at: now,
mime: None,
size: None,
}
}
/// Create new base data with specific ID
pub fn with_id(id: u32, ns: String) -> Self {
let now = OffsetDateTime::now_utc();
Self {
id,
ns,
created_at: now,
modified_at: now,
mime: None,
size: None,
}
}
/// Update the modified timestamp
pub fn update_modified(&mut self) {
self.modified_at = OffsetDateTime::now_utc();
}
/// Set the MIME type
pub fn set_mime(&mut self, mime: Option<String>) {
self.mime = mime;
self.update_modified();
}
/// Set the size
pub fn set_size(&mut self, size: Option<u64>) {
self.size = size;
self.update_modified();
}
}
impl Default for BaseData {
fn default() -> Self {
Self::new()
}
}

View File

@@ -0,0 +1,135 @@
use crate::error::Result;
use crate::index::FieldIndex;
use crate::store::{HeroDbClient, Object};
/// Generic storage layer for OSIRIS objects
#[derive(Debug, Clone)]
pub struct GenericStore {
client: HeroDbClient,
index: FieldIndex,
}
impl GenericStore {
/// Create a new generic store
pub fn new(client: HeroDbClient) -> Self {
let index = FieldIndex::new(client.clone());
Self {
client,
index,
}
}
/// Store an object
pub async fn put<T: Object>(&self, obj: &T) -> Result<()> {
// Serialize object to JSON
let json = obj.to_json()?;
let key = format!("obj:{}:{}", obj.namespace(), obj.id());
// Store in HeroDB
self.client.set(&key, &json).await?;
// Index the object
self.index_object(obj).await?;
Ok(())
}
/// Get an object by ID
pub async fn get<T: Object>(&self, ns: &str, id: &str) -> Result<T> {
let key = format!("obj:{}:{}", ns, id);
let json = self.client.get(&key).await?
.ok_or_else(|| crate::error::Error::NotFound(format!("Object {}:{}", ns, id)))?;
T::from_json(&json)
}
/// Get raw JSON data by ID (for generic access without type)
pub async fn get_raw(&self, ns: &str, id: &str) -> Result<String> {
let key = format!("obj:{}:{}", ns, id);
self.client.get(&key).await?
.ok_or_else(|| crate::error::Error::NotFound(format!("Object {}:{}", ns, id)))
}
/// Delete an object
pub async fn delete<T: Object>(&self, obj: &T) -> Result<bool> {
let key = format!("obj:{}:{}", obj.namespace(), obj.id());
// Deindex first
self.deindex_object(obj).await?;
// Delete from HeroDB
self.client.del(&key).await
}
/// Check if an object exists
pub async fn exists(&self, ns: &str, id: &str) -> Result<bool> {
let key = format!("obj:{}:{}", ns, id);
self.client.exists(&key).await
}
/// Index an object
async fn index_object<T: Object>(&self, obj: &T) -> Result<()> {
let index_keys = obj.index_keys();
for key in index_keys {
let field_key = format!("idx:{}:{}:{}", obj.namespace(), key.name, key.value);
self.client.sadd(&field_key, &obj.id().to_string()).await?;
}
// Add to scan index for full-text search
let scan_key = format!("scan:{}", obj.namespace());
self.client.sadd(&scan_key, &obj.id().to_string()).await?;
Ok(())
}
/// Deindex an object
async fn deindex_object<T: Object>(&self, obj: &T) -> Result<()> {
let index_keys = obj.index_keys();
for key in index_keys {
let field_key = format!("idx:{}:{}:{}", obj.namespace(), key.name, key.value);
self.client.srem(&field_key, &obj.id().to_string()).await?;
}
// Remove from scan index
let scan_key = format!("scan:{}", obj.namespace());
self.client.srem(&scan_key, &obj.id().to_string()).await?;
Ok(())
}
/// Get all IDs matching an index key
pub async fn get_ids_by_index(&self, ns: &str, field: &str, value: &str) -> Result<Vec<String>> {
let field_key = format!("idx:{}:{}:{}", ns, field, value);
self.client.smembers(&field_key).await
}
/// Get all IDs in a namespace
pub async fn get_all_ids(&self, ns: &str) -> Result<Vec<String>> {
let scan_key = format!("scan:{}", ns);
self.client.smembers(&scan_key).await
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::objects::Note;
#[tokio::test]
#[ignore]
async fn test_generic_store() {
let client = HeroDbClient::new("redis://localhost:6379", 1).unwrap();
let store = GenericStore::new(client);
let note = Note::new("test".to_string())
.set_title("Test Note")
.set_content("This is a test");
store.put(&note).await.unwrap();
let retrieved: Note = store.get("test", note.id()).await.unwrap();
assert_eq!(retrieved.title, note.title);
}
}

View File

@@ -0,0 +1,161 @@
use crate::error::{Error, Result};
use crate::store::OsirisObject;
use redis::aio::MultiplexedConnection;
use redis::{AsyncCommands, Client};
/// HeroDB client wrapper for OSIRIS operations
#[derive(Clone, Debug)]
pub struct HeroDbClient {
client: Client,
pub db_id: u16,
}
impl HeroDbClient {
/// Create a new HeroDB client
pub fn new(url: &str, db_id: u16) -> Result<Self> {
let client = Client::open(url)?;
Ok(Self { client, db_id })
}
/// Get a connection to the database
pub async fn get_connection(&self) -> Result<MultiplexedConnection> {
let mut conn = self.client.get_multiplexed_async_connection().await?;
// Select the appropriate database
if self.db_id > 0 {
redis::cmd("SELECT")
.arg(self.db_id)
.query_async(&mut conn)
.await?;
}
Ok(conn)
}
/// Store an object in HeroDB
pub async fn put_object(&self, obj: &OsirisObject) -> Result<()> {
let mut conn = self.get_connection().await?;
let key = format!("meta:{}", obj.id);
let value = serde_json::to_string(obj)?;
conn.set(&key, value).await?;
Ok(())
}
/// Retrieve an object from HeroDB
pub async fn get_object(&self, id: &str) -> Result<OsirisObject> {
let mut conn = self.get_connection().await?;
let key = format!("meta:{}", id);
let value: Option<String> = conn.get(&key).await?;
match value {
Some(v) => {
let obj: OsirisObject = serde_json::from_str(&v)?;
Ok(obj)
}
None => Err(Error::NotFound(format!("Object not found: {}", id))),
}
}
/// Delete an object from HeroDB
pub async fn delete_object(&self, id: &str) -> Result<bool> {
let mut conn = self.get_connection().await?;
let key = format!("meta:{}", id);
let deleted: i32 = conn.del(&key).await?;
Ok(deleted > 0)
}
/// Check if an object exists
pub async fn exists(&self, id: &str) -> Result<bool> {
let mut conn = self.get_connection().await?;
let key = format!("meta:{}", id);
let exists: bool = conn.exists(&key).await?;
Ok(exists)
}
/// Add an ID to a set (for field indexing)
pub async fn sadd(&self, set_key: &str, member: &str) -> Result<()> {
let mut conn = self.get_connection().await?;
conn.sadd(set_key, member).await?;
Ok(())
}
/// Remove an ID from a set
pub async fn srem(&self, set_key: &str, member: &str) -> Result<()> {
let mut conn = self.get_connection().await?;
conn.srem(set_key, member).await?;
Ok(())
}
/// Get all members of a set
pub async fn smembers(&self, set_key: &str) -> Result<Vec<String>> {
let mut conn = self.get_connection().await?;
let members: Vec<String> = conn.smembers(set_key).await?;
Ok(members)
}
/// Get the intersection of multiple sets
pub async fn sinter(&self, keys: &[String]) -> Result<Vec<String>> {
let mut conn = self.get_connection().await?;
let members: Vec<String> = conn.sinter(keys).await?;
Ok(members)
}
/// Get all keys matching a pattern
pub async fn keys(&self, pattern: &str) -> Result<Vec<String>> {
let mut conn = self.get_connection().await?;
let keys: Vec<String> = conn.keys(pattern).await?;
Ok(keys)
}
/// Set a key-value pair
pub async fn set(&self, key: &str, value: &str) -> Result<()> {
let mut conn = self.get_connection().await?;
conn.set(key, value).await?;
Ok(())
}
/// Get a value by key
pub async fn get(&self, key: &str) -> Result<Option<String>> {
let mut conn = self.get_connection().await?;
let value: Option<String> = conn.get(key).await?;
Ok(value)
}
/// Delete a key
pub async fn del(&self, key: &str) -> Result<bool> {
let mut conn = self.get_connection().await?;
let deleted: i32 = conn.del(key).await?;
Ok(deleted > 0)
}
/// Get database size (number of keys)
pub async fn dbsize(&self) -> Result<usize> {
let mut conn = self.get_connection().await?;
let size: usize = redis::cmd("DBSIZE").query_async(&mut conn).await?;
Ok(size)
}
}
#[cfg(test)]
mod tests {
use super::*;
// Note: These tests require a running HeroDB instance
// They are ignored by default
#[tokio::test]
#[ignore]
async fn test_put_get_object() {
let client = HeroDbClient::new("redis://localhost:6379", 1).unwrap();
let obj = OsirisObject::new("test".to_string(), Some("Hello".to_string()));
client.put_object(&obj).await.unwrap();
let retrieved = client.get_object(&obj.id).await.unwrap();
assert_eq!(obj.id, retrieved.id);
assert_eq!(obj.text, retrieved.text);
}
}

View File

@@ -0,0 +1,11 @@
pub mod base_data;
pub mod object_trait;
pub mod herodb_client;
pub mod generic_store;
pub mod object; // Keep old implementation for backwards compat temporarily
pub use base_data::BaseData;
pub use object_trait::{IndexKey, Object, Storable};
pub use herodb_client::HeroDbClient;
pub use generic_store::GenericStore;
pub use object::{Metadata, OsirisObject}; // Old implementation

View File

@@ -0,0 +1,160 @@
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use time::OffsetDateTime;
/// Core OSIRIS object structure
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct OsirisObject {
/// Unique identifier (UUID or user-assigned)
pub id: String,
/// Namespace (e.g., "notes", "calendar")
pub ns: String,
/// Metadata
pub meta: Metadata,
/// Optional plain text content
#[serde(skip_serializing_if = "Option::is_none")]
pub text: Option<String>,
}
/// Object metadata
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Metadata {
/// Optional human-readable title
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
/// MIME type
#[serde(skip_serializing_if = "Option::is_none")]
pub mime: Option<String>,
/// Key-value tags for categorization
#[serde(default)]
pub tags: BTreeMap<String, String>,
/// Creation timestamp
#[serde(with = "time::serde::rfc3339")]
pub created: OffsetDateTime,
/// Last update timestamp
#[serde(with = "time::serde::rfc3339")]
pub updated: OffsetDateTime,
/// Content size in bytes
#[serde(skip_serializing_if = "Option::is_none")]
pub size: Option<u64>,
}
impl OsirisObject {
/// Create a new object with generated UUID
pub fn new(ns: String, text: Option<String>) -> Self {
let now = OffsetDateTime::now_utc();
Self {
id: uuid::Uuid::new_v4().to_string(),
ns,
meta: Metadata {
title: None,
mime: None,
tags: BTreeMap::new(),
created: now,
updated: now,
size: text.as_ref().map(|t| t.len() as u64),
},
text,
}
}
/// Create a new object with specific ID
pub fn with_id(id: String, ns: String, text: Option<String>) -> Self {
let now = OffsetDateTime::now_utc();
Self {
id,
ns,
meta: Metadata {
title: None,
mime: None,
tags: BTreeMap::new(),
created: now,
updated: now,
size: text.as_ref().map(|t| t.len() as u64),
},
text,
}
}
/// Update the object's text content
pub fn update_text(&mut self, text: Option<String>) {
self.meta.updated = OffsetDateTime::now_utc();
self.meta.size = text.as_ref().map(|t| t.len() as u64);
self.text = text;
}
/// Add or update a tag
pub fn set_tag(&mut self, key: String, value: String) {
self.meta.tags.insert(key, value);
self.meta.updated = OffsetDateTime::now_utc();
}
/// Remove a tag
pub fn remove_tag(&mut self, key: &str) -> Option<String> {
let result = self.meta.tags.remove(key);
if result.is_some() {
self.meta.updated = OffsetDateTime::now_utc();
}
result
}
/// Set the title
pub fn set_title(&mut self, title: Option<String>) {
self.meta.title = title;
self.meta.updated = OffsetDateTime::now_utc();
}
/// Set the MIME type
pub fn set_mime(&mut self, mime: Option<String>) {
self.meta.mime = mime;
self.meta.updated = OffsetDateTime::now_utc();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_new_object() {
let obj = OsirisObject::new("notes".to_string(), Some("Hello, world!".to_string()));
assert_eq!(obj.ns, "notes");
assert_eq!(obj.text, Some("Hello, world!".to_string()));
assert_eq!(obj.meta.size, Some(13));
}
#[test]
fn test_update_text() {
let mut obj = OsirisObject::new("notes".to_string(), Some("Initial".to_string()));
let initial_updated = obj.meta.updated;
std::thread::sleep(std::time::Duration::from_millis(10));
obj.update_text(Some("Updated".to_string()));
assert_eq!(obj.text, Some("Updated".to_string()));
assert_eq!(obj.meta.size, Some(7));
assert!(obj.meta.updated > initial_updated);
}
#[test]
fn test_tags() {
let mut obj = OsirisObject::new("notes".to_string(), None);
obj.set_tag("topic".to_string(), "rust".to_string());
obj.set_tag("project".to_string(), "osiris".to_string());
assert_eq!(obj.meta.tags.get("topic"), Some(&"rust".to_string()));
assert_eq!(obj.meta.tags.get("project"), Some(&"osiris".to_string()));
let removed = obj.remove_tag("topic");
assert_eq!(removed, Some("rust".to_string()));
assert_eq!(obj.meta.tags.get("topic"), None);
}
}

View File

@@ -0,0 +1,113 @@
use crate::error::Result;
use crate::store::BaseData;
use serde::{Deserialize, Serialize};
use std::fmt::Debug;
/// Represents an index key for an object field
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct IndexKey {
/// The name of the index key (field name)
pub name: &'static str,
/// The value of the index key for this object instance
pub value: String,
}
impl IndexKey {
pub fn new(name: &'static str, value: impl ToString) -> Self {
Self {
name,
value: value.to_string(),
}
}
}
/// Core trait that all OSIRIS objects must implement
/// Similar to heromodels Model trait but adapted for OSIRIS
pub trait Object: Debug + Clone + Serialize + for<'de> Deserialize<'de> + Send + Sync {
/// Get the object type name (used for routing/identification)
fn object_type() -> &'static str
where
Self: Sized;
/// Get a reference to the base data
fn base_data(&self) -> &BaseData;
/// Get a mutable reference to the base data
fn base_data_mut(&mut self) -> &mut BaseData;
/// Get the unique ID for this object
fn id(&self) -> u32 {
self.base_data().id
}
/// Set the unique ID for this object
fn set_id(&mut self, id: u32) {
self.base_data_mut().id = id;
}
/// Get the namespace for this object
fn namespace(&self) -> &str {
&self.base_data().ns
}
/// Returns a list of index keys for this object instance
/// These are generated from fields marked with #[index]
/// The default implementation returns base_data indexes only
fn index_keys(&self) -> Vec<IndexKey> {
let base = self.base_data();
let mut keys = Vec::new();
// Index MIME type if present
if let Some(mime) = &base.mime {
keys.push(IndexKey::new("mime", mime));
}
keys
}
/// Return a list of field names which have an index applied
/// This should be implemented by the derive macro
fn indexed_fields() -> Vec<&'static str>
where
Self: Sized,
{
Vec::new()
}
/// Get the full-text searchable content for this object
/// Override this to provide custom searchable text
fn searchable_text(&self) -> Option<String> {
None
}
/// Serialize the object to JSON
fn to_json(&self) -> Result<String> {
serde_json::to_string(self).map_err(Into::into)
}
/// Deserialize the object from JSON
fn from_json(json: &str) -> Result<Self>
where
Self: Sized,
{
serde_json::from_str(json).map_err(Into::into)
}
/// Update the modified timestamp
fn touch(&mut self) {
self.base_data_mut().update_modified();
}
}
/// Trait for objects that can be stored in OSIRIS
/// This is automatically implemented for all types that implement Object
pub trait Storable: Object {
/// Prepare the object for storage (update timestamps, etc.)
fn prepare_for_storage(&mut self) {
self.touch();
}
}
// Blanket implementation
impl<T: Object> Storable for T {}

View File

@@ -0,0 +1,14 @@
[package]
name = "osiris-derive"
version.workspace = true
edition.workspace = true
description = "Derive macros for Osiris"
license = "MIT OR Apache-2.0"
[lib]
proc-macro = true
[dependencies]
syn = { version = "2.0", features = ["full", "extra-traits"] }
quote = "1.0"
proc-macro2 = "1.0"

View File

@@ -0,0 +1,202 @@
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, Data, DeriveInput, Fields, Type};
/// Derive macro for the Object trait
///
/// Automatically implements `index_keys()` and `indexed_fields()` based on fields marked with #[index]
///
/// # Example
///
/// ```rust
/// #[derive(Object)]
/// pub struct Note {
/// pub base_data: BaseData,
///
/// #[index]
/// pub title: Option<String>,
///
/// pub content: Option<String>,
///
/// #[index]
/// pub tags: BTreeMap<String, String>,
/// }
/// ```
#[proc_macro_derive(Object, attributes(index))]
pub fn derive_object(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let name = &input.ident;
let generics = &input.generics;
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
// Extract fields with #[index] attribute
let indexed_fields = match &input.data {
Data::Struct(data) => match &data.fields {
Fields::Named(fields) => {
fields.named.iter().filter_map(|field| {
let has_index = field.attrs.iter().any(|attr| {
attr.path().is_ident("index")
});
if has_index {
let field_name = field.ident.as_ref()?;
let field_type = &field.ty;
Some((field_name.clone(), field_type.clone()))
} else {
None
}
}).collect::<Vec<_>>()
}
_ => vec![],
},
_ => vec![],
};
// Generate index_keys() implementation
let index_keys_impl = generate_index_keys(&indexed_fields);
// Generate indexed_fields() implementation
let field_names: Vec<_> = indexed_fields.iter()
.map(|(name, _)| name.to_string())
.collect();
// Always use ::osiris for external usage
// When used inside the osiris crate's src/, the compiler will resolve it correctly
let crate_path = quote! { ::osiris };
let expanded = quote! {
impl #impl_generics #crate_path::Object for #name #ty_generics #where_clause {
fn object_type() -> &'static str {
stringify!(#name)
}
fn base_data(&self) -> &#crate_path::BaseData {
&self.base_data
}
fn base_data_mut(&mut self) -> &mut #crate_path::BaseData {
&mut self.base_data
}
fn index_keys(&self) -> Vec<#crate_path::IndexKey> {
let mut keys = Vec::new();
// Index from base_data
if let Some(mime) = &self.base_data.mime {
keys.push(#crate_path::IndexKey::new("mime", mime));
}
#index_keys_impl
keys
}
fn indexed_fields() -> Vec<&'static str> {
vec![#(#field_names),*]
}
}
};
TokenStream::from(expanded)
}
fn generate_index_keys(fields: &[(syn::Ident, Type)]) -> proc_macro2::TokenStream {
let mut implementations = Vec::new();
// Always use ::osiris
let crate_path = quote! { ::osiris };
for (field_name, field_type) in fields {
let field_name_str = field_name.to_string();
// Check if it's an Option type
if is_option_type(field_type) {
implementations.push(quote! {
if let Some(value) = &self.#field_name {
keys.push(#crate_path::IndexKey::new(#field_name_str, value));
}
});
}
// Check if it's a BTreeMap (for tags)
else if is_btreemap_type(field_type) {
implementations.push(quote! {
for (key, value) in &self.#field_name {
keys.push(#crate_path::IndexKey {
name: concat!(#field_name_str, ":tag"),
value: format!("{}={}", key, value),
});
}
});
}
// Check if it's a Vec
else if is_vec_type(field_type) {
implementations.push(quote! {
for (idx, value) in self.#field_name.iter().enumerate() {
keys.push(#crate_path::IndexKey {
name: concat!(#field_name_str, ":item"),
value: format!("{}:{}", idx, value),
});
}
});
}
// For OffsetDateTime, index as date string
else if is_offsetdatetime_type(field_type) {
implementations.push(quote! {
{
let date_str = self.#field_name.date().to_string();
keys.push(#crate_path::IndexKey::new(#field_name_str, date_str));
}
});
}
// For enums or other types, convert to string
else {
implementations.push(quote! {
{
let value_str = format!("{:?}", &self.#field_name);
keys.push(#crate_path::IndexKey::new(#field_name_str, value_str));
}
});
}
}
quote! {
#(#implementations)*
}
}
fn is_option_type(ty: &Type) -> bool {
if let Type::Path(type_path) = ty {
if let Some(segment) = type_path.path.segments.last() {
return segment.ident == "Option";
}
}
false
}
fn is_btreemap_type(ty: &Type) -> bool {
if let Type::Path(type_path) = ty {
if let Some(segment) = type_path.path.segments.last() {
return segment.ident == "BTreeMap";
}
}
false
}
fn is_vec_type(ty: &Type) -> bool {
if let Type::Path(type_path) = ty {
if let Some(segment) = type_path.path.segments.last() {
return segment.ident == "Vec";
}
}
false
}
fn is_offsetdatetime_type(ty: &Type) -> bool {
if let Type::Path(type_path) = ty {
if let Some(segment) = type_path.path.segments.last() {
return segment.ident == "OffsetDateTime";
}
}
false
}

42
lib/runner/Cargo.toml Normal file
View File

@@ -0,0 +1,42 @@
[package]
name = "hero-runner"
version.workspace = true
edition.workspace = true
description = "Hero Runner library for executing jobs"
license = "MIT OR Apache-2.0"
[lib]
name = "hero_runner"
path = "lib.rs"
[dependencies]
# Core dependencies
anyhow.workspace = true
redis.workspace = true
serde.workspace = true
serde_json.workspace = true
tokio.workspace = true
log.workspace = true
env_logger.workspace = true
uuid.workspace = true
chrono.workspace = true
toml.workspace = true
thiserror.workspace = true
async-trait.workspace = true
# Crypto dependencies
secp256k1.workspace = true
sha2.workspace = true
hex.workspace = true
# Rhai scripting
rhai = { version = "1.21.0", features = ["std", "sync", "decimal", "internals", "serde"] }
# Hero dependencies
hero-job = { path = "../models/job" }
hero-job-client = { path = "../clients/job" }
hero_logger = { git = "https://git.ourworld.tf/herocode/baobab.git", branch = "logger" }
# Tracing
tracing = "0.1.41"
rand = "0.8"

270
lib/runner/async_runner.rs Normal file
View File

@@ -0,0 +1,270 @@
use crate::Job;
use log::{debug, error, info};
use rhai::{Engine, packages::Package};
use std::collections::HashMap;
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::{mpsc, Mutex};
use tokio::task::JoinHandle;
use crate::runner_trait::Runner;
/// Represents a running job with its handle and metadata
struct RunningJob {
job_id: String,
handle: JoinHandle<Result<String, Box<dyn std::error::Error + Send + Sync>>>,
started_at: std::time::Instant,
}
/// Builder for AsyncRunner
#[derive(Default)]
pub struct AsyncRunnerBuilder {
runner_id: Option<String>,
db_path: Option<String>,
redis_url: Option<String>,
default_timeout: Option<Duration>,
engine: Option<Arc<dyn Fn() -> Engine + Send + Sync>>,
}
impl AsyncRunnerBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn runner_id<S: Into<String>>(mut self, runner_id: S) -> Self {
self.runner_id = Some(runner_id.into());
self
}
pub fn db_path<S: Into<String>>(mut self, db_path: S) -> Self {
self.db_path = Some(db_path.into());
self
}
pub fn redis_url<S: Into<String>>(mut self, redis_url: S) -> Self {
self.redis_url = Some(redis_url.into());
self
}
pub fn default_timeout(mut self, timeout: Duration) -> Self {
self.default_timeout = Some(timeout);
self
}
pub fn engine_factory<F>(mut self, factory: F) -> Self
where
F: Fn() -> Engine + Send + Sync + 'static,
{
self.engine = Some(Arc::new(factory));
self
}
pub fn build(self) -> Result<AsyncRunner, String> {
Ok(AsyncRunner {
runner_id: self.runner_id.ok_or("runner_id is required")?,
db_path: self.db_path.ok_or("db_path is required")?,
redis_url: self.redis_url.ok_or("redis_url is required")?,
default_timeout: self.default_timeout.unwrap_or(Duration::from_secs(300)),
engine_factory: self.engine.ok_or("engine factory is required")?,
running_jobs: Arc::new(Mutex::new(HashMap::new())),
})
}
}
/// Asynchronous runner that processes jobs concurrently
pub struct AsyncRunner {
pub runner_id: String,
pub db_path: String,
pub redis_url: String,
pub default_timeout: Duration,
pub engine_factory: Arc<dyn Fn() -> Engine + Send + Sync>,
running_jobs: Arc<Mutex<HashMap<String, RunningJob>>>,
}
impl AsyncRunner {
/// Create a new AsyncRunnerBuilder
pub fn builder() -> AsyncRunnerBuilder {
AsyncRunnerBuilder::new()
}
/// Add a running job to the tracking map
async fn add_running_job(&self, job_id: String, handle: JoinHandle<Result<String, Box<dyn std::error::Error + Send + Sync>>>) {
let running_job = RunningJob {
job_id: job_id.clone(),
handle,
started_at: std::time::Instant::now(),
};
let mut jobs = self.running_jobs.lock().await;
jobs.insert(job_id.clone(), running_job);
debug!("Async Runner: Added running job '{}'. Total running: {}",
job_id, jobs.len());
}
/// Remove a completed job from the tracking map
async fn remove_running_job(&self, job_id: &str) {
let mut jobs = self.running_jobs.lock().await;
if let Some(job) = jobs.remove(job_id) {
let duration = job.started_at.elapsed();
debug!("Async Runner: Removed completed job '{}' after {:?}. Remaining: {}",
job_id, duration, jobs.len());
}
}
/// Get the count of currently running jobs
pub async fn running_job_count(&self) -> usize {
let jobs = self.running_jobs.lock().await;
jobs.len()
}
/// Cleanup any finished jobs from the running jobs map
async fn cleanup_finished_jobs(&self) {
let mut jobs = self.running_jobs.lock().await;
let mut to_remove = Vec::new();
for (job_id, running_job) in jobs.iter() {
if running_job.handle.is_finished() {
to_remove.push(job_id.clone());
}
}
for job_id in to_remove {
if let Some(job) = jobs.remove(&job_id) {
let duration = job.started_at.elapsed();
debug!("Async Runner: Cleaned up finished job '{}' after {:?}",
job_id, duration);
}
}
}
}
impl Runner for AsyncRunner {
fn process_job(&self, job: Job) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
let job_id = job.id.clone();
let runner_id = &self.runner_id;
// Determine timeout (use job-specific timeout if available, otherwise default)
let job_timeout = if job.timeout > 0 {
Duration::from_secs(job.timeout)
} else {
self.default_timeout
};
info!("Async Runner '{}', Job {}: Spawning job execution task with timeout {:?}",
runner_id, job_id, job_timeout);
// Clone necessary data for the spawned task
let job_id_clone = job_id.clone();
let runner_id_clone = runner_id.clone();
let runner_id_debug = runner_id.clone();
let job_id_debug = job_id.clone();
let _redis_url_clone = self.redis_url.clone();
let running_jobs_clone = Arc::clone(&self.running_jobs);
let engine_factory = Arc::clone(&self.engine_factory);
let db_path_clone = self.db_path.clone();
// Spawn the job execution task
let job_handle = tokio::spawn(async move {
// Create a new engine instance (cheap with factory pattern)
let mut engine = engine_factory();
let mut db_config = rhai::Map::new();
db_config.insert("DB_PATH".into(), db_path_clone.into());
db_config.insert("CALLER_ID".into(), job.caller_id.clone().into());
db_config.insert("CONTEXT_ID".into(), job.context_id.clone().into());
engine.set_default_tag(rhai::Dynamic::from(db_config));
// Execute the Rhai script
let result = match engine.eval::<rhai::Dynamic>(&job.payload) {
Ok(result) => {
let result_str = if result.is::<String>() {
result.into_string().unwrap()
} else {
result.to_string()
};
info!("Async Runner '{}', Job {}: Script executed successfully. Result: {}",
runner_id_clone, job_id_clone, result_str);
Ok(result_str)
}
Err(e) => {
let error_msg = format!("Script execution error: {}", e);
error!("Async Runner '{}', Job {}: {}", runner_id_clone, job_id_clone, error_msg);
Err(Box::new(e) as Box<dyn std::error::Error + Send + Sync>)
}
};
// Remove this job from the running jobs map when it completes
let mut jobs = running_jobs_clone.lock().await;
if let Some(running_job) = jobs.remove(&job_id_clone) {
let duration = running_job.started_at.elapsed();
debug!("Async Runner '{}': Removed completed job '{}' after {:?}",
runner_id_debug, job_id_debug, duration);
}
result
});
// Add the job to the running jobs map
let running_job = RunningJob {
job_id: job_id.clone(),
handle: job_handle,
started_at: std::time::Instant::now(),
};
let running_jobs_clone = Arc::clone(&self.running_jobs);
let job_id_for_map = job_id.clone();
tokio::spawn(async move {
let mut jobs = running_jobs_clone.lock().await;
jobs.insert(job_id_for_map, running_job);
debug!("Async Runner: Added running job '{}'. Total running: {}",
job_id, jobs.len());
});
// For async runners, we return immediately with a placeholder
// The actual result will be handled by the spawned task
Ok("Job spawned for async processing".to_string())
}
fn runner_type(&self) -> &'static str {
"Async"
}
fn runner_id(&self) -> &str {
&self.runner_id
}
fn redis_url(&self) -> &str {
&self.redis_url
}
}
/// Convenience function to spawn an asynchronous runner using the trait interface
///
/// This function provides a clean interface for the new async runner implementation
/// with timeout support.
pub fn spawn_async_runner<F>(
runner_id: String,
db_path: String,
redis_url: String,
shutdown_rx: mpsc::Receiver<()>,
default_timeout: std::time::Duration,
engine_factory: F,
) -> JoinHandle<Result<(), Box<dyn std::error::Error + Send + Sync>>>
where
F: Fn() -> Engine + Send + Sync + 'static,
{
use std::sync::Arc;
let runner = Arc::new(
AsyncRunner::builder()
.runner_id(runner_id)
.db_path(db_path)
.redis_url(redis_url)
.default_timeout(default_timeout)
.engine_factory(engine_factory)
.build()
.expect("Failed to build AsyncRunner")
);
crate::runner_trait::spawn_runner(runner, shutdown_rx)
}

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