add rhailib package
This commit is contained in:
parent
635af9ecee
commit
79b37cf9ce
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
target
|
1635
Cargo.lock
generated
Normal file
1635
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
17
Cargo.toml
Normal file
17
Cargo.toml
Normal file
@ -0,0 +1,17 @@
|
||||
[package]
|
||||
name = "rhailib"
|
||||
version = "0.1.0"
|
||||
edition = "2021" # Changed to 2021 for consistency with other crates
|
||||
|
||||
[dependencies]
|
||||
# Dependencies for rhailib itself would go here
|
||||
|
||||
[workspace]
|
||||
members = [
|
||||
".", # Represents the root package (rhailib)
|
||||
"src/client",
|
||||
"src/engine",
|
||||
"src/worker",
|
||||
"examples",
|
||||
]
|
||||
resolver = "2" # Recommended for new workspaces
|
26
examples/Cargo.toml
Normal file
26
examples/Cargo.toml
Normal file
@ -0,0 +1,26 @@
|
||||
[package]
|
||||
name = "rhailib-examples"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false # This is a package of examples, not meant to be published
|
||||
|
||||
[dependencies]
|
||||
# Local Rhailib crates
|
||||
# Allows 'use rhai_client::...'
|
||||
rhai_client = { path = "../src/client" }
|
||||
# Allows 'use worker_lib::...'
|
||||
worker_lib = { path = "../src/worker", package = "worker" }
|
||||
|
||||
# External dependencies (versions aligned with other crates)
|
||||
rhai = { version = "1.18.0", features = ["sync", "decimal"] }
|
||||
tokio = { version = "1", features = ["macros", "rt-multi-thread", "time"] }
|
||||
log = "0.4"
|
||||
env_logger = "0.10"
|
||||
|
||||
[[bin]]
|
||||
name = "example_math_worker"
|
||||
path = "example_math_worker.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "example_string_worker"
|
||||
path = "example_string_worker.rs"
|
10
examples/README.md
Normal file
10
examples/README.md
Normal file
@ -0,0 +1,10 @@
|
||||
# Rhailib Examples
|
||||
|
||||
This directory contains end-to-end examples demonstrating the usage of the `rhailib` project, showcasing how the `client`, `engine`, and `worker` crates interact.
|
||||
|
||||
## Available Examples
|
||||
|
||||
- **`example_math_worker.rs`**: This example demonstrates a worker that performs mathematical operations. It shows how to define Rhai scripts that call Rust functions exposed by the worker, process the results, and interact with the `rhailib` engine and client.
|
||||
- **`example_string_worker.rs`**: This example showcases a worker focused on string manipulations. Similar to the math worker, it illustrates the setup for defining Rhai scripts, registering Rust functions for string operations, and the overall flow of execution within the `rhailib` ecosystem.
|
||||
|
||||
These examples serve as a practical guide to understanding the core functionalities and integration patterns within the `rhailib` project.
|
76
examples/example_math_worker.rs
Normal file
76
examples/example_math_worker.rs
Normal file
@ -0,0 +1,76 @@
|
||||
use rhai::Engine;
|
||||
use rhai_client::RhaiClient; // To submit tasks
|
||||
use worker_lib::{run_worker_loop, Args as WorkerArgs}; // To run the worker
|
||||
use std::time::Duration;
|
||||
use tokio::time::sleep;
|
||||
|
||||
// Custom function for Rhai
|
||||
fn add(a: i64, b: i64) -> i64 {
|
||||
a + b
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
env_logger::init();
|
||||
log::info!("Starting Math Worker Example...");
|
||||
|
||||
// 1. Configure and start the Rhai Worker with a custom engine
|
||||
let mut math_engine = Engine::new();
|
||||
math_engine.register_fn("add", add);
|
||||
log::info!("Custom 'add' function registered with Rhai engine for Math Worker.");
|
||||
|
||||
let worker_args = WorkerArgs {
|
||||
redis_url: "redis://127.0.0.1/".to_string(),
|
||||
circles: vec!["math_circle".to_string()], // Worker listens on a specific queue
|
||||
};
|
||||
let worker_args_clone = worker_args.clone(); // Clone for the worker task
|
||||
|
||||
tokio::spawn(async move {
|
||||
log::info!("Math Worker task starting...");
|
||||
if let Err(e) = run_worker_loop(math_engine, worker_args_clone).await {
|
||||
log::error!("Math Worker loop failed: {}", e);
|
||||
}
|
||||
});
|
||||
|
||||
// Give the worker a moment to start and connect
|
||||
sleep(Duration::from_secs(1)).await;
|
||||
|
||||
// 2. Use RhaiClient to submit a script to the "math_circle"
|
||||
let client = RhaiClient::new("redis://127.0.0.1/")?;
|
||||
let script_content = r#"
|
||||
let x = 10;
|
||||
let y = add(x, 32); // Use the custom registered function
|
||||
print("Math script: 10 + 32 = " + y);
|
||||
y // Return the result
|
||||
"#;
|
||||
|
||||
log::info!("Submitting math script to 'math_circle' and awaiting result...");
|
||||
|
||||
let timeout_duration = Duration::from_secs(10);
|
||||
let poll_interval = Duration::from_millis(500);
|
||||
|
||||
match client.submit_script_and_await_result(
|
||||
"math_circle",
|
||||
script_content.to_string(),
|
||||
None,
|
||||
timeout_duration,
|
||||
poll_interval
|
||||
).await {
|
||||
Ok(details) => {
|
||||
log::info!("Math Worker Example: Task finished. Status: {}, Output: {:?}, Error: {:?}",
|
||||
details.status, details.output, details.error);
|
||||
if details.status == "completed" {
|
||||
assert_eq!(details.output, Some("42".to_string()));
|
||||
log::info!("Math Worker Example: Assertion for output 42 passed!");
|
||||
Ok(())
|
||||
} else {
|
||||
log::error!("Math Worker Example: Task completed with error: {:?}", details.error);
|
||||
Err(format!("Task failed with error: {:?}", details.error).into())
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Math Worker Example: Failed to get task result: {}", e);
|
||||
Err(e.into())
|
||||
}
|
||||
}
|
||||
}
|
76
examples/example_string_worker.rs
Normal file
76
examples/example_string_worker.rs
Normal file
@ -0,0 +1,76 @@
|
||||
use rhai::Engine;
|
||||
use rhai_client::RhaiClient; // To submit tasks
|
||||
use worker_lib::{run_worker_loop, Args as WorkerArgs}; // To run the worker
|
||||
use std::time::Duration;
|
||||
use tokio::time::sleep;
|
||||
|
||||
// Custom function for Rhai
|
||||
fn reverse_string(s: String) -> String {
|
||||
s.chars().rev().collect()
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
env_logger::init();
|
||||
log::info!("Starting String Worker Example...");
|
||||
|
||||
// 1. Configure and start the Rhai Worker with a custom engine
|
||||
let mut string_engine = Engine::new();
|
||||
string_engine.register_fn("reverse_it", reverse_string);
|
||||
log::info!("Custom 'reverse_it' function registered with Rhai engine for String Worker.");
|
||||
|
||||
let worker_args = WorkerArgs {
|
||||
redis_url: "redis://127.0.0.1/".to_string(),
|
||||
circles: vec!["string_circle".to_string()], // Worker listens on a specific queue
|
||||
};
|
||||
let worker_args_clone = worker_args.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
log::info!("String Worker task starting...");
|
||||
if let Err(e) = run_worker_loop(string_engine, worker_args_clone).await {
|
||||
log::error!("String Worker loop failed: {}", e);
|
||||
}
|
||||
});
|
||||
|
||||
// Give the worker a moment to start and connect
|
||||
sleep(Duration::from_secs(1)).await;
|
||||
|
||||
// 2. Use RhaiClient to submit a script to the "string_circle"
|
||||
let client = RhaiClient::new("redis://127.0.0.1/")?;
|
||||
let script_content = r#"
|
||||
let original = "hello world";
|
||||
let reversed = reverse_it(original);
|
||||
print("String script: original = '" + original + "', reversed = '" + reversed + "'");
|
||||
reversed // Return the result
|
||||
"#;
|
||||
|
||||
log::info!("Submitting string script to 'string_circle' and awaiting result...");
|
||||
|
||||
let timeout_duration = Duration::from_secs(10);
|
||||
let poll_interval = Duration::from_millis(500);
|
||||
|
||||
match client.submit_script_and_await_result(
|
||||
"string_circle",
|
||||
script_content.to_string(),
|
||||
None,
|
||||
timeout_duration,
|
||||
poll_interval
|
||||
).await {
|
||||
Ok(details) => {
|
||||
log::info!("String Worker Example: Task finished. Status: {}, Output: {:?}, Error: {:?}",
|
||||
details.status, details.output, details.error);
|
||||
if details.status == "completed" {
|
||||
assert_eq!(details.output, Some("\"dlrow olleh\"".to_string())); // Rhai strings include quotes in `debug` format
|
||||
log::info!("String Worker Example: Assertion for output \"dlrow olleh\" passed!");
|
||||
Ok(())
|
||||
} else {
|
||||
log::error!("String Worker Example: Task completed with error: {:?}", details.error);
|
||||
Err(format!("Task failed with error: {:?}", details.error).into())
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("String Worker Example: Failed to get task result: {}", e);
|
||||
Err(e.into())
|
||||
}
|
||||
}
|
||||
}
|
1
src/client/.gitignore
vendored
Normal file
1
src/client/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
/target
|
17
src/client/Cargo.toml
Normal file
17
src/client/Cargo.toml
Normal file
@ -0,0 +1,17 @@
|
||||
[package]
|
||||
name = "rhai_client"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
redis = { version = "0.25.0", features = ["tokio-comp"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
uuid = { version = "1.6", features = ["v4", "serde"] }
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
log = "0.4"
|
||||
tokio = { version = "1", features = ["macros", "rt-multi-thread"] } # For async main in examples, and general async
|
||||
|
||||
[dev-dependencies] # For examples later
|
||||
env_logger = "0.10"
|
||||
rhai = "1.18.0" # For examples that might need to show engine setup
|
121
src/client/README.md
Normal file
121
src/client/README.md
Normal file
@ -0,0 +1,121 @@
|
||||
# Rhai Client
|
||||
|
||||
The `rhai_client` crate provides a client interface for submitting Rhai scripts to a distributed task execution system that uses Redis as a message broker and task store. It allows applications to offload Rhai script execution to one or more worker services.
|
||||
|
||||
## Features
|
||||
|
||||
- **Script Submission**: Submit Rhai scripts for asynchronous execution.
|
||||
- **Redis Integration**: Uses Redis lists as task queues and Redis hashes for storing task details (status, input, output, errors).
|
||||
- **Asynchronous Operations**: Built with `tokio` for non-blocking operations.
|
||||
- **Result Polling**:
|
||||
- Submit a script and get a `task_id` back immediately.
|
||||
- Poll for task status and results using the `task_id`.
|
||||
- Optionally, submit a script and await its completion (or error/timeout) with configurable timeout and polling intervals.
|
||||
- **Circle-based Task Routing**: Scripts are submitted to named "circles," allowing for different worker pools or configurations.
|
||||
|
||||
## Core Components
|
||||
|
||||
- **`RhaiClient`**: The main struct for interacting with the Rhai task system. It's initialized with a Redis connection URL.
|
||||
- `new(redis_url: &str)`: Creates a new client.
|
||||
- `submit_script(...)`: Submits a script and returns a `task_id`.
|
||||
- `get_task_status(task_id: &str)`: Retrieves the current status and details of a task.
|
||||
- `submit_script_and_await_result(...)`: Submits a script and polls until it completes, errors out, or the specified timeout is reached.
|
||||
- **`RhaiTaskDetails`**: A struct representing the details of a task, including its script, status (`pending`, `processing`, `completed`, `error`), output, error messages, and timestamps.
|
||||
- **`RhaiClientError`**: An enum for various errors that can occur, such as Redis errors, serialization issues, or task timeouts.
|
||||
|
||||
## How It Works
|
||||
|
||||
1. The `RhaiClient` is initialized with the Redis server URL.
|
||||
2. When a script is submitted via `submit_script` or `submit_script_and_await_result`:
|
||||
a. A unique `task_id` (UUID v4) is generated.
|
||||
b. `RhaiTaskDetails` are created with the script, initial status set to "pending", and other relevant metadata.
|
||||
c. These details are stored in a Redis hash with a key like `rhai_task_details:<task_id>`.
|
||||
d. The `task_id` is pushed onto a Redis list named `rhai_tasks:<circle_name>`, which acts as a queue for workers listening to that specific circle.
|
||||
3. Workers (not part of this client crate) would pop `task_id`s from their respective circle queues, retrieve task details from Redis, execute the script, and update the task details (status, output/error) in Redis.
|
||||
4. The `RhaiClient` can then use `get_task_status` to poll the Redis hash for updates or `submit_script_and_await_result` to automate this polling.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- A running Redis instance accessible by the client and the worker services.
|
||||
|
||||
## Usage Example
|
||||
|
||||
The following example demonstrates submitting a script and waiting for its result with a timeout. (This is a conceptual adaptation; see `examples/timeout_example.rs` for a runnable example focused on timeout behavior).
|
||||
|
||||
```rust
|
||||
use rhai_client::RhaiClient;
|
||||
use std::time::Duration;
|
||||
use serde_json::json; // For client_rpc_id example
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
env_logger::init(); // Optional: for logging
|
||||
|
||||
// Initialize the client
|
||||
let client = RhaiClient::new("redis://127.0.0.1/")?;
|
||||
log::info!("RhaiClient created.");
|
||||
|
||||
let script_content = r#"
|
||||
fn add(a, b) { a + b }
|
||||
add(10, 32)
|
||||
"#;
|
||||
let circle_name = "general_compute";
|
||||
let timeout = Duration::from_secs(10);
|
||||
let poll_interval = Duration::from_millis(500);
|
||||
|
||||
// Optional client-side RPC ID to associate with the task
|
||||
let client_rpc_id = Some(json!({ "request_id": "user_request_abc123" }));
|
||||
|
||||
log::info!("Submitting script to circle '{}' and awaiting result...", circle_name);
|
||||
|
||||
match client
|
||||
.submit_script_and_await_result(
|
||||
circle_name,
|
||||
script_content.to_string(),
|
||||
client_rpc_id,
|
||||
timeout,
|
||||
poll_interval,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(details) => {
|
||||
log::info!("Task completed successfully!");
|
||||
log::info!("Task ID: {}", details.script); // Note: This should likely be a dedicated task_id field if needed from details
|
||||
log::info!("Status: {}", details.status);
|
||||
if let Some(output) = details.output {
|
||||
log::info!("Output: {}", output);
|
||||
}
|
||||
if let Some(error_msg) = details.error {
|
||||
log::error!("Error: {}", error_msg);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("An error occurred: {}", e);
|
||||
match e {
|
||||
rhai_client::RhaiClientError::Timeout(task_id) => {
|
||||
log::warn!("Task {} timed out.", task_id);
|
||||
}
|
||||
rhai_client::RhaiClientError::TaskNotFound(task_id) => {
|
||||
log::error!("Task {} was not found after submission.", task_id);
|
||||
}
|
||||
_ => { // Handle other errors like RedisError, SerializationError
|
||||
log::error!("Unhandled client error: {:?}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
Refer to the `examples/` directory for more specific use cases, such as `timeout_example.rs` which tests the timeout mechanism.
|
||||
|
||||
## Building and Running Examples
|
||||
|
||||
To run an example (e.g., `timeout_example`):
|
||||
|
||||
```bash
|
||||
cd src/client # (or wherever this client's Cargo.toml is)
|
||||
cargo run --example timeout_example
|
||||
```
|
||||
Ensure a Redis server is running and accessible at `redis://127.0.0.1/`.
|
70
src/client/examples/timeout_example.rs
Normal file
70
src/client/examples/timeout_example.rs
Normal file
@ -0,0 +1,70 @@
|
||||
use rhai_client::{RhaiClient, RhaiTaskDetails}; // Assuming RhaiTaskDetails might be part of the success path, though we expect error
|
||||
use std::time::Duration;
|
||||
use log::info;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
env_logger::builder().filter_level(log::LevelFilter::Info).init();
|
||||
|
||||
let client = RhaiClient::new("redis://127.0.0.1/")?;
|
||||
info!("RhaiClient created.");
|
||||
|
||||
let script_content = r#"
|
||||
let x = 10;
|
||||
let y = x + 32;
|
||||
y // This script will never be executed by a worker
|
||||
"#;
|
||||
|
||||
let non_existent_circle = "non_existent_circle_for_timeout_test_12345";
|
||||
let very_short_timeout = Duration::from_secs(2);
|
||||
let poll_interval = Duration::from_millis(100);
|
||||
|
||||
info!(
|
||||
"Submitting script to non-existent circle '{}' with a timeout of {:?}...",
|
||||
non_existent_circle, very_short_timeout
|
||||
);
|
||||
|
||||
let start_time = std::time::Instant::now();
|
||||
|
||||
match client
|
||||
.submit_script_and_await_result(
|
||||
non_existent_circle,
|
||||
script_content.to_string(),
|
||||
None, // No specific task_id
|
||||
very_short_timeout,
|
||||
poll_interval,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(details) => {
|
||||
log::error!(
|
||||
"Timeout Example FAILED: Expected a timeout, but got Ok: {:?}",
|
||||
details
|
||||
);
|
||||
Err("Expected timeout, but task completed successfully.".into())
|
||||
}
|
||||
Err(e) => {
|
||||
let elapsed = start_time.elapsed();
|
||||
info!("Timeout Example: Received error as expected: {}", e);
|
||||
info!("Elapsed time: {:?}", elapsed);
|
||||
|
||||
match e {
|
||||
rhai_client::RhaiClientError::Timeout(task_id) => {
|
||||
info!("Timeout Example PASSED: Correctly received RhaiClientError::Timeout for task_id: {}", task_id);
|
||||
// Ensure the elapsed time is close to the timeout duration
|
||||
// Allow for some buffer for processing
|
||||
assert!(elapsed >= very_short_timeout && elapsed < very_short_timeout + Duration::from_secs(1), "Elapsed time {:?} should be close to timeout {:?}", elapsed, very_short_timeout);
|
||||
info!("Elapsed time {:?} is consistent with timeout duration {:?}.", elapsed, very_short_timeout);
|
||||
Ok(())
|
||||
}
|
||||
other_error => {
|
||||
log::error!(
|
||||
"Timeout Example FAILED: Expected RhaiClientError::Timeout, but got other error: {:?}",
|
||||
other_error
|
||||
);
|
||||
Err(format!("Expected RhaiClientError::Timeout, got other error: {:?}", other_error).into())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
212
src/client/src/lib.rs
Normal file
212
src/client/src/lib.rs
Normal file
@ -0,0 +1,212 @@
|
||||
use chrono::Utc;
|
||||
use log::{debug, info, warn, error}; // Added error
|
||||
use redis::AsyncCommands;
|
||||
use tokio::time::{sleep, Instant}; // For polling with timeout
|
||||
use std::time::Duration;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value; // For client_rpc_id, though not directly used by this client's submit method
|
||||
use uuid::Uuid;
|
||||
|
||||
const REDIS_TASK_DETAILS_PREFIX: &str = "rhai_task_details:";
|
||||
const REDIS_QUEUE_PREFIX: &str = "rhai_tasks:";
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct RhaiTaskDetails {
|
||||
pub script: String,
|
||||
pub status: String, // "pending", "processing", "completed", "error"
|
||||
#[serde(rename = "clientRpcId")]
|
||||
pub client_rpc_id: Option<Value>, // Kept for compatibility with worker/server, but optional for client
|
||||
pub output: Option<String>,
|
||||
pub error: Option<String>, // Renamed from error_message for consistency
|
||||
#[serde(rename = "createdAt")]
|
||||
pub created_at: chrono::DateTime<chrono::Utc>,
|
||||
#[serde(rename = "updatedAt")]
|
||||
pub updated_at: chrono::DateTime<chrono::Utc>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum RhaiClientError {
|
||||
RedisError(redis::RedisError),
|
||||
SerializationError(serde_json::Error),
|
||||
Timeout(String), // task_id that timed out
|
||||
TaskNotFound(String), // task_id not found after submission (should be rare)
|
||||
}
|
||||
|
||||
impl From<redis::RedisError> for RhaiClientError {
|
||||
fn from(err: redis::RedisError) -> Self {
|
||||
RhaiClientError::RedisError(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<serde_json::Error> for RhaiClientError {
|
||||
fn from(err: serde_json::Error) -> Self {
|
||||
RhaiClientError::SerializationError(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for RhaiClientError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
RhaiClientError::RedisError(e) => write!(f, "Redis error: {}", e),
|
||||
RhaiClientError::SerializationError(e) => write!(f, "Serialization error: {}", e),
|
||||
RhaiClientError::Timeout(task_id) => write!(f, "Timeout waiting for task {} to complete", task_id),
|
||||
RhaiClientError::TaskNotFound(task_id) => write!(f, "Task {} not found after submission", task_id),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for RhaiClientError {}
|
||||
|
||||
pub struct RhaiClient {
|
||||
redis_client: redis::Client,
|
||||
}
|
||||
|
||||
impl RhaiClient {
|
||||
pub fn new(redis_url: &str) -> Result<Self, RhaiClientError> {
|
||||
let client = redis::Client::open(redis_url)?;
|
||||
Ok(Self { redis_client: client })
|
||||
}
|
||||
|
||||
pub async fn submit_script(
|
||||
&self,
|
||||
circle_name: &str,
|
||||
script: String,
|
||||
client_rpc_id: Option<Value>, // Optional: if the caller has an RPC ID to associate
|
||||
) -> Result<String, RhaiClientError> {
|
||||
let mut conn = self.redis_client.get_multiplexed_async_connection().await?;
|
||||
|
||||
let task_id = Uuid::new_v4().to_string();
|
||||
let now = Utc::now();
|
||||
|
||||
let task_details = RhaiTaskDetails {
|
||||
script,
|
||||
status: "pending".to_string(),
|
||||
client_rpc_id,
|
||||
output: None,
|
||||
error: None,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
};
|
||||
|
||||
let task_key = format!("{}{}", REDIS_TASK_DETAILS_PREFIX, task_id);
|
||||
let queue_key = format!("{}{}", REDIS_QUEUE_PREFIX, circle_name.replace(" ", "_").to_lowercase());
|
||||
|
||||
debug!(
|
||||
"Submitting task_id: {} for circle: {} to queue: {}. Details: {:?}",
|
||||
task_id, circle_name, queue_key, task_details
|
||||
);
|
||||
|
||||
// Using HSET_MULTIPLE for efficiency if redis-rs supports it directly for struct fields.
|
||||
// Otherwise, individual HSETs are fine.
|
||||
// For simplicity and directness with redis-rs async, individual HSETs are used here.
|
||||
conn.hset::<_, _, _, ()>(&task_key, "script", &task_details.script).await?;
|
||||
conn.hset::<_, _, _, ()>(&task_key, "status", &task_details.status).await?;
|
||||
if let Some(rpc_id_val) = &task_details.client_rpc_id {
|
||||
conn.hset::<_, _, _, ()>(&task_key, "clientRpcId", serde_json::to_string(rpc_id_val)?).await?;
|
||||
} else {
|
||||
// Ensure the field exists even if null, or decide if it should be omitted
|
||||
conn.hset::<_, _, _, ()>(&task_key, "clientRpcId", Value::Null.to_string()).await?;
|
||||
}
|
||||
conn.hset::<_, _, _, ()>(&task_key, "createdAt", task_details.created_at.to_rfc3339()).await?;
|
||||
conn.hset::<_, _, _, ()>(&task_key, "updatedAt", task_details.updated_at.to_rfc3339()).await?;
|
||||
// output and error fields are initially None, so they might not be set here or set as empty strings/null
|
||||
|
||||
conn.lpush::<_, _, ()>(&queue_key, &task_id).await?;
|
||||
|
||||
Ok(task_id)
|
||||
}
|
||||
|
||||
// Optional: A method to check task status, similar to what circle_server_ws polling does.
|
||||
// This could be useful for a client that wants to poll for results itself.
|
||||
pub async fn get_task_status(&self, task_id: &str) -> Result<Option<RhaiTaskDetails>, RhaiClientError> {
|
||||
let mut conn = self.redis_client.get_multiplexed_async_connection().await?;
|
||||
let task_key = format!("{}{}", REDIS_TASK_DETAILS_PREFIX, task_id);
|
||||
|
||||
let result_map: Option<std::collections::HashMap<String, String>> = conn.hgetall(&task_key).await?;
|
||||
|
||||
match result_map {
|
||||
Some(map) => {
|
||||
// Reconstruct RhaiTaskDetails from HashMap
|
||||
// This is a simplified reconstruction; ensure all fields are handled robustly
|
||||
let details = RhaiTaskDetails {
|
||||
script: map.get("script").cloned().unwrap_or_default(),
|
||||
status: map.get("status").cloned().unwrap_or_default(),
|
||||
client_rpc_id: map.get("clientRpcId")
|
||||
.and_then(|s| serde_json::from_str(s).ok())
|
||||
.or(Some(Value::Null)), // Default to Value::Null if missing or parse error
|
||||
output: map.get("output").cloned(),
|
||||
error: map.get("error").cloned(),
|
||||
created_at: map.get("createdAt")
|
||||
.and_then(|s| chrono::DateTime::parse_from_rfc3339(s).ok())
|
||||
.map(|dt| dt.with_timezone(&Utc))
|
||||
.unwrap_or_else(Utc::now), // Provide a default
|
||||
updated_at: map.get("updatedAt")
|
||||
.and_then(|s| chrono::DateTime::parse_from_rfc3339(s).ok())
|
||||
.map(|dt| dt.with_timezone(&Utc))
|
||||
.unwrap_or_else(Utc::now), // Provide a default
|
||||
};
|
||||
Ok(Some(details))
|
||||
}
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn submit_script_and_await_result(
|
||||
&self,
|
||||
circle_name: &str,
|
||||
script: String,
|
||||
client_rpc_id: Option<Value>,
|
||||
timeout: Duration,
|
||||
poll_interval: Duration,
|
||||
) -> Result<RhaiTaskDetails, RhaiClientError> {
|
||||
let task_id = self.submit_script(circle_name, script, client_rpc_id).await?;
|
||||
info!("Task {} submitted. Polling for result with timeout {:?}...", task_id, timeout);
|
||||
|
||||
let start_time = Instant::now();
|
||||
loop {
|
||||
if start_time.elapsed() > timeout {
|
||||
warn!("Timeout waiting for task {}", task_id);
|
||||
return Err(RhaiClientError::Timeout(task_id.clone()));
|
||||
}
|
||||
|
||||
match self.get_task_status(&task_id).await {
|
||||
Ok(Some(details)) => {
|
||||
debug!("Polled task {}: status = {}", task_id, details.status);
|
||||
if details.status == "completed" || details.status == "error" {
|
||||
info!("Task {} finished with status: {}", task_id, details.status);
|
||||
return Ok(details);
|
||||
}
|
||||
// else status is "pending" or "processing", continue polling
|
||||
}
|
||||
Ok(None) => {
|
||||
// This case should ideally not happen if submit_script succeeded and worker is running,
|
||||
// unless the task details were manually deleted from Redis.
|
||||
warn!("Task {} not found during polling. This might indicate an issue.", task_id);
|
||||
// Depending on desired robustness, could retry a few times or return an error immediately.
|
||||
// For now, let it continue polling up to timeout, or return a specific error.
|
||||
// If it persists, it's effectively a timeout or a lost task.
|
||||
// Let's consider it a lost task if it's not found after a short while post-submission.
|
||||
if start_time.elapsed() > Duration::from_secs(5) { // Arbitrary short duration
|
||||
return Err(RhaiClientError::TaskNotFound(task_id.clone()));
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
// Log error but continue polling unless it's a critical Redis error
|
||||
error!("Error polling task {}: {}. Will retry.", task_id, e);
|
||||
}
|
||||
}
|
||||
sleep(poll_interval).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
// use super::*;
|
||||
// Basic tests can be added later, especially once examples are in place.
|
||||
// For now, ensuring it compiles is the priority.
|
||||
#[test]
|
||||
fn it_compiles() {
|
||||
assert_eq!(2 + 2, 4);
|
||||
}
|
||||
}
|
794
src/engine/Cargo.lock
generated
Normal file
794
src/engine/Cargo.lock
generated
Normal file
@ -0,0 +1,794 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "ahash"
|
||||
version = "0.8.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"const-random",
|
||||
"getrandom 0.3.3",
|
||||
"once_cell",
|
||||
"version_check",
|
||||
"zerocopy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "android-tzdata"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
|
||||
|
||||
[[package]]
|
||||
name = "android_system_properties"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "arrayvec"
|
||||
version = "0.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
|
||||
|
||||
[[package]]
|
||||
name = "bincode"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "36eaf5d7b090263e8150820482d5d93cd964a81e4019913c972f4edcc6edb740"
|
||||
dependencies = [
|
||||
"bincode_derive",
|
||||
"serde",
|
||||
"unty",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bincode_derive"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bf95709a440f45e986983918d0e8a1f30a9b1df04918fc828670606804ac3c09"
|
||||
dependencies = [
|
||||
"virtue",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967"
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.17.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.2.25"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d0fc897dc1e865cc67c0e05a836d9d3f1df3cbe442aa4a9473b18e12624a4951"
|
||||
dependencies = [
|
||||
"shlex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||
|
||||
[[package]]
|
||||
name = "chrono"
|
||||
version = "0.4.41"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d"
|
||||
dependencies = [
|
||||
"android-tzdata",
|
||||
"iana-time-zone",
|
||||
"js-sys",
|
||||
"num-traits",
|
||||
"serde",
|
||||
"wasm-bindgen",
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "const-random"
|
||||
version = "0.1.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359"
|
||||
dependencies = [
|
||||
"const-random-macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "const-random-macro"
|
||||
version = "0.1.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e"
|
||||
dependencies = [
|
||||
"getrandom 0.2.16",
|
||||
"once_cell",
|
||||
"tiny-keccak",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation-sys"
|
||||
version = "0.8.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
|
||||
|
||||
[[package]]
|
||||
name = "crc32fast"
|
||||
version = "1.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crunchy"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929"
|
||||
|
||||
[[package]]
|
||||
name = "engine"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"heromodels",
|
||||
"heromodels-derive",
|
||||
"heromodels_core",
|
||||
"rhai",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.2.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"wasi 0.11.0+wasi-snapshot-preview1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"r-efi",
|
||||
"wasi 0.14.2+wasi-0.2.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||
|
||||
[[package]]
|
||||
name = "heromodels"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"bincode",
|
||||
"chrono",
|
||||
"heromodels-derive",
|
||||
"heromodels_core",
|
||||
"ourdb",
|
||||
"rhai",
|
||||
"rhai_client_macros",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"strum",
|
||||
"strum_macros",
|
||||
"tst",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "heromodels-derive"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "heromodels_core"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iana-time-zone"
|
||||
version = "0.1.63"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8"
|
||||
dependencies = [
|
||||
"android_system_properties",
|
||||
"core-foundation-sys",
|
||||
"iana-time-zone-haiku",
|
||||
"js-sys",
|
||||
"log",
|
||||
"wasm-bindgen",
|
||||
"windows-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iana-time-zone-haiku"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
|
||||
dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "instant"
|
||||
version = "0.1.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.77"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.172"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa"
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.7.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
|
||||
|
||||
[[package]]
|
||||
name = "no-std-compat"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b93853da6d84c2e3c7d730d6473e8817692dd89be387eb01b94d7f108ecb5b8c"
|
||||
dependencies = [
|
||||
"spin",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
||||
dependencies = [
|
||||
"portable-atomic",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ourdb"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"crc32fast",
|
||||
"log",
|
||||
"rand",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "portable-atomic"
|
||||
version = "1.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e"
|
||||
|
||||
[[package]]
|
||||
name = "ppv-lite86"
|
||||
version = "0.2.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
|
||||
dependencies = [
|
||||
"zerocopy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.95"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.40"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "r-efi"
|
||||
version = "5.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5"
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.8.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"rand_chacha",
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_chacha"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
|
||||
dependencies = [
|
||||
"ppv-lite86",
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.6.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
|
||||
dependencies = [
|
||||
"getrandom 0.2.16",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rhai"
|
||||
version = "1.22.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2780e813b755850e50b178931aaf94ed24f6817f46aaaf5d21c13c12d939a249"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"bitflags",
|
||||
"instant",
|
||||
"no-std-compat",
|
||||
"num-traits",
|
||||
"once_cell",
|
||||
"rhai_codegen",
|
||||
"rust_decimal",
|
||||
"smallvec",
|
||||
"smartstring",
|
||||
"thin-vec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rhai_client_macros"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rhai",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rhai_codegen"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a5a11a05ee1ce44058fa3d5961d05194fdbe3ad6b40f904af764d81b86450e6b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust_decimal"
|
||||
version = "1.37.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "faa7de2ba56ac291bd90c6b9bece784a52ae1411f9506544b3eae36dd2356d50"
|
||||
dependencies = [
|
||||
"arrayvec",
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustversion"
|
||||
version = "1.0.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d"
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "1.0.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.219"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.219"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.140"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
"ryu",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shlex"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9"
|
||||
|
||||
[[package]]
|
||||
name = "smartstring"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3fb72c633efbaa2dd666986505016c32c3044395ceaf881518399d2f4127ee29"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"static_assertions",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "spin"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d"
|
||||
|
||||
[[package]]
|
||||
name = "static_assertions"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
|
||||
|
||||
[[package]]
|
||||
name = "strum"
|
||||
version = "0.26.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06"
|
||||
|
||||
[[package]]
|
||||
name = "strum_macros"
|
||||
version = "0.26.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rustversion",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.101"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thin-vec"
|
||||
version = "0.2.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "144f754d318415ac792f9d69fc87abbbfc043ce2ef041c60f16ad828f638717d"
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.69"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "1.0.69"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tiny-keccak"
|
||||
version = "2.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237"
|
||||
dependencies = [
|
||||
"crunchy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tst"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"ourdb",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
|
||||
|
||||
[[package]]
|
||||
name = "unty"
|
||||
version = "0.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6d49784317cd0d1ee7ec5c716dd598ec5b4483ea832a2dced265471cc0f690ae"
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.17.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d"
|
||||
dependencies = [
|
||||
"getrandom 0.3.3",
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
version = "0.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||
|
||||
[[package]]
|
||||
name = "virtue"
|
||||
version = "0.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "051eb1abcf10076295e815102942cc58f9d5e3b4560e46e53c21e8ff6f3af7b1"
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.11.0+wasi-snapshot-preview1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.14.2+wasi-0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3"
|
||||
dependencies = [
|
||||
"wit-bindgen-rt",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.100"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"once_cell",
|
||||
"rustversion",
|
||||
"wasm-bindgen-macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-backend"
|
||||
version = "0.2.100"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"log",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro"
|
||||
version = "0.2.100"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"wasm-bindgen-macro-support",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro-support"
|
||||
version = "0.2.100"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"wasm-bindgen-backend",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-shared"
|
||||
version = "0.2.100"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-core"
|
||||
version = "0.61.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3"
|
||||
dependencies = [
|
||||
"windows-implement",
|
||||
"windows-interface",
|
||||
"windows-link",
|
||||
"windows-result",
|
||||
"windows-strings",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-implement"
|
||||
version = "0.60.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-interface"
|
||||
version = "0.59.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-link"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38"
|
||||
|
||||
[[package]]
|
||||
name = "windows-result"
|
||||
version = "0.3.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-strings"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen-rt"
|
||||
version = "0.39.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.8.25"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb"
|
||||
dependencies = [
|
||||
"zerocopy-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy-derive"
|
||||
version = "0.8.25"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
37
src/engine/Cargo.toml
Normal file
37
src/engine/Cargo.toml
Normal file
@ -0,0 +1,37 @@
|
||||
[package]
|
||||
name = "engine"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
description = "Central Rhai engine for heromodels"
|
||||
|
||||
[dependencies]
|
||||
rhai = { version = "1.21.0", features = ["std", "sync", "decimal", "internals"] }
|
||||
heromodels = { path = "../../../db/heromodels", features = ["rhai"] }
|
||||
heromodels_core = { path = "../../../db/heromodels_core" }
|
||||
chrono = "0.4"
|
||||
heromodels-derive = { path = "../../../db/heromodels-derive" }
|
||||
|
||||
[features]
|
||||
default = ["calendar", "finance"]
|
||||
calendar = []
|
||||
finance = []
|
||||
# Flow module is now updated to use our approach to Rhai engine registration
|
||||
flow = []
|
||||
legal = []
|
||||
projects = []
|
||||
biz = []
|
||||
|
||||
[[example]]
|
||||
name = "calendar_example"
|
||||
path = "examples/calendar/example.rs"
|
||||
required-features = ["calendar"]
|
||||
|
||||
[[example]]
|
||||
name = "flow_example"
|
||||
path = "examples/flow/example.rs"
|
||||
required-features = ["flow"]
|
||||
|
||||
[[example]]
|
||||
name = "finance_example"
|
||||
path = "examples/finance/example.rs"
|
||||
required-features = ["finance"]
|
135
src/engine/README.md
Normal file
135
src/engine/README.md
Normal file
@ -0,0 +1,135 @@
|
||||
# HeroModels Rhai Engine (`engine`)
|
||||
|
||||
The `engine` crate provides a central Rhai scripting engine for the HeroModels project. It offers a unified way to interact with various HeroModels modules (like Calendar, Flow, Legal, etc.) through Rhai scripts, leveraging a shared database connection.
|
||||
|
||||
## Overview
|
||||
|
||||
This crate facilitates:
|
||||
|
||||
1. **Centralized Engine Creation**: A function `create_heromodels_engine` to instantiate a Rhai engine pre-configured with common settings and all enabled HeroModels modules.
|
||||
2. **Modular Registration**: HeroModels modules (Calendar, Flow, etc.) can be registered with a Rhai engine based on feature flags.
|
||||
3. **Script Evaluation Utilities**: Helper functions for compiling Rhai scripts into Abstract Syntax Trees (ASTs) and for evaluating scripts or ASTs.
|
||||
4. **Mock Database**: Includes a `mock_db` module for testing and running examples without needing a live database.
|
||||
|
||||
## Core Components & Usage
|
||||
|
||||
### Library (`src/lib.rs`)
|
||||
|
||||
- **`create_heromodels_engine(db: Arc<OurDB>) -> Engine`**:
|
||||
Creates and returns a new `rhai::Engine` instance. This engine is configured with default settings (e.g., max expression depths, string/array/map sizes) and then all available HeroModels modules (controlled by feature flags) are registered with it, using the provided `db` (an `Arc<OurDB>`) instance.
|
||||
|
||||
- **`register_all_modules(engine: &mut Engine, db: Arc<OurDB>)`**:
|
||||
Registers all HeroModels modules for which features are enabled (e.g., `calendar`, `flow`, `legal`, `projects`, `biz`) with the given Rhai `engine`. Each module is passed the shared `db` instance.
|
||||
|
||||
- **`eval_script(engine: &Engine, script: &str) -> Result<rhai::Dynamic, Box<rhai::EvalAltResult>>`**:
|
||||
A utility function to directly evaluate a Rhai script string using the provided `engine`.
|
||||
|
||||
- **`compile_script(engine: &Engine, script: &str) -> Result<AST, Box<rhai::EvalAltResult>>`**:
|
||||
Compiles a Rhai script string into an `AST` (Abstract Syntax Tree) for potentially faster repeated execution.
|
||||
|
||||
- **`run_ast(engine: &Engine, ast: &AST, scope: &mut Scope) -> Result<rhai::Dynamic, Box<rhai::EvalAltResult>>`**:
|
||||
Runs a pre-compiled `AST` with a given `scope` using the provided `engine`.
|
||||
|
||||
- **`mock_db` module**:
|
||||
Provides `create_mock_db()` which returns an `Arc<OurDB>` instance suitable for testing and examples. This allows scripts that interact with database functionalities to run without external database dependencies.
|
||||
|
||||
### Basic Usage
|
||||
|
||||
```rust
|
||||
use std::sync::Arc;
|
||||
use engine::{create_heromodels_engine, eval_script};
|
||||
use engine::mock_db::create_mock_db; // For example usage
|
||||
use heromodels::db::hero::OurDB; // Actual DB type
|
||||
|
||||
// Create a mock database (or connect to a real one)
|
||||
let db: Arc<OurDB> = create_mock_db();
|
||||
|
||||
// Create the Rhai engine with all enabled modules registered
|
||||
let engine = create_heromodels_engine(db);
|
||||
|
||||
// Run a Rhai script
|
||||
let script = r#"
|
||||
// Example: Assuming 'calendar' feature is enabled
|
||||
let cal = new_calendar("My Test Calendar");
|
||||
cal.set_description("This is a test.");
|
||||
print(`Created calendar: ${cal.get_name()}`);
|
||||
cal.get_id() // Return the ID
|
||||
"#;
|
||||
|
||||
match eval_script(&engine, script) {
|
||||
Ok(val) => println!("Script returned: {:?}", val),
|
||||
Err(err) => eprintln!("Script error: {}", err),
|
||||
}
|
||||
```
|
||||
|
||||
### Using Specific Modules Manually
|
||||
|
||||
If you need more fine-grained control or only want specific modules (and prefer not to rely solely on feature flags at compile time for `create_heromodels_engine`), you can initialize an engine and register modules manually:
|
||||
|
||||
```rust
|
||||
use std::sync::Arc;
|
||||
use rhai::Engine;
|
||||
use engine::mock_db::create_mock_db; // For example usage
|
||||
use heromodels::db::hero::OurDB;
|
||||
// Import the specific module registration function
|
||||
use heromodels::models::calendar::register_calendar_rhai_module;
|
||||
|
||||
|
||||
// Create a mock database
|
||||
let db: Arc<OurDB> = create_mock_db();
|
||||
|
||||
// Create a new Rhai engine
|
||||
let mut engine = Engine::new();
|
||||
|
||||
// Register only the calendar module
|
||||
register_calendar_rhai_module(&mut engine, db.clone());
|
||||
|
||||
// Now you can use calendar-related functions in your scripts
|
||||
let result = engine.eval::<String>(r#" let c = new_calendar("Solo Cal"); c.get_name() "#);
|
||||
match result {
|
||||
Ok(name) => println!("Calendar name: {}", name),
|
||||
Err(err) => eprintln!("Error: {}", err),
|
||||
}
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
This crate includes several examples demonstrating how to use different HeroModels modules with Rhai. Each example typically requires its corresponding feature to be enabled.
|
||||
|
||||
- `calendar_example`: Working with calendars, events, and attendees (requires `calendar` feature).
|
||||
- `flow_example`: Working with flows, steps, and signature requirements (requires `flow` feature).
|
||||
- `finance_example`: Working with financial models (requires `finance` feature).
|
||||
- *(Additional examples for `legal`, `projects`, `biz` would follow the same pattern if present).*
|
||||
|
||||
To run an example (e.g., `calendar_example`):
|
||||
|
||||
```bash
|
||||
cargo run --example calendar_example --features calendar
|
||||
```
|
||||
*(Note: Examples in `Cargo.toml` already specify `required-features`, so simply `cargo run --example calendar_example` might suffice if those features are part of the default set or already enabled.)*
|
||||
|
||||
## Features
|
||||
|
||||
The crate uses feature flags to control which HeroModels modules are compiled and registered:
|
||||
|
||||
- `calendar`: Enables the Calendar module.
|
||||
- `finance`: Enables the Finance module.
|
||||
- `flow`: Enables the Flow module.
|
||||
- `legal`: Enables the Legal module.
|
||||
- `projects`: Enables the Projects module.
|
||||
- `biz`: Enables the Business module.
|
||||
|
||||
The `default` features are `["calendar", "finance"]`. You can enable other modules by specifying them during the build or in your project's `Cargo.toml` if this `engine` crate is a dependency.
|
||||
|
||||
## Dependencies
|
||||
|
||||
Key dependencies include:
|
||||
- `rhai`: The Rhai scripting engine.
|
||||
- `heromodels`: Provides the core data models and database interaction logic, including the Rhai registration functions for each module.
|
||||
- `heromodels_core`: Core utilities for HeroModels.
|
||||
- `chrono`: For date/time utilities.
|
||||
- `heromodels-derive`: Procedural macros used by HeroModels.
|
||||
|
||||
## License
|
||||
|
||||
This crate is part of the HeroModels project and shares its license.
|
16
src/engine/build.rs
Normal file
16
src/engine/build.rs
Normal file
@ -0,0 +1,16 @@
|
||||
fn main() {
|
||||
// Tell Cargo to re-run this build script if the calendar/rhai.rs file changes
|
||||
println!("cargo:rerun-if-changed=../heromodels/src/models/calendar/rhai.rs");
|
||||
|
||||
// Tell Cargo to re-run this build script if the flow/rhai.rs file changes
|
||||
println!("cargo:rerun-if-changed=../heromodels/src/models/flow/rhai.rs");
|
||||
|
||||
// Tell Cargo to re-run this build script if the legal/rhai.rs file changes
|
||||
println!("cargo:rerun-if-changed=../heromodels/src/models/legal/rhai.rs");
|
||||
|
||||
// Tell Cargo to re-run this build script if the projects/rhai.rs file changes
|
||||
println!("cargo:rerun-if-changed=../heromodels/src/models/projects/rhai.rs");
|
||||
|
||||
// Tell Cargo to re-run this build script if the biz/rhai.rs file changes
|
||||
println!("cargo:rerun-if-changed=../heromodels/src/models/biz/rhai.rs");
|
||||
}
|
101
src/engine/examples/calendar/calendar_script.rhai
Normal file
101
src/engine/examples/calendar/calendar_script.rhai
Normal file
@ -0,0 +1,101 @@
|
||||
// calendar_script.rhai
|
||||
// Example Rhai script for working with Calendar models
|
||||
|
||||
// Constants for AttendanceStatus
|
||||
const NO_RESPONSE = "NoResponse";
|
||||
const ACCEPTED = "Accepted";
|
||||
const DECLINED = "Declined";
|
||||
const TENTATIVE = "Tentative";
|
||||
|
||||
// Create a new calendar using builder pattern
|
||||
let my_calendar = new_calendar()
|
||||
.name("Team Calendar")
|
||||
.description("Calendar for team events and meetings");
|
||||
|
||||
print(`Created calendar: ${my_calendar.name} (${my_calendar.id})`);
|
||||
|
||||
|
||||
// Add attendees to the event
|
||||
let alice = new_attendee()
|
||||
.with_contact_id(1)
|
||||
.with_status(NO_RESPONSE);
|
||||
let bob = new_attendee()
|
||||
.with_contact_id(2)
|
||||
.with_status(ACCEPTED);
|
||||
let charlie = new_attendee()
|
||||
.with_contact_id(3)
|
||||
.with_status(TENTATIVE);
|
||||
|
||||
|
||||
// Create a new event using builder pattern
|
||||
// Note: Timestamps are in seconds since epoch
|
||||
let now = timestamp_now();
|
||||
let one_hour = 60 * 60;
|
||||
let meeting = new_event()
|
||||
.title("Weekly Sync")
|
||||
.reschedule(now, now + one_hour)
|
||||
.location("Conference Room A")
|
||||
.description("Regular team sync meeting")
|
||||
.add_attendee(alice)
|
||||
.add_attendee(bob)
|
||||
.add_attendee(charlie)
|
||||
.save_event();
|
||||
|
||||
print(`Created event: ${meeting.title}`);
|
||||
|
||||
meeting.delete_event();
|
||||
|
||||
print(`Deleted event: ${meeting.title}`);
|
||||
|
||||
// Print attendees info
|
||||
let attendees = meeting.attendees;
|
||||
print(`Added attendees to the event`);
|
||||
|
||||
// Update Charlie's attendee status directly
|
||||
meeting.update_attendee_status(3, ACCEPTED);
|
||||
print(`Updated Charlie's status to: ${ACCEPTED}`);
|
||||
|
||||
// Add the event to the calendar
|
||||
my_calendar.add_event_to_calendar(meeting);
|
||||
// Print events info
|
||||
print(`Added event to calendar`);
|
||||
|
||||
// Save the calendar to the database
|
||||
let saved_calendar = my_calendar.save_calendar();
|
||||
print(`Calendar saved to database with ID: ${saved_calendar.id}`);
|
||||
|
||||
// Retrieve the calendar from the database using the ID from the saved calendar
|
||||
let retrieved_calendar = get_calendar_by_id(saved_calendar.id);
|
||||
if retrieved_calendar != () {
|
||||
print(`Retrieved calendar: ${retrieved_calendar.name}`);
|
||||
print(`Retrieved calendar successfully`);
|
||||
} else {
|
||||
print("Failed to retrieve calendar from database");
|
||||
}
|
||||
|
||||
// List all calendars in the database
|
||||
let all_calendars = list_calendars();
|
||||
print("\nListing all calendars in database:");
|
||||
let calendar_count = 0;
|
||||
for calendar in all_calendars {
|
||||
print(` - Calendar: ${calendar.name} (ID: ${calendar.id})`);
|
||||
calendar_count += 1;
|
||||
}
|
||||
print(`Total calendars: ${calendar_count}`);
|
||||
|
||||
// List all events in the database
|
||||
let all_events = list_events();
|
||||
print("\nListing all events in database:");
|
||||
let event_count = 0;
|
||||
for event in all_events {
|
||||
print(` - Event: ${event.title} (ID: ${event.id})`);
|
||||
event_count += 1;
|
||||
}
|
||||
print(`Total events: ${event_count}`);
|
||||
|
||||
// Helper function to get current timestamp
|
||||
fn timestamp_now() {
|
||||
// This would typically be provided by the host application
|
||||
// For this example, we'll use a fixed timestamp
|
||||
1685620800 // June 1, 2023, 12:00 PM
|
||||
}
|
66
src/engine/examples/calendar/example.rs
Normal file
66
src/engine/examples/calendar/example.rs
Normal file
@ -0,0 +1,66 @@
|
||||
use std::sync::Arc;
|
||||
use std::path::Path;
|
||||
use rhai::{Engine, Scope};
|
||||
use heromodels::models::calendar::{Calendar, Event, Attendee, AttendanceStatus};
|
||||
use engine::{create_heromodels_engine, eval_file};
|
||||
use engine::mock_db::{create_mock_db, seed_mock_db};
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
println!("Calendar Rhai Example");
|
||||
println!("=====================");
|
||||
|
||||
// Create a mock database
|
||||
let db = create_mock_db();
|
||||
|
||||
// Seed the database with some initial data
|
||||
seed_mock_db(db.clone());
|
||||
|
||||
// Create the Rhai engine using our central engine creator
|
||||
let mut engine = create_heromodels_engine(db.clone());
|
||||
|
||||
// Register timestamp helper functions
|
||||
register_timestamp_helpers(&mut engine);
|
||||
|
||||
// Get the path to the script
|
||||
let script_path = Path::new(file!())
|
||||
.parent()
|
||||
.unwrap()
|
||||
.join("calendar_script.rhai");
|
||||
|
||||
println!("\nRunning script: {}", script_path.display());
|
||||
println!("---------------------");
|
||||
|
||||
// Run the script
|
||||
match eval_file(&engine, &script_path.to_string_lossy()) {
|
||||
Ok(result) => {
|
||||
if !result.is_unit() {
|
||||
println!("\nScript returned: {:?}", result);
|
||||
}
|
||||
println!("\nScript executed successfully!");
|
||||
Ok(())
|
||||
},
|
||||
Err(err) => {
|
||||
eprintln!("\nError running script: {}", err);
|
||||
Err(Box::new(std::io::Error::new(std::io::ErrorKind::Other, err.to_string())))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Register timestamp helper functions with the engine
|
||||
fn register_timestamp_helpers(engine: &mut Engine) {
|
||||
use chrono::{DateTime, Utc, TimeZone, NaiveDateTime};
|
||||
|
||||
// Function to get current timestamp
|
||||
engine.register_fn("timestamp_now", || {
|
||||
Utc::now().timestamp() as i64
|
||||
});
|
||||
|
||||
// Function to format a timestamp
|
||||
engine.register_fn("format_timestamp", |ts: i64| {
|
||||
let dt = Utc.timestamp_opt(ts, 0).single()
|
||||
.expect("Invalid timestamp");
|
||||
dt.format("%Y-%m-%d %H:%M:%S UTC").to_string()
|
||||
});
|
||||
|
||||
println!("Timestamp helper functions registered successfully.");
|
||||
}
|
68
src/engine/examples/finance/example.rs
Normal file
68
src/engine/examples/finance/example.rs
Normal file
@ -0,0 +1,68 @@
|
||||
use std::sync::Arc;
|
||||
use std::path::Path;
|
||||
use rhai::{Engine, Scope};
|
||||
use heromodels::models::finance::account::Account;
|
||||
use heromodels::models::finance::asset::{Asset, AssetType};
|
||||
use heromodels::models::finance::marketplace::{Listing, Bid, ListingStatus, ListingType, BidStatus};
|
||||
use engine::{create_heromodels_engine, eval_file};
|
||||
use engine::mock_db::{create_mock_db, seed_mock_db};
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
println!("Finance Rhai Example");
|
||||
println!("===================");
|
||||
|
||||
// Create a mock database
|
||||
let db = create_mock_db();
|
||||
|
||||
// Seed the database with some initial data
|
||||
seed_mock_db(db.clone());
|
||||
|
||||
// Create the Rhai engine using our central engine creator
|
||||
let mut engine = create_heromodels_engine(db.clone());
|
||||
|
||||
// Register timestamp helper functions
|
||||
register_timestamp_helpers(&mut engine);
|
||||
|
||||
// Get the path to the script
|
||||
let script_path = Path::new(file!())
|
||||
.parent()
|
||||
.unwrap()
|
||||
.join("finance_script.rhai");
|
||||
|
||||
println!("\nRunning script: {}", script_path.display());
|
||||
println!("---------------------");
|
||||
|
||||
// Run the script
|
||||
match eval_file(&engine, &script_path.to_string_lossy()) {
|
||||
Ok(result) => {
|
||||
if !result.is_unit() {
|
||||
println!("\nScript returned: {:?}", result);
|
||||
}
|
||||
println!("\nScript executed successfully!");
|
||||
Ok(())
|
||||
},
|
||||
Err(err) => {
|
||||
eprintln!("\nError running script: {}", err);
|
||||
Err(Box::new(std::io::Error::new(std::io::ErrorKind::Other, err.to_string())))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Register timestamp helper functions with the engine
|
||||
fn register_timestamp_helpers(engine: &mut Engine) {
|
||||
use chrono::{DateTime, Utc, TimeZone, NaiveDateTime};
|
||||
|
||||
// Function to get current timestamp
|
||||
engine.register_fn("timestamp_now", || {
|
||||
Utc::now().timestamp() as i64
|
||||
});
|
||||
|
||||
// Function to format a timestamp
|
||||
engine.register_fn("format_timestamp", |ts: i64| {
|
||||
let dt = Utc.timestamp_opt(ts, 0).single()
|
||||
.expect("Invalid timestamp");
|
||||
dt.format("%Y-%m-%d %H:%M:%S UTC").to_string()
|
||||
});
|
||||
|
||||
println!("Timestamp helper functions registered successfully.");
|
||||
}
|
202
src/engine/examples/finance/finance_script.rhai
Normal file
202
src/engine/examples/finance/finance_script.rhai
Normal file
@ -0,0 +1,202 @@
|
||||
// finance_script.rhai
|
||||
// Example Rhai script for working with Finance models
|
||||
|
||||
// Constants for AssetType
|
||||
const NATIVE = "Native";
|
||||
const ERC20 = "Erc20";
|
||||
const ERC721 = "Erc721";
|
||||
const ERC1155 = "Erc1155";
|
||||
|
||||
// Constants for ListingStatus
|
||||
const ACTIVE = "Active";
|
||||
const SOLD = "Sold";
|
||||
const CANCELLED = "Cancelled";
|
||||
const EXPIRED = "Expired";
|
||||
|
||||
// Constants for ListingType
|
||||
const FIXED_PRICE = "FixedPrice";
|
||||
const AUCTION = "Auction";
|
||||
const EXCHANGE = "Exchange";
|
||||
|
||||
// Constants for BidStatus
|
||||
const BID_ACTIVE = "Active";
|
||||
const BID_ACCEPTED = "Accepted";
|
||||
const BID_REJECTED = "Rejected";
|
||||
const BID_CANCELLED = "Cancelled";
|
||||
|
||||
// Create a new account using builder pattern
|
||||
let alice_account = new_account()
|
||||
.name("Alice's Account")
|
||||
.user_id(101)
|
||||
.description("Alice's primary trading account")
|
||||
.ledger("ethereum")
|
||||
.address("0x1234567890abcdef1234567890abcdef12345678")
|
||||
.pubkey("0xabcdef1234567890abcdef1234567890abcdef12");
|
||||
|
||||
print(`Created account: ${alice_account.get_name()} (User ID: ${alice_account.get_user_id()})`);
|
||||
|
||||
// Save the account to the database
|
||||
let saved_alice = set_account(alice_account);
|
||||
print(`Account saved to database with ID: ${saved_alice.get_id()}`);
|
||||
|
||||
// Create a new asset using builder pattern
|
||||
let token_asset = new_asset()
|
||||
.name("HERO Token")
|
||||
.description("Herocode governance token")
|
||||
.amount(1000.0)
|
||||
.address("0x9876543210abcdef9876543210abcdef98765432")
|
||||
.asset_type(ERC20)
|
||||
.decimals(18);
|
||||
|
||||
print(`Created asset: ${token_asset.get_name()} (${token_asset.get_amount()} ${token_asset.get_asset_type()})`);
|
||||
|
||||
// Save the asset to the database
|
||||
let saved_token = set_asset(token_asset);
|
||||
print(`Asset saved to database with ID: ${saved_token.get_id()}`);
|
||||
|
||||
// Add the asset to Alice's account
|
||||
saved_alice = saved_alice.add_asset(saved_token.get_id());
|
||||
saved_alice = set_account(saved_alice);
|
||||
print(`Added asset ${saved_token.get_name()} to ${saved_alice.get_name()}`);
|
||||
|
||||
// Create a new NFT asset
|
||||
let nft_asset = new_asset()
|
||||
.name("Herocode #42")
|
||||
.description("Unique digital collectible")
|
||||
.amount(1.0)
|
||||
.address("0xabcdef1234567890abcdef1234567890abcdef12")
|
||||
.asset_type(ERC721)
|
||||
.decimals(0);
|
||||
|
||||
// Save the NFT to the database
|
||||
let saved_nft = set_asset(nft_asset);
|
||||
print(`NFT saved to database with ID: ${saved_nft.get_id()}`);
|
||||
|
||||
// Create Bob's account
|
||||
let bob_account = new_account()
|
||||
.name("Bob's Account")
|
||||
.user_id(102)
|
||||
.description("Bob's trading account")
|
||||
.ledger("ethereum")
|
||||
.address("0xfedcba0987654321fedcba0987654321fedcba09")
|
||||
.pubkey("0x654321fedcba0987654321fedcba0987654321fe");
|
||||
|
||||
// Save Bob's account
|
||||
let saved_bob = set_account(bob_account);
|
||||
print(`Created and saved Bob's account with ID: ${saved_bob.get_id()}`);
|
||||
|
||||
// Create a listing for the NFT
|
||||
let nft_listing = new_listing()
|
||||
.seller_id(saved_alice.get_id())
|
||||
.asset_id(saved_nft.get_id())
|
||||
.price(0.5)
|
||||
.currency("ETH")
|
||||
.listing_type(AUCTION)
|
||||
.title("Rare Herocode NFT")
|
||||
.description("One of a kind digital collectible")
|
||||
.image_url("https://example.com/nft/42.png")
|
||||
.expires_at(timestamp_now() + 86400) // 24 hours from now
|
||||
.add_tag("rare")
|
||||
.add_tag("collectible")
|
||||
.add_tag("digital art")
|
||||
.set_listing();
|
||||
|
||||
// Save the listing
|
||||
print(`Created listing: ${nft_listing.get_title()} (ID: ${nft_listing.get_id()})`);
|
||||
print(`Listing status: ${nft_listing.get_status()}, Type: ${nft_listing.get_listing_type()}`);
|
||||
print(`Listing price: ${nft_listing.get_price()} ${nft_listing.get_currency()}`);
|
||||
|
||||
// Create a bid from Bob
|
||||
let bob_bid = new_bid()
|
||||
.listing_id(nft_listing.get_id().to_string())
|
||||
.bidder_id(saved_bob.get_id())
|
||||
.amount(1.5)
|
||||
.currency("ETH")
|
||||
.set_bid();
|
||||
|
||||
// Save the bid
|
||||
print(`Created bid from ${saved_bob.get_name()} for ${bob_bid.get_amount()} ${bob_bid.get_currency()}`);
|
||||
|
||||
// Add the bid to the listing
|
||||
nft_listing.add_bid(bob_bid);
|
||||
nft_listing.set_listing();
|
||||
print(`Added bid to listing ${nft_listing.get_title()}`);
|
||||
|
||||
// Create another bid with higher amount
|
||||
let charlie_account = new_account()
|
||||
.name("Charlie's Account")
|
||||
.user_id(103)
|
||||
.description("Charlie's trading account")
|
||||
.ledger("ethereum")
|
||||
.address("0x1122334455667788991122334455667788990011")
|
||||
.pubkey("0x8877665544332211887766554433221188776655");
|
||||
|
||||
let saved_charlie = set_account(charlie_account);
|
||||
print(`Created and saved Charlie's account with ID: ${saved_charlie.get_id()}`);
|
||||
|
||||
let charlie_bid = new_bid()
|
||||
.listing_id(nft_listing.get_id().to_string())
|
||||
.bidder_id(saved_charlie.get_id())
|
||||
.amount(2.5)
|
||||
.currency("ETH")
|
||||
.set_bid();
|
||||
|
||||
print(`Created higher bid from ${saved_charlie.get_name()} for ${charlie_bid.get_amount()} ${charlie_bid.get_currency()}`);
|
||||
|
||||
// Add the higher bid to the listing
|
||||
nft_listing.add_bid(charlie_bid)
|
||||
.set_listing();
|
||||
|
||||
|
||||
|
||||
print(`Added higher bid to listing ${nft_listing.get_title()}`);
|
||||
|
||||
nft_listing.sale_price(2.5)
|
||||
.set_listing();
|
||||
|
||||
// Complete the sale to the highest bidder (Charlie)
|
||||
nft_listing.complete_sale(saved_charlie.get_id())
|
||||
.set_listing();
|
||||
|
||||
print(`Completed sale of ${nft_listing.get_title()} to ${saved_charlie.get_name()}`);
|
||||
print(`New listing status: ${saved_listing.get_status()}`);
|
||||
|
||||
// Retrieve the listing from the database
|
||||
let retrieved_listing = get_listing_by_id(saved_listing.get_id());
|
||||
print(`Retrieved listing: ${retrieved_listing.get_title()} (Status: ${retrieved_listing.get_status()})`);
|
||||
|
||||
// Create a fixed price listing
|
||||
let token_listing = new_listing()
|
||||
.seller_id(saved_alice.get_id())
|
||||
.asset_id(saved_token.get_id())
|
||||
.price(100.0)
|
||||
.currency("USDC")
|
||||
.listing_type(FIXED_PRICE)
|
||||
.title("HERO Tokens for Sale")
|
||||
.description("100 HERO tokens at fixed price")
|
||||
.set_listing();
|
||||
|
||||
// Save the fixed price listing
|
||||
print(`Created fixed price listing: ${token_listing.get_title()} (ID: ${token_listing.get_id()})`);
|
||||
|
||||
// Cancel the listing
|
||||
token_listing.cancel();
|
||||
token_listing.set_listing();
|
||||
print(`Cancelled listing: ${token_listing.get_title()}`);
|
||||
print(`Listing status: ${token_listing.get_status()}`);
|
||||
|
||||
// Print summary of all accounts
|
||||
print("\nAccount Summary:");
|
||||
print(`Alice (ID: ${saved_alice.get_id()}): ${saved_alice.get_assets().len()} assets`);
|
||||
print(`Bob (ID: ${saved_bob.get_id()}): ${saved_bob.get_assets().len()} assets`);
|
||||
print(`Charlie (ID: ${saved_charlie.get_id()}): ${saved_charlie.get_assets().len()} assets`);
|
||||
|
||||
// Print summary of all listings
|
||||
print("\nListing Summary:");
|
||||
print(`NFT Auction (ID: ${nft_listing.get_id()}): ${nft_listing.get_status()}`);
|
||||
print(`Token Sale (ID: ${token_listing.get_id()}): ${token_listing.get_status()}`);
|
||||
|
||||
// Print summary of all bids
|
||||
print("\nBid Summary:");
|
||||
print(`Bob's bid: ${bob_bid.get_amount()} ${bob_bid.get_currency()} (Status: ${bob_bid.get_status()})`);
|
||||
print(`Charlie's bid: ${charlie_bid.get_amount()} ${charlie_bid.get_currency()} (Status: ${charlie_bid.get_status()})`);
|
120
src/engine/examples/flow/example.rs
Normal file
120
src/engine/examples/flow/example.rs
Normal file
@ -0,0 +1,120 @@
|
||||
use std::path::Path;
|
||||
use rhai::{Scope};
|
||||
use heromodels::models::flow::{Flow, FlowStep, SignatureRequirement};
|
||||
use engine::{create_heromodels_engine, eval_file};
|
||||
use engine::mock_db::{create_mock_db, seed_mock_db};
|
||||
use heromodels_core::Model;
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
println!("Flow Rhai Example");
|
||||
println!("=================");
|
||||
|
||||
// Create a mock database
|
||||
let db = create_mock_db();
|
||||
|
||||
// Seed the database with initial data
|
||||
seed_mock_db(db.clone());
|
||||
|
||||
// Create the Rhai engine with all modules registered
|
||||
let engine = create_heromodels_engine(db.clone());
|
||||
|
||||
// Get the path to the script
|
||||
let script_path = Path::new(file!())
|
||||
.parent()
|
||||
.unwrap()
|
||||
.join("flow_script.rhai");
|
||||
|
||||
println!("\nRunning script: {}", script_path.display());
|
||||
println!("---------------------");
|
||||
|
||||
// Run the script
|
||||
match eval_file(&engine, &script_path.to_string_lossy()) {
|
||||
Ok(result) => {
|
||||
if !result.is_unit() {
|
||||
println!("\nScript returned: {:?}", result);
|
||||
}
|
||||
println!("\nScript executed successfully!");
|
||||
},
|
||||
Err(err) => {
|
||||
eprintln!("\nError running script: {}", err);
|
||||
return Err(Box::new(std::io::Error::new(std::io::ErrorKind::Other, err.to_string())));
|
||||
}
|
||||
}
|
||||
|
||||
// Demonstrate direct Rust interaction with the Rhai-exposed flow functionality
|
||||
println!("\nDirect Rust interaction with Rhai-exposed flow functionality");
|
||||
println!("----------------------------------------------------------");
|
||||
|
||||
// Create a new scope
|
||||
let mut scope = Scope::new();
|
||||
|
||||
// Create a new flow using the Rhai function
|
||||
let result = engine.eval::<Flow>("new_flow(0, \"Direct Rust Flow\")");
|
||||
match result {
|
||||
Ok(mut flow) => {
|
||||
println!("Created flow from Rust: {} (ID: {})", flow.name, flow.get_id());
|
||||
|
||||
// Set flow status using the builder pattern
|
||||
flow = flow.status("active".to_string());
|
||||
println!("Set flow status to: {}", flow.status);
|
||||
|
||||
// Create a new flow step using the Rhai function
|
||||
let result = engine.eval::<FlowStep>("new_flow_step(0, 1)");
|
||||
|
||||
match result {
|
||||
Ok(mut step) => {
|
||||
println!("Created flow step from Rust: Step Order {} (ID: {})",
|
||||
step.step_order, step.get_id());
|
||||
|
||||
// Set step description
|
||||
step = step.description("Direct Rust Step".to_string());
|
||||
println!("Set step description to: {}",
|
||||
step.description.clone().unwrap_or_else(|| "None".to_string()));
|
||||
|
||||
// Create a signature requirement using the Rhai function
|
||||
let result = engine.eval::<SignatureRequirement>(
|
||||
"new_signature_requirement(0, 1, \"Direct Rust Signer\", \"Please sign this document\")"
|
||||
);
|
||||
|
||||
match result {
|
||||
Ok(req) => {
|
||||
println!("Created signature requirement from Rust: Public Key {} (ID: {})",
|
||||
req.public_key, req.get_id());
|
||||
|
||||
// Add the step to the flow using the builder pattern
|
||||
flow = flow.add_step(step);
|
||||
println!("Added step to flow. Flow now has {} steps", flow.steps.len());
|
||||
|
||||
// Save the flow to the database using the Rhai function
|
||||
let save_flow_script = "fn save_it(f) { return db::save_flow(f); }";
|
||||
let save_flow_ast = engine.compile(save_flow_script).unwrap();
|
||||
let result = engine.call_fn::<Flow>(&mut scope, &save_flow_ast, "save_it", (flow,));
|
||||
match result {
|
||||
Ok(saved_flow) => {
|
||||
println!("Saved flow to database with ID: {}", saved_flow.get_id());
|
||||
},
|
||||
Err(err) => eprintln!("Error saving flow: {}", err),
|
||||
}
|
||||
|
||||
// Save the signature requirement to the database using the Rhai function
|
||||
let save_req_script = "fn save_it(r) { return db::save_signature_requirement(r); }";
|
||||
let save_req_ast = engine.compile(save_req_script).unwrap();
|
||||
let result = engine.call_fn::<SignatureRequirement>(&mut scope, &save_req_ast, "save_it", (req,));
|
||||
match result {
|
||||
Ok(saved_req) => {
|
||||
println!("Saved signature requirement to database with ID: {}", saved_req.get_id());
|
||||
},
|
||||
Err(err) => eprintln!("Error saving signature requirement: {}", err),
|
||||
}
|
||||
},
|
||||
Err(err) => eprintln!("Error creating signature requirement: {}", err),
|
||||
}
|
||||
},
|
||||
Err(err) => eprintln!("Error creating flow step: {}", err),
|
||||
}
|
||||
},
|
||||
Err(err) => eprintln!("Error creating flow: {}", err),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
111
src/engine/examples/flow/flow_script.rhai
Normal file
111
src/engine/examples/flow/flow_script.rhai
Normal file
@ -0,0 +1,111 @@
|
||||
// flow_script.rhai
|
||||
// Example Rhai script for working with Flow models
|
||||
|
||||
// Constants for Flow status
|
||||
const STATUS_DRAFT = "draft";
|
||||
const STATUS_ACTIVE = "active";
|
||||
const STATUS_COMPLETED = "completed";
|
||||
const STATUS_CANCELLED = "cancelled";
|
||||
|
||||
// Create a new flow using builder pattern
|
||||
let my_flow = new_flow(0, "flow-123");
|
||||
name(my_flow, "Document Approval Flow");
|
||||
status(my_flow, STATUS_DRAFT);
|
||||
|
||||
print(`Created flow: ${get_flow_name(my_flow)} (ID: ${get_flow_id(my_flow)})`);
|
||||
print(`Status: ${get_flow_status(my_flow)}`);
|
||||
|
||||
// Create flow steps using builder pattern
|
||||
let step1 = new_flow_step(0, 1);
|
||||
description(step1, "Initial review by legal team");
|
||||
status(step1, STATUS_DRAFT);
|
||||
|
||||
let step2 = new_flow_step(0, 2);
|
||||
description(step2, "Approval by department head");
|
||||
status(step2, STATUS_DRAFT);
|
||||
|
||||
let step3 = new_flow_step(0, 3);
|
||||
description(step3, "Final signature by CEO");
|
||||
status(step3, STATUS_DRAFT);
|
||||
|
||||
// Create signature requirements using builder pattern
|
||||
let req1 = new_signature_requirement(0, get_flow_step_id(step1), "legal@example.com", "Please review this document");
|
||||
signed_by(req1, "Legal Team");
|
||||
status(req1, STATUS_DRAFT);
|
||||
|
||||
let req2 = new_signature_requirement(0, get_flow_step_id(step2), "dept@example.com", "Department approval needed");
|
||||
signed_by(req2, "Department Head");
|
||||
status(req2, STATUS_DRAFT);
|
||||
|
||||
let req3 = new_signature_requirement(0, get_flow_step_id(step3), "ceo@example.com", "Final approval required");
|
||||
signed_by(req3, "CEO");
|
||||
status(req3, STATUS_DRAFT);
|
||||
|
||||
print(`Created flow steps with signature requirements`);
|
||||
|
||||
// Add steps to the flow
|
||||
let flow_with_steps = my_flow;
|
||||
add_step(flow_with_steps, step1);
|
||||
add_step(flow_with_steps, step2);
|
||||
add_step(flow_with_steps, step3);
|
||||
|
||||
print(`Added steps to flow. Flow now has ${get_flow_steps(flow_with_steps).len()} steps`);
|
||||
|
||||
// Activate the flow
|
||||
let active_flow = flow_with_steps;
|
||||
status(active_flow, STATUS_ACTIVE);
|
||||
print(`Updated flow status to: ${get_flow_status(active_flow)}`);
|
||||
|
||||
// Save the flow to the database
|
||||
let saved_flow = db::save_flow(active_flow);
|
||||
print(`Flow saved to database with ID: ${get_flow_id(saved_flow)}`);
|
||||
|
||||
// Save signature requirements to the database
|
||||
let saved_req1 = db::save_signature_requirement(req1);
|
||||
let saved_req2 = db::save_signature_requirement(req2);
|
||||
let saved_req3 = db::save_signature_requirement(req3);
|
||||
print(`Signature requirements saved to database with IDs: ${get_signature_requirement_id(saved_req1)}, ${get_signature_requirement_id(saved_req2)}, ${get_signature_requirement_id(saved_req3)}`);
|
||||
|
||||
// Retrieve the flow from the database
|
||||
let retrieved_flow = db::get_flow_by_id(get_flow_id(saved_flow));
|
||||
print(`Retrieved flow: ${get_flow_name(retrieved_flow)}`);
|
||||
print(`It has ${get_flow_steps(retrieved_flow).len()} steps`);
|
||||
|
||||
// Complete the flow
|
||||
let completed_flow = retrieved_flow;
|
||||
status(completed_flow, STATUS_COMPLETED);
|
||||
print(`Updated retrieved flow status to: ${get_flow_status(completed_flow)}`);
|
||||
|
||||
// Save the updated flow
|
||||
db::save_flow(completed_flow);
|
||||
print("Updated flow saved to database");
|
||||
|
||||
// List all flows in the database
|
||||
let all_flows = db::list_flows();
|
||||
print("\nListing all flows in database:");
|
||||
let flow_count = 0;
|
||||
for flow in all_flows {
|
||||
print(` - Flow: ${get_flow_name(flow)} (ID: ${get_flow_id(flow)})`);
|
||||
flow_count += 1;
|
||||
}
|
||||
print(`Total flows: ${flow_count}`);
|
||||
|
||||
// List all signature requirements
|
||||
let all_reqs = db::list_signature_requirements();
|
||||
print("\nListing all signature requirements in database:");
|
||||
let req_count = 0;
|
||||
for req in all_reqs {
|
||||
print(` - Requirement for step ${get_signature_requirement_flow_step_id(req)} (ID: ${get_signature_requirement_id(req)})`);
|
||||
req_count += 1;
|
||||
}
|
||||
print(`Total signature requirements: ${req_count}`);
|
||||
|
||||
// Clean up - delete the flow
|
||||
db::delete_flow(get_flow_id(completed_flow));
|
||||
print(`Deleted flow with ID: ${get_flow_id(completed_flow)}`);
|
||||
|
||||
// Clean up - delete signature requirements
|
||||
db::delete_signature_requirement(get_signature_requirement_id(saved_req1));
|
||||
db::delete_signature_requirement(get_signature_requirement_id(saved_req2));
|
||||
db::delete_signature_requirement(get_signature_requirement_id(saved_req3));
|
||||
print("Deleted all signature requirements");
|
67
src/engine/src/lib.rs
Normal file
67
src/engine/src/lib.rs
Normal file
@ -0,0 +1,67 @@
|
||||
use rhai::{Engine, AST, Scope};
|
||||
use std::sync::Arc;
|
||||
use std::sync::Mutex;
|
||||
use std::collections::HashMap;
|
||||
use heromodels::db::hero::OurDB;
|
||||
|
||||
// Export the mock database module
|
||||
pub mod mock_db;
|
||||
|
||||
pub fn create_heromodels_engine(db: Arc<OurDB>) -> Engine {
|
||||
let mut engine = Engine::new();
|
||||
|
||||
// Configure engine settings
|
||||
engine.set_max_expr_depths(128, 128);
|
||||
engine.set_max_string_size(10 * 1024 * 1024); // 10 MB
|
||||
engine.set_max_array_size(10 * 1024); // 10K elements
|
||||
engine.set_max_map_size(10 * 1024); // 10K elements
|
||||
|
||||
// Register all heromodels Rhai modules
|
||||
register_all_modules(&mut engine, db);
|
||||
|
||||
engine
|
||||
}
|
||||
|
||||
/// Register all heromodels Rhai modules with the engine
|
||||
pub fn register_all_modules(engine: &mut Engine, db: Arc<OurDB>) {
|
||||
// Register the calendar module if the feature is enabled
|
||||
#[cfg(feature = "calendar")]
|
||||
heromodels::models::calendar::register_calendar_rhai_module(engine, db.clone());
|
||||
|
||||
// Register the flow module if the feature is enabled
|
||||
#[cfg(feature = "flow")]
|
||||
heromodels::models::flow::register_flow_rhai_module(engine, db.clone());
|
||||
|
||||
// // Register the finance module if the feature is enabled
|
||||
// #[cfg(feature = "finance")]
|
||||
// heromodels::models::finance::register_finance_rhai_module(engine, db.clone());
|
||||
|
||||
// Register the legal module if the feature is enabled
|
||||
#[cfg(feature = "legal")]
|
||||
heromodels::models::legal::register_legal_rhai_module(engine, db.clone());
|
||||
|
||||
// Register the projects module if the feature is enabled
|
||||
#[cfg(feature = "projects")]
|
||||
heromodels::models::projects::register_projects_rhai_module(engine, db.clone());
|
||||
|
||||
// Register the biz module if the feature is enabled
|
||||
#[cfg(feature = "biz")]
|
||||
heromodels::models::biz::register_biz_rhai_module(engine, db.clone());
|
||||
|
||||
println!("Heromodels Rhai modules registered successfully.");
|
||||
}
|
||||
|
||||
/// Evaluate a Rhai script string
|
||||
pub fn eval_script(engine: &Engine, script: &str) -> Result<rhai::Dynamic, Box<rhai::EvalAltResult>> {
|
||||
engine.eval::<rhai::Dynamic>(script)
|
||||
}
|
||||
|
||||
/// Compile a Rhai script to AST for repeated execution
|
||||
pub fn compile_script(engine: &Engine, script: &str) -> Result<AST, Box<rhai::EvalAltResult>> {
|
||||
Ok(engine.compile(script)?)
|
||||
}
|
||||
|
||||
/// Run a compiled Rhai script AST
|
||||
pub fn run_ast(engine: &Engine, ast: &AST, scope: &mut Scope) -> Result<rhai::Dynamic, Box<rhai::EvalAltResult>> {
|
||||
engine.eval_ast_with_scope(scope, ast)
|
||||
}
|
315
src/engine/src/mock_db.rs
Normal file
315
src/engine/src/mock_db.rs
Normal file
@ -0,0 +1,315 @@
|
||||
use std::sync::Arc;
|
||||
use std::env;
|
||||
use heromodels::db::hero::OurDB;
|
||||
use heromodels::db::{Db, Collection}; // Import both Db and Collection traits
|
||||
use heromodels::models::calendar::{Calendar, Event, Attendee, AttendanceStatus};
|
||||
use heromodels_core::Model; // Import Model trait to use build method
|
||||
use chrono::Utc;
|
||||
use heromodels::models::userexample::User;
|
||||
|
||||
// Import finance models
|
||||
use heromodels::models::finance::account::Account;
|
||||
use heromodels::models::finance::asset::{Asset, AssetType};
|
||||
use heromodels::models::finance::marketplace::{Listing, Bid, ListingStatus, ListingType, BidStatus};
|
||||
|
||||
// Conditionally import other modules based on features
|
||||
#[cfg(feature = "flow")]
|
||||
use heromodels::models::flow::{Flow, FlowStep, SignatureRequirement};
|
||||
|
||||
#[cfg(feature = "legal")]
|
||||
use heromodels::models::legal::{Contract, ContractRevision, ContractSigner, ContractStatus, SignerStatus};
|
||||
|
||||
#[cfg(feature = "projects")]
|
||||
use heromodels::models::projects::{Project, Status as ProjectStatus, Priority, ItemType};
|
||||
|
||||
/// Create a mock in-memory database for examples
|
||||
pub fn create_mock_db() -> Arc<OurDB> {
|
||||
// Create a temporary directory for the database files
|
||||
let temp_dir = env::temp_dir().join("engine_examples");
|
||||
std::fs::create_dir_all(&temp_dir).expect("Failed to create temp directory");
|
||||
|
||||
// Create a new OurDB instance with reset=true to ensure it's clean
|
||||
let db = OurDB::new(temp_dir, true).expect("Failed to create OurDB instance");
|
||||
|
||||
Arc::new(db)
|
||||
}
|
||||
|
||||
/// Seed the mock database with some initial data for all modules
|
||||
pub fn seed_mock_db(db: Arc<OurDB>) {
|
||||
// Seed calendar data
|
||||
seed_calendar_data(db.clone());
|
||||
|
||||
// Seed finance data
|
||||
seed_finance_data(db.clone());
|
||||
|
||||
// Seed flow data if the feature is enabled
|
||||
#[cfg(feature = "flow")]
|
||||
seed_flow_data(db.clone());
|
||||
|
||||
// Seed legal data if the feature is enabled
|
||||
#[cfg(feature = "legal")]
|
||||
seed_legal_data(db.clone());
|
||||
|
||||
// Seed projects data if the feature is enabled
|
||||
#[cfg(feature = "projects")]
|
||||
seed_projects_data(db.clone());
|
||||
|
||||
println!("Mock database seeded with initial data for all enabled modules.");
|
||||
}
|
||||
|
||||
/// Seed the mock database with calendar data
|
||||
fn seed_calendar_data(db: Arc<OurDB>) {
|
||||
// Create a calendar
|
||||
let mut calendar = Calendar::new(None, "Work Calendar".to_string());
|
||||
calendar.description = Some("My work schedule".to_string());
|
||||
|
||||
// Store the calendar in the database
|
||||
let (calendar_id, updated_calendar) = db.collection::<Calendar>()
|
||||
.expect("Failed to get Calendar collection")
|
||||
.set(&calendar)
|
||||
.expect("Failed to store calendar");
|
||||
|
||||
|
||||
// Create an event
|
||||
let now = Utc::now().timestamp();
|
||||
let end_time = now + 3600; // Add 1 hour in seconds
|
||||
|
||||
// Use the builder pattern for Event
|
||||
let event = Event::new()
|
||||
.title("Team Meeting".to_string())
|
||||
.reschedule(now, end_time)
|
||||
.location("Conference Room A".to_string())
|
||||
.description("Weekly sync".to_string())
|
||||
// .add_attendee(Attendee::new(1))
|
||||
// .add_attendee(Attendee::new(2))
|
||||
.build();
|
||||
|
||||
// // Add attendees to the event using the builder pattern
|
||||
// let attendee1 = Attendee::new(1);
|
||||
// let attendee2 = Attendee::new(2);
|
||||
|
||||
// // Add attendees using the builder pattern
|
||||
// event = event.add_attendee(attendee1);
|
||||
// event = event.add_attendee(attendee2);
|
||||
|
||||
// Call build and capture the returned value
|
||||
// let event = event.build();
|
||||
|
||||
// Store the event in the database first to get its ID
|
||||
let (event_id, updated_event) = db.collection()
|
||||
.expect("Failed to get Event collection")
|
||||
.set(&event)
|
||||
.expect("Failed to store event");
|
||||
|
||||
// Add the event ID to the calendar
|
||||
calendar = calendar.add_event(event_id as i64);
|
||||
|
||||
// Store the calendar in the database
|
||||
let (calendar_id, updated_calendar) = db.collection::<Calendar>()
|
||||
.expect("Failed to get Calendar collection")
|
||||
.set(&calendar)
|
||||
.expect("Failed to store calendar");
|
||||
|
||||
println!("Mock database seeded with calendar data:");
|
||||
println!(" - Added calendar: {} (ID: {})", updated_calendar.name, updated_calendar.base_data.id);
|
||||
println!(" - Added event: {} (ID: {})", updated_event.title, updated_event.base_data.id);
|
||||
}
|
||||
|
||||
/// Seed the mock database with flow data
|
||||
#[cfg(feature = "flow")]
|
||||
fn seed_flow_data(db: Arc<OurDB>) {
|
||||
// Create a flow
|
||||
let mut flow = Flow::new(0, "Document Approval".to_string());
|
||||
|
||||
// Set flow properties using the builder pattern
|
||||
flow = flow.status("draft".to_string());
|
||||
flow = flow.name("Document Approval Flow".to_string());
|
||||
|
||||
// Create flow steps
|
||||
let mut step1 = FlowStep::new(0, 1);
|
||||
step1 = step1.description("Initial review by legal team".to_string());
|
||||
step1 = step1.status("pending".to_string());
|
||||
|
||||
let mut step2 = FlowStep::new(0, 2);
|
||||
step2 = step2.description("Approval by department head".to_string());
|
||||
step2 = step2.status("pending".to_string());
|
||||
|
||||
// Add signature requirements
|
||||
let mut req1 = SignatureRequirement::new(0, 1, "Legal Team".to_string(), "Please review this document".to_string());
|
||||
let mut req2 = SignatureRequirement::new(0, 2, "Department Head".to_string(), "Please approve this document".to_string());
|
||||
|
||||
// Add steps to flow
|
||||
flow = flow.add_step(step1);
|
||||
flow = flow.add_step(step2);
|
||||
|
||||
// Store in the database
|
||||
let (_, updated_flow) = db.collection::<Flow>()
|
||||
.expect("Failed to get Flow collection")
|
||||
.set(&flow)
|
||||
.expect("Failed to store flow");
|
||||
|
||||
// Store signature requirements in the database
|
||||
let (_, updated_req1) = db.collection::<SignatureRequirement>()
|
||||
.expect("Failed to get SignatureRequirement collection")
|
||||
.set(&req1)
|
||||
.expect("Failed to store signature requirement");
|
||||
|
||||
let (_, updated_req2) = db.collection::<SignatureRequirement>()
|
||||
.expect("Failed to get SignatureRequirement collection")
|
||||
.set(&req2)
|
||||
.expect("Failed to store signature requirement");
|
||||
|
||||
println!("Mock database seeded with flow data:");
|
||||
println!(" - Added flow: {} (ID: {})", updated_flow.name, updated_flow.base_data.id);
|
||||
println!(" - Added {} steps", updated_flow.steps.len());
|
||||
println!(" - Added signature requirements with IDs: {} and {}",
|
||||
updated_req1.base_data.id, updated_req2.base_data.id);
|
||||
}
|
||||
|
||||
/// Seed the mock database with legal data
|
||||
#[cfg(feature = "legal")]
|
||||
fn seed_legal_data(db: Arc<OurDB>) {
|
||||
// Create a contract
|
||||
let mut contract = Contract::new(None, "Service Agreement".to_string());
|
||||
contract.description = Some("Agreement for software development services".to_string());
|
||||
contract.status = ContractStatus::Draft;
|
||||
|
||||
// Create a revision
|
||||
let revision = ContractRevision::new(
|
||||
None,
|
||||
"Initial draft".to_string(),
|
||||
"https://example.com/contract/v1".to_string(),
|
||||
);
|
||||
|
||||
// Create signers
|
||||
let signer1 = ContractSigner::new(None, 1, "Client".to_string());
|
||||
let signer2 = ContractSigner::new(None, 2, "Provider".to_string());
|
||||
|
||||
// Add revision and signers to contract
|
||||
contract.add_revision(revision);
|
||||
contract.add_signer(signer1);
|
||||
contract.add_signer(signer2);
|
||||
|
||||
// Store in the database
|
||||
let (_, updated_contract) = db.collection::<Contract>()
|
||||
.expect("Failed to get Contract collection")
|
||||
.set(&contract)
|
||||
.expect("Failed to store contract");
|
||||
|
||||
println!("Mock database seeded with legal data:");
|
||||
println!(" - Added contract: {} (ID: {})", updated_contract.name, updated_contract.base_data.id);
|
||||
println!(" - Added {} revisions and {} signers",
|
||||
updated_contract.revisions.len(),
|
||||
updated_contract.signers.len());
|
||||
}
|
||||
|
||||
/// Seed the mock database with projects data
|
||||
#[cfg(feature = "projects")]
|
||||
fn seed_projects_data(db: Arc<OurDB>) {
|
||||
// Create a project
|
||||
let mut project = Project::new(None, "Website Redesign".to_string());
|
||||
project.description = Some("Redesign the company website".to_string());
|
||||
project.status = ProjectStatus::InProgress;
|
||||
project.priority = Priority::High;
|
||||
|
||||
// Add members and tags
|
||||
project.add_member_id(1);
|
||||
project.add_member_id(2);
|
||||
project.add_tag("design".to_string());
|
||||
project.add_tag("web".to_string());
|
||||
|
||||
// Store in the database
|
||||
let (_, updated_project) = db.collection::<Project>()
|
||||
.expect("Failed to get Project collection")
|
||||
.set(&project)
|
||||
.expect("Failed to store project");
|
||||
|
||||
println!("Mock database seeded with projects data:");
|
||||
println!(" - Added project: {} (ID: {})", updated_project.name, updated_project.base_data.id);
|
||||
println!(" - Status: {}, Priority: {}", updated_project.status, updated_project.priority);
|
||||
println!(" - Added {} members and {} tags",
|
||||
updated_project.member_ids.len(),
|
||||
updated_project.tags.len());
|
||||
}
|
||||
/// Seed the mock database with finance data
|
||||
fn seed_finance_data(db: Arc<OurDB>) {
|
||||
// Create a user account
|
||||
let mut account = Account::new()
|
||||
.name("Demo Account")
|
||||
.user_id(1)
|
||||
.description("Demo trading account")
|
||||
.ledger("ethereum")
|
||||
.address("0x1234567890abcdef1234567890abcdef12345678")
|
||||
.pubkey("0xabcdef1234567890abcdef1234567890abcdef12");
|
||||
|
||||
// Store the account in the database
|
||||
let (account_id, updated_account) = db.collection::<Account>()
|
||||
.expect("Failed to get Account collection")
|
||||
.set(&account)
|
||||
.expect("Failed to store account");
|
||||
|
||||
// Create an ERC20 token asset
|
||||
let token_asset = Asset::new()
|
||||
.name("HERO Token")
|
||||
.description("Herocode governance token")
|
||||
.amount(1000.0)
|
||||
.address("0x9876543210abcdef9876543210abcdef98765432")
|
||||
.asset_type(AssetType::Erc20)
|
||||
.decimals(18);
|
||||
|
||||
// Store the token asset in the database
|
||||
let (token_id, updated_token) = db.collection::<Asset>()
|
||||
.expect("Failed to get Asset collection")
|
||||
.set(&token_asset)
|
||||
.expect("Failed to store token asset");
|
||||
|
||||
// Create an NFT asset
|
||||
let nft_asset = Asset::new()
|
||||
.name("Herocode #1")
|
||||
.description("Unique digital collectible")
|
||||
.amount(1.0)
|
||||
.address("0xabcdef1234567890abcdef1234567890abcdef12")
|
||||
.asset_type(AssetType::Erc721)
|
||||
.decimals(0);
|
||||
|
||||
// Store the NFT asset in the database
|
||||
let (nft_id, updated_nft) = db.collection::<Asset>()
|
||||
.expect("Failed to get Asset collection")
|
||||
.set(&nft_asset)
|
||||
.expect("Failed to store NFT asset");
|
||||
|
||||
// Add assets to the account
|
||||
account = updated_account.add_asset(token_id);
|
||||
account = account.add_asset(nft_id);
|
||||
|
||||
// Update the account in the database
|
||||
let (_, updated_account) = db.collection::<Account>()
|
||||
.expect("Failed to get Account collection")
|
||||
.set(&account)
|
||||
.expect("Failed to store updated account");
|
||||
|
||||
// Create a listing for the NFT
|
||||
let listing = Listing::new()
|
||||
.seller_id(account_id)
|
||||
.asset_id(nft_id)
|
||||
.price(0.5)
|
||||
.currency("ETH")
|
||||
.listing_type(ListingType::Auction)
|
||||
.title("Rare Herocode NFT".to_string())
|
||||
.description("One of a kind digital collectible".to_string())
|
||||
.image_url(Some("hcttps://example.com/nft/1.png".to_string()))
|
||||
.add_tag("rare".to_string())
|
||||
.add_tag("collectible".to_string());
|
||||
|
||||
// Store the listing in the database
|
||||
let (listing_id, updated_listing) = db.collection::<Listing>()
|
||||
.expect("Failed to get Listing collection")
|
||||
.set(&listing)
|
||||
.expect("Failed to store listing");
|
||||
|
||||
println!("Mock database seeded with finance data:");
|
||||
println!(" - Added account: {} (ID: {})", updated_account.name, updated_account.base_data.id);
|
||||
println!(" - Added token asset: {} (ID: {})", updated_token.name, updated_token.base_data.id);
|
||||
println!(" - Added NFT asset: {} (ID: {})", updated_nft.name, updated_nft.base_data.id);
|
||||
println!(" - Added listing: {} (ID: {})", updated_listing.title, updated_listing.base_data.id);
|
||||
}
|
82
src/engine/src/mock_db.rs.part
Normal file
82
src/engine/src/mock_db.rs.part
Normal file
@ -0,0 +1,82 @@
|
||||
/// Seed the mock database with finance data
|
||||
fn seed_finance_data(db: Arc<OurDB>) {
|
||||
// Create a user account
|
||||
let mut account = Account::new()
|
||||
.name("Demo Account")
|
||||
.user_id(1)
|
||||
.description("Demo trading account")
|
||||
.ledger("ethereum")
|
||||
.address("0x1234567890abcdef1234567890abcdef12345678")
|
||||
.pubkey("0xabcdef1234567890abcdef1234567890abcdef12");
|
||||
|
||||
// Store the account in the database
|
||||
let (account_id, updated_account) = db.collection::<Account>()
|
||||
.expect("Failed to get Account collection")
|
||||
.set(&account)
|
||||
.expect("Failed to store account");
|
||||
|
||||
// Create an ERC20 token asset
|
||||
let token_asset = Asset::new()
|
||||
.name("HERO Token")
|
||||
.description("Herocode governance token")
|
||||
.amount(1000.0)
|
||||
.address("0x9876543210abcdef9876543210abcdef98765432")
|
||||
.asset_type(AssetType::Erc20)
|
||||
.decimals(18);
|
||||
|
||||
// Store the token asset in the database
|
||||
let (token_id, updated_token) = db.collection::<Asset>()
|
||||
.expect("Failed to get Asset collection")
|
||||
.set(&token_asset)
|
||||
.expect("Failed to store token asset");
|
||||
|
||||
// Create an NFT asset
|
||||
let nft_asset = Asset::new()
|
||||
.name("Herocode #1")
|
||||
.description("Unique digital collectible")
|
||||
.amount(1.0)
|
||||
.address("0xabcdef1234567890abcdef1234567890abcdef12")
|
||||
.asset_type(AssetType::Erc721)
|
||||
.decimals(0);
|
||||
|
||||
// Store the NFT asset in the database
|
||||
let (nft_id, updated_nft) = db.collection::<Asset>()
|
||||
.expect("Failed to get Asset collection")
|
||||
.set(&nft_asset)
|
||||
.expect("Failed to store NFT asset");
|
||||
|
||||
// Add assets to the account
|
||||
account = updated_account.add_asset(token_id);
|
||||
account = account.add_asset(nft_id);
|
||||
|
||||
// Update the account in the database
|
||||
let (_, updated_account) = db.collection::<Account>()
|
||||
.expect("Failed to get Account collection")
|
||||
.set(&account)
|
||||
.expect("Failed to store updated account");
|
||||
|
||||
// Create a listing for the NFT
|
||||
let listing = Listing::new()
|
||||
.seller_id(account_id)
|
||||
.asset_id(nft_id)
|
||||
.price(0.5)
|
||||
.currency("ETH")
|
||||
.listing_type(ListingType::Auction)
|
||||
.title(Some("Rare Herocode NFT".to_string()))
|
||||
.description(Some("One of a kind digital collectible".to_string()))
|
||||
.image_url(Some("https://example.com/nft/1.png".to_string()))
|
||||
.add_tag("rare".to_string())
|
||||
.add_tag("collectible".to_string());
|
||||
|
||||
// Store the listing in the database
|
||||
let (listing_id, updated_listing) = db.collection::<Listing>()
|
||||
.expect("Failed to get Listing collection")
|
||||
.set(&listing)
|
||||
.expect("Failed to store listing");
|
||||
|
||||
println!("Mock database seeded with finance data:");
|
||||
println!(" - Added account: {} (ID: {})", updated_account.name, updated_account.base_data.id);
|
||||
println!(" - Added token asset: {} (ID: {})", updated_token.name, updated_token.base_data.id);
|
||||
println!(" - Added NFT asset: {} (ID: {})", updated_nft.name, updated_nft.base_data.id);
|
||||
println!(" - Added listing: {} (ID: {})", updated_listing.title.unwrap_or_default(), updated_listing.base_data.id);
|
||||
}
|
1
src/worker/.gitignore
vendored
Normal file
1
src/worker/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
/target
|
1423
src/worker/Cargo.lock
generated
Normal file
1423
src/worker/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
27
src/worker/Cargo.toml
Normal file
27
src/worker/Cargo.toml
Normal file
@ -0,0 +1,27 @@
|
||||
[package]
|
||||
name = "worker"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
name = "worker_lib" # Can be different from package name, or same
|
||||
path = "src/lib.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "worker"
|
||||
path = "src/bin/worker.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.18.0", features = ["sync", "decimal"] } # Added "decimal" for broader script support
|
||||
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"] }
|
||||
rhai_client = { path = "../client" }
|
86
src/worker/README.md
Normal file
86
src/worker/README.md
Normal file
@ -0,0 +1,86 @@
|
||||
# Rhai Worker
|
||||
|
||||
The `rhai_worker` crate implements a worker service that listens for Rhai script execution tasks from a Redis queue, executes them using the Rhai scripting engine, and posts results back to Redis. It is designed to work in conjunction with the `rhai_client` crate.
|
||||
|
||||
## Features
|
||||
|
||||
- **Redis Queue Consumption**: Listens to one or more specified Redis lists (acting as task queues) for incoming task IDs.
|
||||
- **Rhai Script Execution**: Executes Rhai scripts retrieved based on task IDs.
|
||||
- **Task State Management**: Updates task status (`processing`, `completed`, `error`) and stores results (output or error messages) in Redis hashes.
|
||||
- **Configurable**:
|
||||
- Redis URL can be specified via command-line arguments.
|
||||
- Listens to specific "circles" (task queues) provided as command-line arguments.
|
||||
- **Asynchronous Operations**: Built with `tokio` for non-blocking Redis communication and script processing.
|
||||
- **Graceful Error Handling**: Captures errors during script execution and stores them for the client.
|
||||
|
||||
## Core Components
|
||||
|
||||
- **`worker_lib` (Library Crate)**:
|
||||
- **`Args`**: A struct (using `clap`) for parsing command-line arguments like Redis URL and target circle names.
|
||||
- **`run_worker_loop(engine: Engine, args: Args)`**: The main asynchronous function that:
|
||||
- Connects to Redis.
|
||||
- Continuously polls specified Redis queues (e.g., `rhai_tasks:<circle_name>`) using `BLPOP`.
|
||||
- Upon receiving a `task_id`:
|
||||
- Fetches task details (including the script) from a Redis hash (e.g., `rhai_task_details:<task_id>`).
|
||||
- Updates the task status to "processing".
|
||||
- Executes the Rhai script using the provided `rhai::Engine`.
|
||||
- Updates the task status to "completed" with the script's output or "error" with the error message.
|
||||
- **`update_task_status_in_redis(...)`**: A helper function to update task details in Redis.
|
||||
- **`worker` (Binary Crate - `cmd/worker.rs`)**:
|
||||
- The main executable entry point.
|
||||
- Parses command-line arguments.
|
||||
- Initializes a default `rhai::Engine`.
|
||||
- Invokes `run_worker_loop` from `worker_lib`.
|
||||
|
||||
## How It Works
|
||||
|
||||
1. The worker executable is launched, typically with command-line arguments specifying the Redis URL and the "circle(s)" (queues) to monitor.
|
||||
```bash
|
||||
./worker --redis-url redis://your-redis-host/ --circles circle_A circle_B
|
||||
```
|
||||
2. The `run_worker_loop` connects to Redis and starts listening to the designated task queues (e.g., `rhai_tasks:circle_a`, `rhai_tasks:circle_b`).
|
||||
3. When a `rhai_client` submits a task, it pushes a `task_id` to one of these queues and stores task details (script, initial status "pending") in a Redis hash.
|
||||
4. The worker's `BLPOP` command picks up a `task_id` from a queue.
|
||||
5. The worker retrieves the script from the corresponding `rhai_task_details:<task_id>` hash in Redis.
|
||||
6. It updates the task's status to "processing" in the Redis hash.
|
||||
7. The Rhai script is executed.
|
||||
8. After execution:
|
||||
- If successful, the status is updated to "completed", and the output is stored in the Redis hash.
|
||||
- If an error occurs, the status is updated to "error", and the error message is stored.
|
||||
9. The worker then goes back to listening for the next task.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- A running Redis instance accessible by the worker.
|
||||
- The `rhai_client` (or another system) populating the Redis queues and task detail hashes.
|
||||
|
||||
## Building and Running
|
||||
|
||||
1. **Build the worker:**
|
||||
```bash
|
||||
# From the root of the rhailib project or within src/worker
|
||||
cargo build # Or cargo build --release for an optimized version
|
||||
```
|
||||
The binary will typically be found in `target/debug/worker` or `target/release/worker`.
|
||||
|
||||
2. **Run the worker:**
|
||||
```bash
|
||||
# Example:
|
||||
./target/debug/worker --redis-url redis://127.0.0.1/ --circles my_circle_1 my_circle_2
|
||||
```
|
||||
Replace `redis://127.0.0.1/` with your Redis server's URL and `my_circle_1 my_circle_2` with the names of the task queues you want this worker instance to process.
|
||||
|
||||
You can run multiple instances of the worker, potentially listening to the same or different circles, to scale out processing.
|
||||
|
||||
## Dependencies
|
||||
|
||||
Key dependencies include:
|
||||
- `redis`: For asynchronous Redis communication.
|
||||
- `rhai`: The Rhai script engine.
|
||||
- `clap`: For command-line argument parsing.
|
||||
- `tokio`: For the asynchronous runtime.
|
||||
- `log`, `env_logger`: For logging.
|
||||
- `rhai_client`: For shared definitions (potentially, though direct usage is minimal in current `lib.rs`).
|
||||
|
||||
## Note on Binary Path
|
||||
The `Cargo.toml` for the worker specifies the binary path as `src/bin/worker.rs`. However, the actual file is located at `src/cmd/worker.rs`. This README assumes the latter is the correct and current location. If `Cargo.toml` is updated, this note might become obsolete.
|
18
src/worker/cmd/worker.rs
Normal file
18
src/worker/cmd/worker.rs
Normal file
@ -0,0 +1,18 @@
|
||||
use rhai::Engine;
|
||||
use worker_lib::{run_worker_loop, Args}; // Use the library name defined in Cargo.toml
|
||||
use clap::Parser; // Required for Args::parse() to be in scope
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
env_logger::init();
|
||||
|
||||
let args = Args::parse();
|
||||
|
||||
log::info!("Rhai Worker (binary) starting with default engine.");
|
||||
|
||||
let engine = Engine::new();
|
||||
// If specific default configurations are needed for the binary's engine, set them up here.
|
||||
// For example: engine.set_max_operations(1_000_000);
|
||||
|
||||
run_worker_loop(engine, args).await
|
||||
}
|
144
src/worker/src/lib.rs
Normal file
144
src/worker/src/lib.rs
Normal file
@ -0,0 +1,144 @@
|
||||
use chrono::Utc;
|
||||
use clap::Parser;
|
||||
use log::{debug, error, info}; // Removed warn as it wasn't used in the loop
|
||||
use redis::AsyncCommands;
|
||||
use rhai::{Engine, Scope}; // EvalAltResult is not directly returned by the loop
|
||||
use std::collections::HashMap; // For hgetall result
|
||||
|
||||
// Re-export RhaiTaskDetails from rhai_client if needed by examples,
|
||||
// or examples can depend on rhai_client directly.
|
||||
// For now, the worker logic itself just interacts with the hash fields.
|
||||
|
||||
const REDIS_TASK_DETAILS_PREFIX: &str = "rhai_task_details:";
|
||||
const REDIS_QUEUE_PREFIX: &str = "rhai_tasks:";
|
||||
const BLPOP_TIMEOUT_SECONDS: usize = 5;
|
||||
|
||||
#[derive(Parser, Debug, Clone)] // Added Clone for potential use in examples
|
||||
#[clap(author, version, about, long_about = None)]
|
||||
pub struct Args {
|
||||
#[clap(long, value_parser, default_value = "redis://127.0.0.1/")]
|
||||
pub redis_url: String,
|
||||
|
||||
#[clap(short, long, value_parser, required = true, num_args = 1..)]
|
||||
pub circles: Vec<String>,
|
||||
}
|
||||
|
||||
// This function updates specific fields in the Redis hash.
|
||||
// It doesn't need to know the full RhaiTaskDetails struct, only the field names.
|
||||
async fn update_task_status_in_redis(
|
||||
conn: &mut redis::aio::MultiplexedConnection,
|
||||
task_id: &str,
|
||||
status: &str,
|
||||
output: Option<String>,
|
||||
error_msg: Option<String>,
|
||||
) -> redis::RedisResult<()> {
|
||||
let task_key = format!("{}{}", REDIS_TASK_DETAILS_PREFIX, task_id);
|
||||
let mut updates: Vec<(&str, String)> = vec![
|
||||
("status", status.to_string()),
|
||||
("updatedAt", Utc::now().to_rfc3339()), // Ensure this field name matches what rhai_client sets/expects
|
||||
];
|
||||
if let Some(out) = output {
|
||||
updates.push(("output", out)); // Ensure this field name matches
|
||||
}
|
||||
if let Some(err) = error_msg {
|
||||
updates.push(("error", err)); // Ensure this field name matches
|
||||
}
|
||||
debug!("Updating task {} in Redis with status: {}, updates: {:?}", task_id, status, updates);
|
||||
conn.hset_multiple::<_, _, _, ()>(&task_key, &updates).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn run_worker_loop(engine: Engine, args: Args) -> Result<(), Box<dyn std::error::Error>> {
|
||||
info!("Rhai Worker Loop starting. Connecting to Redis at {}", args.redis_url);
|
||||
info!("Worker Loop will listen for tasks for circles: {:?}", args.circles);
|
||||
|
||||
let redis_client = redis::Client::open(args.redis_url.as_str())?;
|
||||
let mut redis_conn = redis_client.get_multiplexed_async_connection().await?;
|
||||
info!("Worker Loop successfully connected to Redis.");
|
||||
|
||||
let queue_keys: Vec<String> = args
|
||||
.circles
|
||||
.iter()
|
||||
.map(|name| format!("{}{}", REDIS_QUEUE_PREFIX, name.replace(" ", "_").to_lowercase()))
|
||||
.collect();
|
||||
|
||||
info!("Worker Loop listening on Redis queues: {:?}", queue_keys);
|
||||
|
||||
loop {
|
||||
let response: Option<(String, String)> = redis_conn
|
||||
.blpop(&queue_keys, BLPOP_TIMEOUT_SECONDS as f64)
|
||||
.await?;
|
||||
|
||||
if let Some((queue_name, task_id)) = response {
|
||||
info!("Worker Loop received task_id: {} from queue: {}", task_id, queue_name);
|
||||
|
||||
let task_key = format!("{}{}", REDIS_TASK_DETAILS_PREFIX, task_id);
|
||||
|
||||
let task_details_map: Result<HashMap<String, String>, _> =
|
||||
redis_conn.hgetall(&task_key).await;
|
||||
|
||||
match task_details_map {
|
||||
Ok(details_map) => {
|
||||
let script_content_opt = details_map.get("script").cloned();
|
||||
|
||||
if let Some(script_content) = script_content_opt {
|
||||
info!("Worker Loop processing task_id: {}. Script: {:.50}...", task_id, script_content);
|
||||
update_task_status_in_redis(&mut redis_conn, &task_id, "processing", None, None).await?;
|
||||
|
||||
let mut scope = Scope::new();
|
||||
// Examples can show how to pre-populate the scope via the engine or here
|
||||
|
||||
match engine.eval_with_scope::<rhai::Dynamic>(&mut scope, &script_content) {
|
||||
Ok(result) => {
|
||||
let output_str = format!("{:?}", result);
|
||||
info!("Worker Loop task {} completed. Output: {}", task_id, output_str);
|
||||
update_task_status_in_redis(
|
||||
&mut redis_conn,
|
||||
&task_id,
|
||||
"completed",
|
||||
Some(output_str),
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
Err(e) => {
|
||||
let error_str = format!("{:?}", *e); // Dereference EvalAltResult
|
||||
error!("Worker Loop task {} failed. Error: {}", task_id, error_str);
|
||||
update_task_status_in_redis(
|
||||
&mut redis_conn,
|
||||
&task_id,
|
||||
"error",
|
||||
None,
|
||||
Some(error_str),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
error!(
|
||||
"Worker Loop: Could not find script content for task_id: {} in Redis hash: {}",
|
||||
task_id, task_key
|
||||
);
|
||||
update_task_status_in_redis(
|
||||
&mut redis_conn,
|
||||
&task_id,
|
||||
"error",
|
||||
None,
|
||||
Some("Script content not found in Redis hash".to_string()),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
error!(
|
||||
"Worker Loop: Failed to fetch details for task_id: {} from Redis. Error: {:?}",
|
||||
task_id, e
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
debug!("Worker Loop: BLPOP timed out. No new tasks.");
|
||||
}
|
||||
}
|
||||
// Loop is infinite, Ok(()) is effectively unreachable unless loop breaks
|
||||
}
|
Loading…
Reference in New Issue
Block a user