implement osis actor
This commit is contained in:
parent
8f6ea5350f
commit
33dfc0dbe3
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
target
|
4189
Cargo.lock
generated
Normal file
4189
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
52
Cargo.toml
Normal file
52
Cargo.toml
Normal file
@ -0,0 +1,52 @@
|
||||
[package]
|
||||
name = "actor_osis"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[lib]
|
||||
name = "actor_osis" # Can be different from package name, or same
|
||||
path = "src/lib.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "actor_osis"
|
||||
path = "cmd/actor_osis.rs"
|
||||
|
||||
[[example]]
|
||||
name = "engine"
|
||||
path = "examples/engine.rs"
|
||||
|
||||
[[example]]
|
||||
name = "actor"
|
||||
path = "examples/actor.rs"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
redis = { version = "0.25.0", features = ["tokio-comp"] }
|
||||
rhai = { version = "1.21.0", features = ["std", "sync", "decimal", "internals"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
tokio = { version = "1", features = ["macros", "rt-multi-thread", "time"] }
|
||||
log = "0.4"
|
||||
env_logger = "0.10"
|
||||
clap = { version = "4.4", features = ["derive"] }
|
||||
uuid = { version = "1.6", features = ["v4", "serde"] } # Though task_id is string, uuid might be useful
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
toml = "0.8"
|
||||
thiserror = "1.0"
|
||||
async-trait = "0.1"
|
||||
hero_job = { git = "https://git.ourworld.tf/herocode/baobab.git"}
|
||||
baobab_actor = { git = "https://git.ourworld.tf/herocode/baobab.git"}
|
||||
heromodels = { git = "https://git.ourworld.tf/herocode/db.git" }
|
||||
heromodels_core = { git = "https://git.ourworld.tf/herocode/db.git" }
|
||||
heromodels-derive = { git = "https://git.ourworld.tf/herocode/db.git" }
|
||||
rhailib_dsl = { git = "https://git.ourworld.tf/herocode/rhailib.git" }
|
||||
|
||||
[features]
|
||||
default = ["calendar", "finance"]
|
||||
calendar = []
|
||||
finance = []
|
||||
flow = []
|
||||
legal = []
|
||||
projects = []
|
||||
biz = []
|
233
_archive/osis.rs
Normal file
233
_archive/osis.rs
Normal file
@ -0,0 +1,233 @@
|
||||
//! OSIS Worker Binary - Synchronous actor for system-level operations
|
||||
|
||||
use clap::Parser;
|
||||
use log::{error, info};
|
||||
use baobab_actor::config::{ConfigError, WorkerConfig};
|
||||
use baobab_actor::engine::create_heromodels_engine;
|
||||
use baobab_actor::sync_actor::SyncWorker;
|
||||
use baobab_actor::actor_trait::{spawn_actor, WorkerConfig as TraitWorkerConfig};
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use tokio::signal;
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(
|
||||
name = "osis",
|
||||
version = "0.1.0",
|
||||
about = "OSIS (Operating System Integration Service) - Synchronous Worker",
|
||||
long_about = "A synchronous actor for Hero framework that processes jobs sequentially. \
|
||||
Ideal for system-level operations that require careful resource management."
|
||||
)]
|
||||
struct Args {
|
||||
/// Path to TOML configuration file
|
||||
#[arg(short, long, help = "Path to TOML configuration file")]
|
||||
config: PathBuf,
|
||||
|
||||
/// Override actor ID from config
|
||||
#[arg(long, help = "Override actor ID from configuration file")]
|
||||
actor_id: Option<String>,
|
||||
|
||||
/// Override Redis URL from config
|
||||
#[arg(long, help = "Override Redis URL from configuration file")]
|
||||
redis_url: Option<String>,
|
||||
|
||||
/// Override database path from config
|
||||
#[arg(long, help = "Override database path from configuration file")]
|
||||
db_path: Option<String>,
|
||||
|
||||
/// Enable verbose logging (debug level)
|
||||
#[arg(short, long, help = "Enable verbose logging")]
|
||||
verbose: bool,
|
||||
|
||||
/// Disable timestamps in log output
|
||||
#[arg(long, help = "Remove timestamps from log output")]
|
||||
no_timestamp: bool,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
let args = Args::parse();
|
||||
|
||||
// Load configuration from TOML file
|
||||
let mut config = match WorkerConfig::from_file(&args.config) {
|
||||
Ok(config) => config,
|
||||
Err(e) => {
|
||||
eprintln!("Failed to load configuration from {:?}: {}", args.config, e);
|
||||
std::process::exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
// Validate that this is a sync actor configuration
|
||||
if !config.is_sync() {
|
||||
eprintln!("Error: OSIS actor requires a sync actor configuration");
|
||||
eprintln!("Expected: [actor_type] type = \"sync\"");
|
||||
eprintln!("Found: {:?}", config.actor_type);
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
// Apply command line overrides
|
||||
if let Some(actor_id) = args.actor_id {
|
||||
config.actor_id = actor_id;
|
||||
}
|
||||
if let Some(redis_url) = args.redis_url {
|
||||
config.redis_url = redis_url;
|
||||
}
|
||||
if let Some(db_path) = args.db_path {
|
||||
config.db_path = db_path;
|
||||
}
|
||||
|
||||
// Configure logging
|
||||
setup_logging(&config, args.verbose, args.no_timestamp)?;
|
||||
|
||||
info!("🚀 OSIS Worker starting...");
|
||||
info!("Worker ID: {}", config.actor_id);
|
||||
info!("Redis URL: {}", config.redis_url);
|
||||
info!("Database Path: {}", config.db_path);
|
||||
info!("Preserve Tasks: {}", config.preserve_tasks);
|
||||
|
||||
// Create Rhai engine
|
||||
let engine = create_heromodels_engine();
|
||||
info!("✅ Rhai engine initialized");
|
||||
|
||||
// Create actor configuration for the trait-based interface
|
||||
let actor_config = TraitWorkerConfig::new(
|
||||
config.actor_id.clone(),
|
||||
config.db_path.clone(),
|
||||
config.redis_url.clone(),
|
||||
config.preserve_tasks,
|
||||
);
|
||||
|
||||
// Create sync actor instance
|
||||
let actor = Arc::new(SyncWorker::default());
|
||||
info!("✅ Sync actor instance created");
|
||||
|
||||
// Setup shutdown signal handling
|
||||
let (shutdown_tx, shutdown_rx) = mpsc::channel(1);
|
||||
|
||||
// Spawn shutdown signal handler
|
||||
let shutdown_tx_clone = shutdown_tx.clone();
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = signal::ctrl_c().await {
|
||||
error!("Failed to listen for shutdown signal: {}", e);
|
||||
return;
|
||||
}
|
||||
info!("🛑 Shutdown signal received");
|
||||
if let Err(e) = shutdown_tx_clone.send(()).await {
|
||||
error!("Failed to send shutdown signal: {}", e);
|
||||
}
|
||||
});
|
||||
|
||||
// Spawn the actor
|
||||
info!("🔄 Starting actor loop...");
|
||||
let actor_handle = spawn_actor(actor, engine, shutdown_rx);
|
||||
|
||||
// Wait for the actor to complete
|
||||
match actor_handle.await {
|
||||
Ok(Ok(())) => {
|
||||
info!("✅ OSIS Worker shut down gracefully");
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
error!("❌ OSIS Worker encountered an error: {}", e);
|
||||
std::process::exit(1);
|
||||
}
|
||||
Err(e) => {
|
||||
error!("❌ Failed to join actor task: {}", e);
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Setup logging based on configuration and command line arguments
|
||||
fn setup_logging(
|
||||
config: &WorkerConfig,
|
||||
verbose: bool,
|
||||
no_timestamp: bool,
|
||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
let mut builder = env_logger::Builder::new();
|
||||
|
||||
// Determine log level
|
||||
let log_level = if verbose {
|
||||
"debug"
|
||||
} else {
|
||||
&config.logging.level
|
||||
};
|
||||
|
||||
// Set log level
|
||||
builder.filter_level(match log_level.to_lowercase().as_str() {
|
||||
"trace" => log::LevelFilter::Trace,
|
||||
"debug" => log::LevelFilter::Debug,
|
||||
"info" => log::LevelFilter::Info,
|
||||
"warn" => log::LevelFilter::Warn,
|
||||
"error" => log::LevelFilter::Error,
|
||||
_ => {
|
||||
eprintln!("Invalid log level: {}. Using 'info'", log_level);
|
||||
log::LevelFilter::Info
|
||||
}
|
||||
});
|
||||
|
||||
// Configure timestamps
|
||||
let show_timestamps = !no_timestamp && config.logging.timestamps;
|
||||
if !show_timestamps {
|
||||
builder.format_timestamp(None);
|
||||
}
|
||||
|
||||
builder.init();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::io::Write;
|
||||
use tempfile::NamedTempFile;
|
||||
|
||||
#[test]
|
||||
fn test_config_validation() {
|
||||
let config_toml = r#"
|
||||
actor_id = "test_osis"
|
||||
redis_url = "redis://localhost:6379"
|
||||
db_path = "/tmp/test_db"
|
||||
|
||||
[actor_type]
|
||||
type = "sync"
|
||||
|
||||
[logging]
|
||||
level = "info"
|
||||
"#;
|
||||
|
||||
let mut temp_file = NamedTempFile::new().unwrap();
|
||||
temp_file.write_all(config_toml.as_bytes()).unwrap();
|
||||
|
||||
let config = WorkerConfig::from_file(temp_file.path()).unwrap();
|
||||
assert!(config.is_sync());
|
||||
assert!(!config.is_async());
|
||||
assert_eq!(config.actor_id, "test_osis");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_async_config_rejection() {
|
||||
let config_toml = r#"
|
||||
actor_id = "test_osis"
|
||||
redis_url = "redis://localhost:6379"
|
||||
db_path = "/tmp/test_db"
|
||||
|
||||
[actor_type]
|
||||
type = "async"
|
||||
default_timeout_seconds = 300
|
||||
|
||||
[logging]
|
||||
level = "info"
|
||||
"#;
|
||||
|
||||
let mut temp_file = NamedTempFile::new().unwrap();
|
||||
temp_file.write_all(config_toml.as_bytes()).unwrap();
|
||||
|
||||
let config = WorkerConfig::from_file(temp_file.path()).unwrap();
|
||||
assert!(!config.is_sync());
|
||||
assert!(config.is_async());
|
||||
// This would be rejected in main() function
|
||||
}
|
||||
}
|
60
cmd/actor_osis.rs
Normal file
60
cmd/actor_osis.rs
Normal file
@ -0,0 +1,60 @@
|
||||
use actor_osis::OSISActor;
|
||||
use clap::Parser;
|
||||
use log::info;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(name = "actor_osis")]
|
||||
#[command(about = "OSIS Actor - Synchronous job processing actor")]
|
||||
struct Args {
|
||||
/// Database path
|
||||
#[arg(short, long, default_value = "/tmp/osis_db")]
|
||||
db_path: String,
|
||||
|
||||
/// Redis URL
|
||||
#[arg(short, long, default_value = "redis://localhost:6379")]
|
||||
redis_url: String,
|
||||
|
||||
/// Preserve completed tasks in Redis
|
||||
#[arg(short, long)]
|
||||
preserve_tasks: bool,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
env_logger::init();
|
||||
|
||||
let args = Args::parse();
|
||||
|
||||
info!("Starting OSIS Actor");
|
||||
|
||||
// Create shutdown channel
|
||||
let (shutdown_tx, shutdown_rx) = mpsc::channel(1);
|
||||
|
||||
// Setup signal handler for graceful shutdown
|
||||
let shutdown_tx_clone = shutdown_tx.clone();
|
||||
tokio::spawn(async move {
|
||||
tokio::signal::ctrl_c().await.expect("Failed to listen for Ctrl+C");
|
||||
info!("Received Ctrl+C, initiating shutdown...");
|
||||
let _ = shutdown_tx_clone.send(()).await;
|
||||
});
|
||||
|
||||
// Create and start the actor
|
||||
let actor = Arc::new(
|
||||
OSISActor::builder()
|
||||
.db_path(args.db_path)
|
||||
.redis_url(args.redis_url)
|
||||
.build()?
|
||||
);
|
||||
|
||||
let handle = baobab_actor::spawn_actor(actor, shutdown_rx);
|
||||
|
||||
info!("OSIS Actor started, waiting for jobs...");
|
||||
|
||||
// Wait for the actor to complete
|
||||
handle.await??;
|
||||
|
||||
info!("OSIS Actor shutdown complete");
|
||||
Ok(())
|
||||
}
|
174
examples/actor.rs
Normal file
174
examples/actor.rs
Normal file
@ -0,0 +1,174 @@
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use std::time::Duration;
|
||||
use tokio;
|
||||
use tokio::sync::mpsc;
|
||||
use tokio::time::{sleep, timeout};
|
||||
use redis::AsyncCommands;
|
||||
|
||||
use actor_osis::spawn_osis_actor;
|
||||
use hero_job::{Job, JobStatus, ScriptType};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Initialize logging
|
||||
env_logger::init();
|
||||
|
||||
println!("=== OSIS Actor Redis Dispatch Example ===");
|
||||
|
||||
// Find all Rhai scripts in examples/scripts directory
|
||||
let scripts_dir = Path::new("examples/scripts");
|
||||
if !scripts_dir.exists() {
|
||||
eprintln!("Scripts directory not found: {}", scripts_dir.display());
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut script_files = Vec::new();
|
||||
for entry in fs::read_dir(scripts_dir)? {
|
||||
let entry = entry?;
|
||||
let path = entry.path();
|
||||
if path.extension().and_then(|s| s.to_str()) == Some("rhai") {
|
||||
script_files.push(path);
|
||||
}
|
||||
}
|
||||
|
||||
script_files.sort();
|
||||
println!("Found {} Rhai scripts in {}", script_files.len(), scripts_dir.display());
|
||||
|
||||
if script_files.is_empty() {
|
||||
println!("No Rhai scripts found. Exiting.");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Create temporary database path
|
||||
let db_path = "temp_osis_actor_example_db";
|
||||
|
||||
// Clean up previous database if it exists
|
||||
if Path::new(db_path).exists() {
|
||||
fs::remove_dir_all(db_path)?;
|
||||
}
|
||||
|
||||
// Redis configuration
|
||||
let redis_url = "redis://127.0.0.1:6379";
|
||||
|
||||
// Try to connect to Redis
|
||||
let redis_client = redis::Client::open(redis_url)?;
|
||||
let mut redis_conn = match redis_client.get_multiplexed_async_connection().await {
|
||||
Ok(conn) => {
|
||||
println!("Connected to Redis at {}", redis_url);
|
||||
conn
|
||||
}
|
||||
Err(e) => {
|
||||
println!("Failed to connect to Redis: {}", e);
|
||||
println!("Please ensure Redis is running on localhost:6379");
|
||||
println!("You can start Redis with: redis-server");
|
||||
return Err(e.into());
|
||||
}
|
||||
};
|
||||
|
||||
// Create shutdown channel for the actor
|
||||
let (shutdown_tx, shutdown_rx) = mpsc::channel(1);
|
||||
|
||||
// Spawn the OSIS actor
|
||||
println!("\n--- Spawning OSIS Actor ---");
|
||||
let actor_handle = spawn_osis_actor(
|
||||
db_path.to_string(),
|
||||
redis_url.to_string(),
|
||||
shutdown_rx,
|
||||
);
|
||||
|
||||
println!("OSIS actor spawned and listening for jobs");
|
||||
|
||||
// Process each script
|
||||
for (i, script_path) in script_files.iter().enumerate() {
|
||||
println!("\n=== Processing Script {}/{}: {} ===", i + 1, script_files.len(), script_path.file_name().unwrap().to_string_lossy());
|
||||
|
||||
let script_content = match fs::read_to_string(script_path) {
|
||||
Ok(content) => content,
|
||||
Err(e) => {
|
||||
println!("Failed to read script {}: {}", script_path.display(), e);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
// Create a job for this script
|
||||
let job = Job::new(
|
||||
"example_caller".to_string(),
|
||||
format!("script_{}", script_path.file_stem().unwrap().to_string_lossy()),
|
||||
script_content,
|
||||
ScriptType::OSIS,
|
||||
);
|
||||
|
||||
println!("Created job with ID: {}", job.id);
|
||||
|
||||
// Store the job in Redis
|
||||
let job_key = format!("job:{}", job.id);
|
||||
job.store_in_redis(&mut redis_conn).await?;
|
||||
|
||||
// Set initial status to Dispatched (since store_in_redis sets it to "pending" which isn't in the enum)
|
||||
Job::update_status(&mut redis_conn, &job.id, JobStatus::Dispatched).await?;
|
||||
println!("Stored job in Redis with key: {} and status: Dispatched", job_key);
|
||||
|
||||
// Add the job to the OSIS queue for processing
|
||||
// Note: The supervisor uses "actor_queue:" prefix, so the correct queue is:
|
||||
let queue_key = "hero:job:actor_queue:osis";
|
||||
let _: () = redis_conn.lpush(&queue_key, &job.id).await?;
|
||||
println!("Dispatched job {} to OSIS queue: {}", job.id, queue_key);
|
||||
|
||||
println!("\n--- Waiting for Job Result ---");
|
||||
|
||||
// Wait for result or error from Redis queues with timeout
|
||||
let result_key = format!("hero:job:{}:result", job.id);
|
||||
let error_key = format!("hero:job:{}:error", job.id);
|
||||
let timeout_secs = 10.0;
|
||||
|
||||
// Use BLPOP to block and wait for either result or error
|
||||
let keys = vec![result_key.clone(), error_key.clone()];
|
||||
match redis_conn.blpop::<_, Option<(String, String)>>(&keys, timeout_secs).await {
|
||||
Ok(Some((queue_name, value))) => {
|
||||
if queue_name == result_key {
|
||||
println!("✓ Job completed successfully!");
|
||||
println!(" Result: {}", value);
|
||||
} else if queue_name == error_key {
|
||||
println!("✗ Job failed with error!");
|
||||
println!(" Error: {}", value);
|
||||
}
|
||||
}
|
||||
Ok(None) => {
|
||||
println!("⏱ Job timed out after {} seconds", timeout_secs);
|
||||
}
|
||||
Err(e) => {
|
||||
println!("❌ Failed to wait for job result: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Shutdown the actor
|
||||
println!("\n--- Shutting Down Actor ---");
|
||||
if let Err(e) = shutdown_tx.send(()).await {
|
||||
println!("Failed to send shutdown signal: {}", e);
|
||||
}
|
||||
|
||||
// Wait for actor to shutdown with timeout
|
||||
match timeout(Duration::from_secs(5), actor_handle).await {
|
||||
Ok(result) => {
|
||||
match result {
|
||||
Ok(Ok(())) => println!("OSIS actor shut down successfully"),
|
||||
Ok(Err(e)) => println!("OSIS actor shut down with error: {}", e),
|
||||
Err(e) => println!("OSIS actor panicked: {}", e),
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
println!("OSIS actor shutdown timed out");
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up the temporary database
|
||||
if Path::new(db_path).exists() {
|
||||
fs::remove_dir_all(db_path)?;
|
||||
println!("Cleaned up temporary database: {}", db_path);
|
||||
}
|
||||
|
||||
println!("=== Actor Example Complete ===");
|
||||
Ok(())
|
||||
}
|
174
examples/engine.rs
Normal file
174
examples/engine.rs
Normal file
@ -0,0 +1,174 @@
|
||||
use std::env;
|
||||
use std::fs;
|
||||
use std::panic;
|
||||
use std::path::Path;
|
||||
use rhai::{Engine, Dynamic};
|
||||
|
||||
use actor_osis::OSISActor;
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Parse command line arguments for verbosity
|
||||
let args: Vec<String> = env::args().collect();
|
||||
let verbose = args.contains(&"--verbose".to_string()) || args.contains(&"-v".to_string());
|
||||
|
||||
// Set up custom panic hook to suppress panic messages unless verbose
|
||||
if !verbose {
|
||||
panic::set_hook(Box::new(|_| {
|
||||
// Suppress panic output in non-verbose mode
|
||||
}));
|
||||
}
|
||||
|
||||
// Initialize logging only if verbose
|
||||
if verbose {
|
||||
env_logger::init();
|
||||
}
|
||||
|
||||
println!("=== OSIS Engine Direct Execution Example ===");
|
||||
|
||||
// Find all Rhai scripts in examples/scripts directory
|
||||
let scripts_dir = Path::new("examples/scripts");
|
||||
if !scripts_dir.exists() {
|
||||
eprintln!("Scripts directory not found: {}", scripts_dir.display());
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut script_files = Vec::new();
|
||||
for entry in fs::read_dir(scripts_dir)? {
|
||||
let entry = entry?;
|
||||
let path = entry.path();
|
||||
if path.extension().and_then(|s| s.to_str()) == Some("rhai") {
|
||||
script_files.push(path);
|
||||
}
|
||||
}
|
||||
|
||||
script_files.sort();
|
||||
|
||||
if verbose {
|
||||
println!("Found {} Rhai scripts in {}", script_files.len(), scripts_dir.display());
|
||||
} else {
|
||||
println!("Testing {} Rhai scripts:\n", script_files.len());
|
||||
}
|
||||
|
||||
// Create temporary database path
|
||||
let db_path = "temp_osis_engine_example_db";
|
||||
|
||||
// Clean up previous database if it exists
|
||||
if Path::new(db_path).exists() {
|
||||
fs::remove_dir_all(db_path)?;
|
||||
}
|
||||
|
||||
if verbose {
|
||||
println!("Created temporary database path: {}", db_path);
|
||||
}
|
||||
|
||||
// Track results for summary
|
||||
let mut success_count = 0;
|
||||
let mut failure_count = 0;
|
||||
|
||||
// Execute all scripts with colored output
|
||||
for (i, script_path) in script_files.iter().enumerate() {
|
||||
let script_name = script_path.file_name().unwrap().to_string_lossy();
|
||||
|
||||
// Read script content
|
||||
let script_content = match fs::read_to_string(script_path) {
|
||||
Ok(content) => content,
|
||||
Err(e) => {
|
||||
println!("\x1b[31m✗\x1b[0m {} ... \x1b[31mFAILED\x1b[0m (read error: {})", script_name, e);
|
||||
failure_count += 1;
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
if verbose {
|
||||
println!("\n=== Script {}/{}: {} ===", i + 1, script_files.len(), script_name);
|
||||
println!("--- Using Fresh OSIS Engine with Job Context ---");
|
||||
}
|
||||
|
||||
// Create a new engine instance and configure it with DSL modules
|
||||
let mut engine_with_context = match create_configured_engine(db_path, i + 1, verbose) {
|
||||
Ok(engine) => engine,
|
||||
Err(e) => {
|
||||
println!("\x1b[31m✗\x1b[0m {} ... \x1b[31mFAILED\x1b[0m (engine setup: {})", script_name, e);
|
||||
failure_count += 1;
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
// Execute the script with graceful error handling (catches both errors and panics)
|
||||
let script_result = panic::catch_unwind(panic::AssertUnwindSafe(|| {
|
||||
engine_with_context.eval::<rhai::Dynamic>(&script_content)
|
||||
}));
|
||||
|
||||
match script_result {
|
||||
Ok(Ok(result)) => {
|
||||
println!("\x1b[32m✓\x1b[0m {} ... \x1b[32mSUCCESS\x1b[0m", script_name);
|
||||
if verbose {
|
||||
println!(" Result: {:?}", result);
|
||||
}
|
||||
success_count += 1;
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
println!("\x1b[31m✗\x1b[0m {} ... \x1b[31mFAILED\x1b[0m", script_name);
|
||||
if verbose {
|
||||
println!(" Error: {}", e);
|
||||
}
|
||||
failure_count += 1;
|
||||
}
|
||||
Err(panic_err) => {
|
||||
let panic_msg = if let Some(s) = panic_err.downcast_ref::<String>() {
|
||||
s.clone()
|
||||
} else if let Some(s) = panic_err.downcast_ref::<&str>() {
|
||||
s.to_string()
|
||||
} else {
|
||||
"Unknown panic".to_string()
|
||||
};
|
||||
println!("\x1b[31m✗\x1b[0m {} ... \x1b[31mFAILED\x1b[0m", script_name);
|
||||
if verbose {
|
||||
println!(" Panic: {}", panic_msg);
|
||||
}
|
||||
failure_count += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Print summary
|
||||
println!("\n=== Summary ===");
|
||||
println!("\x1b[32m✓ {} scripts succeeded\x1b[0m", success_count);
|
||||
println!("\x1b[31m✗ {} scripts failed\x1b[0m", failure_count);
|
||||
println!("Total: {} scripts", success_count + failure_count);
|
||||
|
||||
// Clean up the temporary database
|
||||
if Path::new(db_path).exists() {
|
||||
fs::remove_dir_all(db_path)?;
|
||||
if verbose {
|
||||
println!("\nCleaned up temporary database: {}", db_path);
|
||||
}
|
||||
}
|
||||
|
||||
if verbose {
|
||||
println!("=== Engine Example Complete ===");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Create a configured Rhai engine with DSL modules and job context
|
||||
fn create_configured_engine(db_path: &str, script_index: usize, verbose: bool) -> Result<Engine, String> {
|
||||
// Create a new engine instance
|
||||
let mut engine = Engine::new();
|
||||
|
||||
// Register all DSL modules (same as OSIS engine configuration)
|
||||
actor_osis::register_dsl_modules(&mut engine);
|
||||
|
||||
// Set up job context tags (similar to execute_job_with_engine)
|
||||
let mut db_config = rhai::Map::new();
|
||||
db_config.insert("DB_PATH".into(), db_path.to_string().into());
|
||||
db_config.insert("CALLER_ID".into(), "engine_example".to_string().into());
|
||||
db_config.insert("CONTEXT_ID".into(), format!("script_{}", script_index).into());
|
||||
engine.set_default_tag(Dynamic::from(db_config));
|
||||
|
||||
if verbose {
|
||||
println!(" Set job context: DB_PATH={}, CALLER_ID=engine_example, CONTEXT_ID=script_{}", db_path, script_index);
|
||||
}
|
||||
|
||||
Ok(engine)
|
||||
}
|
15
examples/scripts/access.rhai
Normal file
15
examples/scripts/access.rhai
Normal file
@ -0,0 +1,15 @@
|
||||
// heromodels/examples/access/access.rhai
|
||||
|
||||
print("--- Testing Access Rhai Module ---");
|
||||
|
||||
// --- Image ---
|
||||
print("\n1. Creating and saving an access...");
|
||||
let new_access = new_access()
|
||||
.object_id(1)
|
||||
.circle_public_key("some_pk")
|
||||
.group_id(1)
|
||||
.contact_id(1)
|
||||
.expires_at(1735689600) // Future timestamp
|
||||
.save_access();
|
||||
|
||||
print("Access saved with ID: " + new_access.id);
|
8
examples/scripts/biz.rhai
Normal file
8
examples/scripts/biz.rhai
Normal file
@ -0,0 +1,8 @@
|
||||
// biz.rhai
|
||||
|
||||
// Example of using the company module
|
||||
let new_company = new_company()
|
||||
.name("HeroCode Inc.")
|
||||
.save_company();
|
||||
|
||||
print(new_company.name);
|
16
examples/scripts/calendar.rhai
Normal file
16
examples/scripts/calendar.rhai
Normal file
@ -0,0 +1,16 @@
|
||||
// calendar.rhai
|
||||
|
||||
let new_event = new_event()
|
||||
.title("Team Meeting")
|
||||
.description("Weekly sync-up")
|
||||
.location("Virtual")
|
||||
.add_attendee(new_attendee(1).status("Accepted"))
|
||||
.reschedule(1672531200, 1672534800) // Example timestamps
|
||||
.save_event();
|
||||
|
||||
let new_calendar = new_calendar("Work Calendar")
|
||||
.description("Calendar for all work-related events")
|
||||
.save_calendar();
|
||||
|
||||
print(new_calendar);
|
||||
print(new_event);
|
13
examples/scripts/circle.rhai
Normal file
13
examples/scripts/circle.rhai
Normal file
@ -0,0 +1,13 @@
|
||||
// circle.rhai
|
||||
|
||||
let new_circle = new_circle()
|
||||
.title("HeroCode Community")
|
||||
.ws_url("ws://herocode.com/community")
|
||||
.description("A circle for HeroCode developers.")
|
||||
.logo("logo.png")
|
||||
.theme_property("primaryColor", "#FF0000")
|
||||
.add_circle("General")
|
||||
.add_member("user123")
|
||||
.save_circle();
|
||||
|
||||
print(new_circle);
|
18
examples/scripts/company.rhai
Normal file
18
examples/scripts/company.rhai
Normal file
@ -0,0 +1,18 @@
|
||||
// company.rhai
|
||||
|
||||
let new_company = new_company()
|
||||
.name("HeroCode Solutions")
|
||||
.registration_number("HC12345")
|
||||
.incorporation_date(1609459200)
|
||||
.fiscal_year_end("31-12")
|
||||
.email("contact@herocode.com")
|
||||
.phone("123-456-7890")
|
||||
.website("www.herocode.com")
|
||||
.address("123 Hero Way, Codeville")
|
||||
.business_type("Global")
|
||||
.industry("Software Development")
|
||||
.description("Providing heroic coding solutions.")
|
||||
.status("Active")
|
||||
.save_company();
|
||||
|
||||
print(new_company);
|
21
examples/scripts/contact.rhai
Normal file
21
examples/scripts/contact.rhai
Normal file
@ -0,0 +1,21 @@
|
||||
// contact.rhai
|
||||
|
||||
let new_contact = new_contact()
|
||||
.name("John Doe")
|
||||
.description("A test contact")
|
||||
.address("123 Main St")
|
||||
.phone("555-1234")
|
||||
.email("john.doe@example.com")
|
||||
.notes("This is a note.")
|
||||
.circle("friends")
|
||||
.save_contact();
|
||||
|
||||
print(new_contact);
|
||||
|
||||
let new_group = new_group()
|
||||
.name("Test Group")
|
||||
.description("A group for testing")
|
||||
.add_contact(1)
|
||||
.save_group();
|
||||
|
||||
print(new_group);
|
9
examples/scripts/core.rhai
Normal file
9
examples/scripts/core.rhai
Normal file
@ -0,0 +1,9 @@
|
||||
// core.rhai
|
||||
|
||||
let new_comment = new_comment()
|
||||
.user_id(1)
|
||||
.content("This is a test comment.")
|
||||
.parent_comment_id(0)
|
||||
.save_comment();
|
||||
|
||||
print(new_comment);
|
58
examples/scripts/finance.rhai
Normal file
58
examples/scripts/finance.rhai
Normal file
@ -0,0 +1,58 @@
|
||||
// finance.rhai
|
||||
|
||||
// Account
|
||||
let new_account = new_account()
|
||||
.name("My Test Account")
|
||||
.user_id(1)
|
||||
.description("A test account for finance.")
|
||||
.ledger("main")
|
||||
.address("0x123...")
|
||||
.pubkey("0x456...")
|
||||
.add_asset(1)
|
||||
.save_account();
|
||||
|
||||
print("New Account:");
|
||||
print(new_account);
|
||||
|
||||
// Asset
|
||||
let new_asset = new_asset()
|
||||
.name("HeroCoin")
|
||||
.description("The official coin of HeroCode.")
|
||||
.amount(1000.0)
|
||||
.address("0xabc...")
|
||||
.decimals(18)
|
||||
.asset_type("erc20")
|
||||
.save_asset();
|
||||
|
||||
print("\nNew Asset:");
|
||||
print(new_asset);
|
||||
|
||||
// Listing
|
||||
let new_listing = new_listing()
|
||||
.title("100 HeroCoins for sale")
|
||||
.description("Get your HeroCoins now!")
|
||||
.asset_id("1")
|
||||
.seller_id("1")
|
||||
.price(1.5)
|
||||
.currency("USD")
|
||||
.asset_type("erc20")
|
||||
.listing_type("FixedPrice")
|
||||
.status("Active")
|
||||
.expires_at(1735689600) // Some future timestamp
|
||||
.image_url("http://example.com/herocoin.png")
|
||||
.add_tag("crypto")
|
||||
.save_listing();
|
||||
|
||||
print("\nNew Listing:");
|
||||
print(new_listing);
|
||||
|
||||
// Bid
|
||||
let new_bid = new_bid()
|
||||
.listing_id("1")
|
||||
.bidder_id(2)
|
||||
.amount(1.6)
|
||||
.currency("USD")
|
||||
.status("Active");
|
||||
|
||||
print("\nNew Bid:");
|
||||
print(new_bid);
|
32
examples/scripts/flow.rhai
Normal file
32
examples/scripts/flow.rhai
Normal file
@ -0,0 +1,32 @@
|
||||
// flow.rhai
|
||||
|
||||
// Create a signature requirement
|
||||
let sig_req = new_signature_requirement()
|
||||
.flow_step_id(1)
|
||||
.public_key("0xABCDEF1234567890")
|
||||
.message("Please sign to approve this step.")
|
||||
.status("Pending")
|
||||
.save_signature_requirement();
|
||||
|
||||
print("New Signature Requirement:");
|
||||
print(sig_req);
|
||||
|
||||
// Create a flow step
|
||||
let step1 = new_flow_step()
|
||||
.description("Initial approval by manager")
|
||||
.step_order(1)
|
||||
.status("Pending")
|
||||
.save_flow_step();
|
||||
|
||||
print("\nNew Flow Step:");
|
||||
print(step1);
|
||||
|
||||
// Create a flow and add the step
|
||||
let my_flow = new_flow("purchase-request-flow-123")
|
||||
.name("Purchase Request Approval Flow")
|
||||
.status("Active")
|
||||
.add_step(step1)
|
||||
.save_flow();
|
||||
|
||||
print("\nNew Flow:");
|
||||
print(my_flow);
|
12
examples/scripts/object.rhai
Normal file
12
examples/scripts/object.rhai
Normal file
@ -0,0 +1,12 @@
|
||||
// object.rhai
|
||||
|
||||
// Assuming a builder function `object__builder` exists based on the project's pattern.
|
||||
let new_object = object__builder(1)
|
||||
.name("My Dynamic Object")
|
||||
.description("An example of a generic object.")
|
||||
.set_property("custom_field", "custom_value")
|
||||
.build()
|
||||
.save_object();
|
||||
|
||||
print("New Object:");
|
||||
print(new_object);
|
190
examples/scripts/payment.rhai
Normal file
190
examples/scripts/payment.rhai
Normal file
@ -0,0 +1,190 @@
|
||||
// ===== Stripe Payment Integration Example =====
|
||||
// This script demonstrates the complete payment workflow using Stripe
|
||||
|
||||
print("🔧 Configuring Stripe...");
|
||||
// Configure Stripe with API key from environment variables
|
||||
// The STRIPE_API_KEY is loaded from .env file by main.rs
|
||||
let config_result = configure_stripe(STRIPE_API_KEY);
|
||||
print(`Configuration result: ${config_result}`);
|
||||
|
||||
print("\n📦 Creating a Product...");
|
||||
// Create a new product using builder pattern
|
||||
let product = new_product()
|
||||
.name("Premium Software License")
|
||||
.description("A comprehensive software solution for businesses")
|
||||
.metadata("category", "software")
|
||||
.metadata("tier", "premium");
|
||||
|
||||
print(`Product created: ${product.name}`);
|
||||
|
||||
// Create the product in Stripe (non-blocking)
|
||||
print("🔄 Dispatching product creation to Stripe...");
|
||||
try {
|
||||
let product_result = product.create_async("payment-example", "payment-context", STRIPE_API_KEY);
|
||||
print(`✅ Product creation dispatched: ${product_result}`);
|
||||
} catch(error) {
|
||||
print(`❌ Failed to dispatch product creation: ${error}`);
|
||||
print("This is expected with a demo API key. In production, use a valid Stripe secret key.");
|
||||
let product_id = "prod_demo_example_id"; // Continue with demo ID
|
||||
}
|
||||
|
||||
print("\n💰 Creating Prices...");
|
||||
|
||||
// Create upfront price (one-time payment)
|
||||
let upfront_price = new_price()
|
||||
.amount(19999) // $199.99 in cents
|
||||
.currency("usd")
|
||||
.product(product_id)
|
||||
.metadata("type", "upfront");
|
||||
|
||||
let upfront_result = upfront_price.create_async("payment-example", "payment-context", STRIPE_API_KEY);
|
||||
print(`✅ Upfront Price creation dispatched: ${upfront_result}`);
|
||||
let upfront_price_id = "price_demo_upfront_id";
|
||||
|
||||
// Create monthly subscription price
|
||||
let monthly_price = new_price()
|
||||
.amount(2999) // $29.99 in cents
|
||||
.currency("usd")
|
||||
.product(product_id)
|
||||
.recurring("month")
|
||||
.metadata("type", "monthly_subscription");
|
||||
|
||||
let monthly_result = monthly_price.create_async("payment-example", "payment-context", STRIPE_API_KEY);
|
||||
print(`✅ Monthly Price creation dispatched: ${monthly_result}`);
|
||||
let monthly_price_id = "price_demo_monthly_id";
|
||||
|
||||
// Create annual subscription price with discount
|
||||
let annual_price = new_price()
|
||||
.amount(29999) // $299.99 in cents (2 months free)
|
||||
.currency("usd")
|
||||
.product(product_id)
|
||||
.recurring("year")
|
||||
.metadata("type", "annual_subscription")
|
||||
.metadata("discount", "2_months_free");
|
||||
|
||||
let annual_result = annual_price.create_async("payment-example", "payment-context", STRIPE_API_KEY);
|
||||
print(`✅ Annual Price creation dispatched: ${annual_result}`);
|
||||
let annual_price_id = "price_demo_annual_id";
|
||||
|
||||
print("\n🎟️ Creating Discount Coupons...");
|
||||
|
||||
// Create a percentage-based coupon
|
||||
let percent_coupon = new_coupon()
|
||||
.duration("once")
|
||||
.percent_off(25)
|
||||
.metadata("campaign", "new_customer_discount")
|
||||
.metadata("code", "WELCOME25");
|
||||
|
||||
let percent_result = percent_coupon.create_async("payment-example", "payment-context", STRIPE_API_KEY);
|
||||
print(`✅ 25% Off Coupon creation dispatched: ${percent_result}`);
|
||||
let percent_coupon_id = "coupon_demo_25percent_id";
|
||||
|
||||
// Create a fixed amount coupon
|
||||
let amount_coupon = new_coupon()
|
||||
.duration("repeating")
|
||||
.duration_in_months(3)
|
||||
.amount_off(500, "usd") // $5.00 off
|
||||
.metadata("campaign", "loyalty_program")
|
||||
.metadata("code", "LOYAL5");
|
||||
|
||||
let amount_result = amount_coupon.create_async("payment-example", "payment-context", STRIPE_API_KEY);
|
||||
print(`✅ $5 Off Coupon creation dispatched: ${amount_result}`);
|
||||
let amount_coupon_id = "coupon_demo_5dollar_id";
|
||||
|
||||
print("\n💳 Creating Payment Intent for Upfront Payment...");
|
||||
|
||||
// Create a payment intent for one-time payment
|
||||
let payment_intent = new_payment_intent()
|
||||
.amount(19999)
|
||||
.currency("usd")
|
||||
.customer("cus_example_customer_id")
|
||||
.description("Premium Software License - One-time Payment")
|
||||
.add_payment_method_type("card")
|
||||
.add_payment_method_type("us_bank_account")
|
||||
.metadata("product_id", product_id)
|
||||
.metadata("price_id", upfront_price_id)
|
||||
.metadata("payment_type", "upfront");
|
||||
|
||||
let payment_result = payment_intent.create_async("payment-example", "payment-context", STRIPE_API_KEY);
|
||||
print(`✅ Payment Intent creation dispatched: ${payment_result}`);
|
||||
let payment_intent_id = "pi_demo_payment_intent_id";
|
||||
|
||||
print("\n🔄 Creating Subscription...");
|
||||
|
||||
// Create a subscription for monthly billing
|
||||
let subscription = new_subscription()
|
||||
.customer("cus_example_customer_id")
|
||||
.add_price(monthly_price_id)
|
||||
.trial_days(14) // 14-day free trial
|
||||
.coupon(percent_coupon_id) // Apply 25% discount
|
||||
.metadata("plan", "monthly")
|
||||
.metadata("trial", "14_days")
|
||||
.metadata("source", "website_signup");
|
||||
|
||||
let subscription_result = subscription.create_async("payment-example", "payment-context", STRIPE_API_KEY);
|
||||
print(`✅ Subscription creation dispatched: ${subscription_result}`);
|
||||
let subscription_id = "sub_demo_subscription_id";
|
||||
|
||||
print("\n🎯 Creating Multi-Item Subscription...");
|
||||
|
||||
// Create a subscription with multiple items
|
||||
let multi_subscription = new_subscription()
|
||||
.customer("cus_example_enterprise_customer")
|
||||
.add_price_with_quantity(monthly_price_id, 5) // 5 licenses
|
||||
.add_price("price_addon_support_monthly") // Support addon
|
||||
.trial_days(30) // 30-day trial for enterprise
|
||||
.metadata("plan", "enterprise")
|
||||
.metadata("licenses", "5")
|
||||
.metadata("addons", "premium_support");
|
||||
|
||||
let multi_result = multi_subscription.create_async("payment-example", "payment-context", STRIPE_API_KEY);
|
||||
print(`✅ Multi-Item Subscription creation dispatched: ${multi_result}`);
|
||||
let multi_subscription_id = "sub_demo_multi_subscription_id";
|
||||
|
||||
print("\n💰 Creating Payment Intent with Coupon...");
|
||||
|
||||
// Create another payment intent with discount applied
|
||||
let discounted_payment = new_payment_intent()
|
||||
.amount(14999) // Discounted amount after coupon
|
||||
.currency("usd")
|
||||
.customer("cus_example_customer_2")
|
||||
.description("Premium Software License - With 25% Discount")
|
||||
.metadata("original_amount", "19999")
|
||||
.metadata("coupon_applied", percent_coupon_id)
|
||||
.metadata("discount_percent", "25");
|
||||
|
||||
let discounted_result = discounted_payment.create_async("payment-example", "payment-context", STRIPE_API_KEY);
|
||||
print(`✅ Discounted Payment Intent creation dispatched: ${discounted_result}`);
|
||||
let discounted_payment_id = "pi_demo_discounted_payment_id";
|
||||
|
||||
print("\n📊 Summary of Created Items:");
|
||||
print("================================");
|
||||
print(`Product ID: ${product_id}`);
|
||||
print(`Upfront Price ID: ${upfront_price_id}`);
|
||||
print(`Monthly Price ID: ${monthly_price_id}`);
|
||||
print(`Annual Price ID: ${annual_price_id}`);
|
||||
print(`25% Coupon ID: ${percent_coupon_id}`);
|
||||
print(`$5 Coupon ID: ${amount_coupon_id}`);
|
||||
print(`Payment Intent ID: ${payment_intent_id}`);
|
||||
print(`Subscription ID: ${subscription_id}`);
|
||||
print(`Multi-Subscription ID: ${multi_subscription_id}`);
|
||||
print(`Discounted Payment ID: ${discounted_payment_id}`);
|
||||
|
||||
print("\n🎉 Payment workflow demonstration completed!");
|
||||
print("All Stripe object creation requests have been dispatched using the non-blocking pattern.");
|
||||
print("💡 In non-blocking mode:");
|
||||
print(" ✓ Functions return immediately with dispatch confirmations");
|
||||
print(" ✓ HTTP requests happen in background using tokio::spawn");
|
||||
print(" ✓ Results are handled by response/error scripts via RhaiDispatcher");
|
||||
print(" ✓ No thread blocking or waiting for API responses");
|
||||
|
||||
// Example of accessing object properties
|
||||
print("\n🔍 Accessing Object Properties:");
|
||||
print(`Product Name: ${product.name}`);
|
||||
print(`Product Description: ${product.description}`);
|
||||
print(`Upfront Price Amount: $${upfront_price.amount / 100}`);
|
||||
print(`Monthly Price Currency: ${monthly_price.currency}`);
|
||||
print(`Subscription Customer: ${subscription.customer}`);
|
||||
print(`Payment Intent Amount: $${payment_intent.amount / 100}`);
|
||||
print(`Percent Coupon Duration: ${percent_coupon.duration}`);
|
||||
print(`Percent Coupon Discount: ${percent_coupon.percent_off}%`);
|
27
examples/scripts/product.rhai
Normal file
27
examples/scripts/product.rhai
Normal file
@ -0,0 +1,27 @@
|
||||
// product.rhai
|
||||
|
||||
// Create a product component
|
||||
let component = new_product_component()
|
||||
.name("Sub-component A")
|
||||
.description("A vital part of the main product.")
|
||||
.quantity(10);
|
||||
|
||||
print("New Product Component:");
|
||||
print(component);
|
||||
|
||||
// Create a product and add the component
|
||||
let new_product = new_product()
|
||||
.name("Super Product")
|
||||
.description("A product that does amazing things.")
|
||||
.price(99.99)
|
||||
.type("Product")
|
||||
.category("Gadgets")
|
||||
.status("Available")
|
||||
.max_amount(100)
|
||||
.purchase_till(1735689600) // Future timestamp
|
||||
.active_till(1735689600) // Future timestamp
|
||||
.add_component(component)
|
||||
.save_product();
|
||||
|
||||
print("\nNew Product:");
|
||||
print(new_product);
|
28
examples/scripts/sale.rhai
Normal file
28
examples/scripts/sale.rhai
Normal file
@ -0,0 +1,28 @@
|
||||
// sale.rhai
|
||||
|
||||
// Create a sale item
|
||||
let sale_item = new_sale_item()
|
||||
.product_id(1)
|
||||
.name("Super Product")
|
||||
.quantity(2)
|
||||
.unit_price(99.99)
|
||||
.subtotal(199.98)
|
||||
.service_active_until(1735689600); // Future timestamp
|
||||
|
||||
print("New Sale Item:");
|
||||
print(sale_item);
|
||||
|
||||
// Create a sale and add the item
|
||||
let new_sale = new_sale()
|
||||
.company_id(1)
|
||||
.buyer_id(2)
|
||||
.transaction_id(12345)
|
||||
.total_amount(199.98)
|
||||
.status("Completed")
|
||||
.sale_date(1672531200) // Past timestamp
|
||||
.add_item(sale_item)
|
||||
.notes("This is a test sale.")
|
||||
.save_sale();
|
||||
|
||||
print("\nNew Sale:");
|
||||
print(new_sale);
|
15
examples/scripts/shareholder.rhai
Normal file
15
examples/scripts/shareholder.rhai
Normal file
@ -0,0 +1,15 @@
|
||||
// shareholder.rhai
|
||||
|
||||
// Create a shareholder
|
||||
let new_shareholder = new_shareholder()
|
||||
.company_id(1)
|
||||
.user_id(1)
|
||||
.name("John Doe")
|
||||
.shares(1000.0)
|
||||
.percentage(10.0)
|
||||
.type("Individual")
|
||||
.since(1640995200) // Timestamp for Jan 1, 2022
|
||||
.save_shareholder();
|
||||
|
||||
print("New Shareholder:");
|
||||
print(new_shareholder);
|
175
src/engine.rs
Normal file
175
src/engine.rs
Normal file
@ -0,0 +1,175 @@
|
||||
//! # Rhailib Domain-Specific Language (DSL) Engine
|
||||
//!
|
||||
//! This module provides a comprehensive Domain-Specific Language implementation for the Rhai
|
||||
//! scripting engine, exposing business domain models and operations through a fluent,
|
||||
//! chainable API.
|
||||
//!
|
||||
//! ## Overview
|
||||
//!
|
||||
//! The DSL is organized into business domain modules, each providing Rhai-compatible
|
||||
//! functions for creating, manipulating, and persisting domain entities. All operations
|
||||
//! include proper authorization checks and type safety.
|
||||
//!
|
||||
//! ## Available Domains
|
||||
//!
|
||||
//! - **Business Operations** (`biz`): Companies, products, sales, shareholders
|
||||
//! - **Financial Models** (`finance`): Accounts, assets, marketplace operations
|
||||
//! - **Content Management** (`library`): Collections, images, PDFs, books, slideshows
|
||||
//! - **Workflow Management** (`flow`): Flows, steps, signature requirements
|
||||
//! - **Community Management** (`circle`): Circles, themes, membership
|
||||
//! - **Contact Management** (`contact`): Contact information and relationships
|
||||
//! - **Access Control** (`access`): Security and permissions
|
||||
//! - **Time Management** (`calendar`): Calendar and scheduling
|
||||
//! - **Core Utilities** (`core`): Comments and fundamental operations
|
||||
//! - **Generic Objects** (`object`): Generic object manipulation
|
||||
//!
|
||||
//! ## Usage Example
|
||||
//!
|
||||
//! ```rust
|
||||
//! use rhai::Engine;
|
||||
//! use crate::engine::register_dsl_modules;
|
||||
//!
|
||||
//! let mut engine = Engine::new();
|
||||
//! register_dsl_modules(&mut engine);
|
||||
//!
|
||||
//! // Now the engine can execute scripts like:
|
||||
//! // let company = new_company().name("Acme Corp").email("contact@acme.com");
|
||||
//! // let saved = save_company(company);
|
||||
//! ```
|
||||
|
||||
use rhai::Engine;
|
||||
use rhailib_dsl;
|
||||
use std::sync::{Arc, OnceLock};
|
||||
|
||||
/// Engine factory for creating and sharing Rhai engines.
|
||||
pub struct EngineFactory {
|
||||
engine: Arc<Engine>,
|
||||
}
|
||||
|
||||
impl EngineFactory {
|
||||
/// Create a new engine factory with a configured Rhai engine.
|
||||
pub fn new() -> Self {
|
||||
let mut engine = Engine::new();
|
||||
register_dsl_modules(&mut engine);
|
||||
|
||||
Self {
|
||||
engine: Arc::new(engine),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a shared reference to the engine.
|
||||
pub fn get_engine(&self) -> Arc<Engine> {
|
||||
Arc::clone(&self.engine)
|
||||
}
|
||||
|
||||
/// Get the global singleton engine factory.
|
||||
pub fn global() -> &'static EngineFactory {
|
||||
static FACTORY: OnceLock<EngineFactory> = OnceLock::new();
|
||||
FACTORY.get_or_init(|| EngineFactory::new())
|
||||
}
|
||||
}
|
||||
|
||||
/// Register basic object functions directly in the engine.
|
||||
/// This provides object functionality without relying on the problematic rhailib_dsl object module.
|
||||
fn register_object_functions(engine: &mut Engine) {
|
||||
use heromodels::models::object::Object;
|
||||
|
||||
// Register the Object type
|
||||
engine.register_type_with_name::<Object>("Object");
|
||||
|
||||
// Register constructor function
|
||||
engine.register_fn("new_object", || Object::new());
|
||||
|
||||
// Register setter functions
|
||||
engine.register_fn("object_title", |obj: &mut Object, title: String| {
|
||||
obj.title = title;
|
||||
obj.clone()
|
||||
});
|
||||
|
||||
engine.register_fn("object_description", |obj: &mut Object, description: String| {
|
||||
obj.description = description;
|
||||
obj.clone()
|
||||
});
|
||||
|
||||
// Register getter functions
|
||||
engine.register_fn("get_object_id", |obj: &mut Object| obj.id() as i64);
|
||||
engine.register_fn("get_object_title", |obj: &mut Object| obj.title.clone());
|
||||
engine.register_fn("get_object_description", |obj: &mut Object| obj.description.clone());
|
||||
}
|
||||
|
||||
/// Registers all DSL modules with the provided Rhai engine.
|
||||
///
|
||||
/// This function is the main entry point for integrating the rhailib DSL with a Rhai engine.
|
||||
/// It registers all business domain modules, making their functions available to Rhai scripts.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `engine` - A mutable reference to the Rhai engine to register modules with
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// use rhai::Engine;
|
||||
/// use crate::engine::register_dsl_modules;
|
||||
///
|
||||
/// let mut engine = Engine::new();
|
||||
/// register_dsl_modules(&mut engine);
|
||||
///
|
||||
/// // Engine now has access to all DSL functions
|
||||
/// let result = engine.eval::<String>(r#"
|
||||
/// let company = new_company().name("Test Corp");
|
||||
/// company.name
|
||||
/// "#).unwrap();
|
||||
/// assert_eq!(result, "Test Corp");
|
||||
/// ```
|
||||
///
|
||||
/// # Registered Modules
|
||||
///
|
||||
/// This function registers the following domain modules:
|
||||
/// - Access control functions
|
||||
/// - Business operation functions (companies, products, sales, shareholders)
|
||||
/// - Calendar and scheduling functions
|
||||
/// - Circle and community management functions
|
||||
/// - Company management functions
|
||||
/// - Contact management functions
|
||||
/// - Core utility functions
|
||||
/// - Financial operation functions (accounts, assets, marketplace)
|
||||
/// - Workflow management functions (flows, steps, signatures)
|
||||
/// - Library and content management functions
|
||||
/// - Generic object manipulation functions (custom implementation)
|
||||
pub fn register_dsl_modules(engine: &mut Engine) {
|
||||
rhailib_dsl::access::register_access_rhai_module(engine);
|
||||
rhailib_dsl::biz::register_biz_rhai_module(engine);
|
||||
rhailib_dsl::calendar::register_calendar_rhai_module(engine);
|
||||
rhailib_dsl::circle::register_circle_rhai_module(engine);
|
||||
rhailib_dsl::company::register_company_rhai_module(engine);
|
||||
rhailib_dsl::contact::register_contact_rhai_module(engine);
|
||||
rhailib_dsl::core::register_core_rhai_module(engine);
|
||||
rhailib_dsl::finance::register_finance_rhai_modules(engine);
|
||||
// rhailib_dsl::flow::register_flow_rhai_modules(engine);
|
||||
rhailib_dsl::library::register_library_rhai_module(engine);
|
||||
// Skip problematic object module for now - can be implemented separately if needed
|
||||
// rhailib_dsl::object::register_object_fns(engine);
|
||||
rhailib_dsl::payment::register_payment_rhai_module(engine);
|
||||
|
||||
// Register basic object functionality directly
|
||||
register_object_functions(engine);
|
||||
|
||||
println!("Rhailib Domain Specific Language modules registered successfully.");
|
||||
}
|
||||
|
||||
|
||||
/// Create a shared heromodels engine using the factory.
|
||||
pub fn create_osis_engine() -> Arc<Engine> {
|
||||
EngineFactory::global().get_engine()
|
||||
}
|
||||
|
||||
/// Evaluate a Rhai script string.
|
||||
pub fn eval_script(
|
||||
engine: &Engine,
|
||||
script: &str,
|
||||
) -> Result<rhai::Dynamic, Box<rhai::EvalAltResult>> {
|
||||
engine.eval(script)
|
||||
}
|
||||
|
||||
|
249
src/lib.rs
Normal file
249
src/lib.rs
Normal file
@ -0,0 +1,249 @@
|
||||
mod engine;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use baobab_actor::execute_job_with_engine;
|
||||
use hero_job::{Job, JobStatus, ScriptType};
|
||||
use log::{error, info};
|
||||
use redis::AsyncCommands;
|
||||
use rhai::Engine;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::mpsc;
|
||||
use tokio::task::JoinHandle;
|
||||
|
||||
|
||||
use baobab_actor::{actor_trait::Actor, spawn_actor};
|
||||
|
||||
/// Constant actor ID for OSIS actor
|
||||
const OSIS: &str = "osis";
|
||||
|
||||
/// Builder for OSISActor
|
||||
#[derive(Debug)]
|
||||
pub struct OSISActorBuilder {
|
||||
engine: Option<Arc<Engine>>,
|
||||
db_path: Option<String>,
|
||||
redis_url: Option<String>,
|
||||
}
|
||||
|
||||
impl Default for OSISActorBuilder {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
engine: None,
|
||||
db_path: None,
|
||||
redis_url: Some("redis://localhost:6379".to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl OSISActorBuilder {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub fn engine(mut self, engine: Engine) -> Self {
|
||||
self.engine = Some(Arc::new(engine));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn shared_engine(mut self, engine: Arc<Engine>) -> Self {
|
||||
self.engine = Some(engine);
|
||||
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 build(self) -> Result<OSISActor, String> {
|
||||
let engine = self.engine.unwrap_or_else(|| crate::engine::create_osis_engine());
|
||||
|
||||
Ok(OSISActor {
|
||||
engine,
|
||||
db_path: self.db_path.ok_or("db_path is required")?,
|
||||
redis_url: self.redis_url.unwrap_or("redis://localhost:6379".to_string()),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// OSIS actor that processes jobs in a blocking, synchronized manner
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct OSISActor {
|
||||
pub engine: Arc<Engine>,
|
||||
pub db_path: String,
|
||||
pub redis_url: String,
|
||||
}
|
||||
|
||||
impl OSISActor {
|
||||
/// Create a new OSISActorBuilder
|
||||
pub fn builder() -> OSISActorBuilder {
|
||||
OSISActorBuilder::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for OSISActor {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
engine: crate::engine::create_osis_engine(),
|
||||
db_path: "/tmp".to_string(),
|
||||
redis_url: "redis://localhost:6379".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl Actor for OSISActor {
|
||||
async fn process_job(
|
||||
&self,
|
||||
job: Job,
|
||||
redis_conn: &mut redis::aio::MultiplexedConnection,
|
||||
) {
|
||||
let job_id = &job.id;
|
||||
let _db_path = &self.db_path;
|
||||
|
||||
info!("OSIS Actor '{}', Job {}: Starting sequential processing", OSIS, job_id);
|
||||
|
||||
// Update job status to Started
|
||||
if let Err(e) = Job::update_status(redis_conn, job_id, JobStatus::Started).await {
|
||||
error!("OSIS Actor '{}', Job {}: Failed to update status to Started: {}",
|
||||
OSIS, job_id, e);
|
||||
return;
|
||||
}
|
||||
|
||||
// Execute the Rhai script with proper job context
|
||||
// Note: We create a fresh engine instance for each job to avoid state conflicts
|
||||
let mut job_engine = Engine::new();
|
||||
register_dsl_modules(&mut job_engine);
|
||||
match execute_job_with_engine(&mut job_engine, &job, &self.db_path).await {
|
||||
Ok(result) => {
|
||||
let result_str = format!("{:?}", result);
|
||||
info!("OSIS Actor '{}', Job {}: Script executed successfully. Result: {}",
|
||||
OSIS, job_id, result_str);
|
||||
|
||||
// Update job with success result (stores in job hash output field)
|
||||
if let Err(e) = Job::set_result(redis_conn, job_id, &result_str).await {
|
||||
error!("OSIS Actor '{}', Job {}: Failed to set result: {}",
|
||||
OSIS, job_id, e);
|
||||
return;
|
||||
}
|
||||
|
||||
// Also push result to result queue for retrieval
|
||||
let result_queue_key = format!("hero:job:{}:result", job_id);
|
||||
if let Err(e) = redis_conn.lpush::<_, _, ()>(&result_queue_key, &result_str).await {
|
||||
error!("OSIS Actor '{}', Job {}: Failed to push result to queue {}: {}",
|
||||
OSIS, job_id, result_queue_key, e);
|
||||
} else {
|
||||
info!("OSIS Actor '{}', Job {}: Result pushed to queue: {}",
|
||||
OSIS, job_id, result_queue_key);
|
||||
}
|
||||
|
||||
if let Err(e) = Job::update_status(redis_conn, job_id, JobStatus::Finished).await {
|
||||
error!("OSIS Actor '{}', Job {}: Failed to update status to Finished: {}",
|
||||
OSIS, job_id, e);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
let error_msg = format!("Script execution error: {}", e);
|
||||
error!("OSIS Actor '{}', Job {}: {}", OSIS, job_id, error_msg);
|
||||
|
||||
// Update job with error (stores in job hash error field)
|
||||
if let Err(e) = Job::set_error(redis_conn, job_id, &error_msg).await {
|
||||
error!("OSIS Actor '{}', Job {}: Failed to set error: {}",
|
||||
OSIS, job_id, e);
|
||||
}
|
||||
|
||||
// Also push error to error queue for retrieval
|
||||
let error_queue_key = format!("hero:job:{}:error", job_id);
|
||||
if let Err(e) = redis_conn.lpush::<_, _, ()>(&error_queue_key, &error_msg).await {
|
||||
error!("OSIS Actor '{}', Job {}: Failed to push error to queue {}: {}",
|
||||
OSIS, job_id, error_queue_key, e);
|
||||
} else {
|
||||
info!("OSIS Actor '{}', Job {}: Error pushed to queue: {}",
|
||||
OSIS, job_id, error_queue_key);
|
||||
}
|
||||
|
||||
if let Err(e) = Job::update_status(redis_conn, job_id, JobStatus::Error).await {
|
||||
error!("OSIS Actor '{}', Job {}: Failed to update status to Error: {}",
|
||||
OSIS, job_id, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
info!("OSIS Actor '{}', Job {}: Sequential processing completed", OSIS, job_id);
|
||||
}
|
||||
|
||||
fn actor_type(&self) -> &'static str {
|
||||
"OSIS"
|
||||
}
|
||||
|
||||
fn actor_id(&self) -> &str {
|
||||
// Use actor_queue:osis to match supervisor's dispatch queue naming
|
||||
"actor_queue:osis"
|
||||
}
|
||||
|
||||
fn redis_url(&self) -> &str {
|
||||
&self.redis_url
|
||||
}
|
||||
}
|
||||
|
||||
/// Convenience function to spawn an OSIS actor using the trait interface
|
||||
///
|
||||
/// This function provides backward compatibility with the original actor API
|
||||
/// while using the new trait-based implementation.
|
||||
pub fn spawn_osis_actor(
|
||||
db_path: String,
|
||||
redis_url: String,
|
||||
shutdown_rx: mpsc::Receiver<()>,
|
||||
) -> JoinHandle<Result<(), Box<dyn std::error::Error + Send + Sync>>> {
|
||||
let actor = Arc::new(
|
||||
OSISActor::builder()
|
||||
.db_path(db_path)
|
||||
.redis_url(redis_url)
|
||||
.build()
|
||||
.expect("Failed to build OSISActor")
|
||||
);
|
||||
spawn_actor(actor, shutdown_rx)
|
||||
}
|
||||
|
||||
// Re-export engine functions for examples and external use
|
||||
pub use crate::engine::{create_osis_engine, register_dsl_modules};
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_osis_actor_creation() {
|
||||
let actor = OSISActor::builder().build().unwrap();
|
||||
assert_eq!(actor.actor_type(), "OSIS");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_osis_actor_default() {
|
||||
let actor = OSISActor::default();
|
||||
assert_eq!(actor.actor_type(), "OSIS");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_osis_actor_process_job_interface() {
|
||||
let actor = OSISActor::default();
|
||||
|
||||
// Create a simple test job
|
||||
let _job = Job::new(
|
||||
"test_caller".to_string(),
|
||||
"test_context".to_string(),
|
||||
r#"print("Hello from sync actor test!"); 42"#.to_string(),
|
||||
ScriptType::OSIS,
|
||||
);
|
||||
|
||||
// Note: This test doesn't actually connect to Redis, it just tests the interface
|
||||
// In a real test environment, you'd need a Redis instance or mock
|
||||
|
||||
// For now, just verify the actor was created successfully
|
||||
assert_eq!(actor.actor_type(), "OSIS");
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user