implement osis actor
This commit is contained in:
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");
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user