move rhailib to herolib
This commit is contained in:
17
rhailib/src/flow/Cargo.toml
Normal file
17
rhailib/src/flow/Cargo.toml
Normal file
@@ -0,0 +1,17 @@
|
||||
[package]
|
||||
name = "flow"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
description = "Simple flow manager for Rhai scripts"
|
||||
|
||||
[dependencies]
|
||||
rhai = { version = "=1.21.0", features = ["std", "sync"] }
|
||||
rhai_dispatcher = { path = "../dispatcher" }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
redis = { version = "0.23", features = ["tokio-comp"] }
|
||||
uuid = { version = "1.0", features = ["v4"] }
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3"
|
110
rhailib/src/flow/README.md
Normal file
110
rhailib/src/flow/README.md
Normal file
@@ -0,0 +1,110 @@
|
||||
# Flow Manager
|
||||
|
||||
A simple, generic flow manager for Rhai scripts with builder pattern API and non-blocking execution.
|
||||
|
||||
## Features
|
||||
|
||||
- **Builder Pattern API**: Fluent interface for creating steps and flows
|
||||
- **Non-blocking Execution**: Uses `tokio::spawn` for async step execution
|
||||
- **Simple State Management**: Redis-based state tracking
|
||||
- **Retry Logic**: Configurable timeouts and retry attempts
|
||||
- **Mock API Support**: Built-in mock API for testing different scenarios
|
||||
- **RhaiDispatcher Integration**: Seamless integration with existing Rhai execution system
|
||||
|
||||
## Quick Start
|
||||
|
||||
```rust
|
||||
use flow::{new_step, new_flow, FlowExecutor};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
// Create executor
|
||||
let executor = FlowExecutor::new("redis://127.0.0.1/").await?;
|
||||
|
||||
// Build steps using fluent API
|
||||
let step1 = new_step("stripe_config")
|
||||
.script("stripe_config_script")
|
||||
.timeout(5)
|
||||
.retries(2)
|
||||
.build();
|
||||
|
||||
let step2 = new_step("stripe_config_confirm")
|
||||
.script("script that looks up stripe config confirmation in db")
|
||||
.timeout(5)
|
||||
.build();
|
||||
|
||||
let step3 = new_step("create_product")
|
||||
.script("create_product_script")
|
||||
.timeout(10)
|
||||
.retries(1)
|
||||
.build();
|
||||
|
||||
// Build flow using fluent API
|
||||
let flow = new_flow("stripe_payment_request")
|
||||
.add_step(step1)
|
||||
.add_step(step2)
|
||||
.add_step(step3)
|
||||
.build();
|
||||
|
||||
// Execute flow (non-blocking)
|
||||
let result = executor.execute_flow(flow).await?;
|
||||
println!("Flow started: {}", result);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
### Core Components
|
||||
|
||||
- **Types** (`types.rs`): Core data structures (Flow, Step, Status enums)
|
||||
- **Builder** (`builder.rs`): Fluent API for constructing flows and steps
|
||||
- **State** (`state.rs`): Simple Redis-based state management
|
||||
- **Executor** (`executor.rs`): Non-blocking flow execution engine
|
||||
- **Mock API** (`mock_api.rs`): Testing utilities for different response scenarios
|
||||
|
||||
### State Management
|
||||
|
||||
The system tracks minimal state:
|
||||
|
||||
**Flow State:**
|
||||
- `flow_id: String` - unique identifier
|
||||
- `status: FlowStatus` (Created, Running, Completed, Failed)
|
||||
- `current_step: Option<String>` - currently executing step
|
||||
- `completed_steps: Vec<String>` - list of finished steps
|
||||
|
||||
**Step State:**
|
||||
- `step_id: String` - unique identifier
|
||||
- `status: StepStatus` (Pending, Running, Completed, Failed)
|
||||
- `attempt_count: u32` - for retry logic
|
||||
- `output: Option<String>` - result from script execution
|
||||
|
||||
**Storage:**
|
||||
- Redis key-value pairs: `flow:{flow_id}` and `step:{flow_id}:{step_id}`
|
||||
|
||||
## Examples
|
||||
|
||||
Run the example:
|
||||
|
||||
```bash
|
||||
cd ../rhailib/src/flow
|
||||
cargo run --example stripe_flow_example
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
```bash
|
||||
cargo test
|
||||
```
|
||||
|
||||
Note: Some tests require Redis to be running. Set `SKIP_REDIS_TESTS=1` to skip Redis-dependent tests.
|
||||
|
||||
## Integration
|
||||
|
||||
The flow manager integrates with:
|
||||
- **RhaiDispatcher**: For executing Rhai scripts
|
||||
- **Redis**: For state persistence
|
||||
- **tokio**: For non-blocking async execution
|
||||
|
||||
This provides a simple, reliable foundation for orchestrating complex workflows while maintaining the non-blocking execution pattern established in the payment system.
|
90
rhailib/src/flow/examples/stripe_flow_example.rs
Normal file
90
rhailib/src/flow/examples/stripe_flow_example.rs
Normal file
@@ -0,0 +1,90 @@
|
||||
//! Example demonstrating the flow manager with mock Stripe API calls
|
||||
|
||||
use flow::{new_step, new_flow, FlowExecutor};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
println!("=== Flow Manager Example ===");
|
||||
println!("Demonstrating the builder pattern API with mock Stripe workflow\n");
|
||||
|
||||
// Create the flow executor
|
||||
let executor = FlowExecutor::new("redis://127.0.0.1/").await?;
|
||||
|
||||
// Build steps using the fluent API
|
||||
let step1 = new_step("stripe_config")
|
||||
.script("mock_api_call stripe_config")
|
||||
.timeout(5)
|
||||
.retries(2)
|
||||
.build();
|
||||
|
||||
let step2 = new_step("stripe_config_confirm")
|
||||
.script("mock_api_call create_product")
|
||||
.timeout(5)
|
||||
.retries(1)
|
||||
.build();
|
||||
|
||||
let step3 = new_step("create_product")
|
||||
.script("mock_api_call create_product")
|
||||
.timeout(10)
|
||||
.retries(1)
|
||||
.build();
|
||||
|
||||
// Build flow using the fluent API
|
||||
let flow = new_flow("stripe_payment_request")
|
||||
.add_step(step1)
|
||||
.add_step(step2)
|
||||
.add_step(step3)
|
||||
.build();
|
||||
|
||||
println!("Created flow: {}", flow.name);
|
||||
println!("Flow ID: {}", flow.id);
|
||||
println!("Number of steps: {}", flow.steps.len());
|
||||
|
||||
for (i, step) in flow.steps.iter().enumerate() {
|
||||
println!(" Step {}: {} (timeout: {}s, retries: {})",
|
||||
i + 1, step.name, step.timeout_seconds, step.max_retries);
|
||||
}
|
||||
|
||||
// Execute the flow (non-blocking)
|
||||
println!("\n🚀 Starting flow execution...");
|
||||
let result = executor.execute_flow(flow.clone()).await?;
|
||||
println!("✅ {}", result);
|
||||
|
||||
// Monitor flow progress
|
||||
println!("\n📊 Monitoring flow progress...");
|
||||
for i in 0..10 {
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
|
||||
|
||||
if let Ok(Some(flow_state)) = executor.get_flow_status(&flow.id).await {
|
||||
println!(" Status: {:?}, Current step: {:?}, Completed: {}/{}",
|
||||
flow_state.status,
|
||||
flow_state.current_step,
|
||||
flow_state.completed_steps.len(),
|
||||
flow.steps.len());
|
||||
|
||||
if matches!(flow_state.status, flow::FlowStatus::Completed | flow::FlowStatus::Failed) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check final status
|
||||
if let Ok(Some(final_state)) = executor.get_flow_status(&flow.id).await {
|
||||
println!("\n🎯 Final flow status: {:?}", final_state.status);
|
||||
println!("Completed steps: {:?}", final_state.completed_steps);
|
||||
|
||||
// Check individual step results
|
||||
for step in &flow.steps {
|
||||
if let Ok(Some(step_state)) = executor.get_step_status(&flow.id, &step.id).await {
|
||||
println!(" Step '{}': {:?} (attempts: {})",
|
||||
step.name, step_state.status, step_state.attempt_count);
|
||||
if let Some(output) = &step_state.output {
|
||||
println!(" Output: {}", output);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
println!("\n✨ Flow execution demonstration completed!");
|
||||
Ok(())
|
||||
}
|
108
rhailib/src/flow/src/builder.rs
Normal file
108
rhailib/src/flow/src/builder.rs
Normal file
@@ -0,0 +1,108 @@
|
||||
//! Builder patterns for steps and flows
|
||||
|
||||
use crate::types::{Step, Flow};
|
||||
|
||||
/// Builder for creating steps with fluent API
|
||||
pub struct StepBuilder {
|
||||
step: Step,
|
||||
}
|
||||
|
||||
impl StepBuilder {
|
||||
pub fn new(name: &str) -> Self {
|
||||
Self {
|
||||
step: Step::new(name),
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the script content for this step
|
||||
pub fn script(mut self, script: &str) -> Self {
|
||||
self.step.script = script.to_string();
|
||||
self
|
||||
}
|
||||
|
||||
/// Set timeout in seconds
|
||||
pub fn timeout(mut self, seconds: u64) -> Self {
|
||||
self.step.timeout_seconds = seconds;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set maximum retry attempts
|
||||
pub fn retries(mut self, count: u32) -> Self {
|
||||
self.step.max_retries = count;
|
||||
self
|
||||
}
|
||||
|
||||
/// Build the final step
|
||||
pub fn build(self) -> Step {
|
||||
self.step
|
||||
}
|
||||
}
|
||||
|
||||
/// Builder for creating flows with fluent API
|
||||
pub struct FlowBuilder {
|
||||
flow: Flow,
|
||||
}
|
||||
|
||||
impl FlowBuilder {
|
||||
pub fn new(name: &str) -> Self {
|
||||
Self {
|
||||
flow: Flow::new(name),
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a step to this flow
|
||||
pub fn add_step(mut self, step: Step) -> Self {
|
||||
self.flow.steps.push(step);
|
||||
self
|
||||
}
|
||||
|
||||
/// Build the final flow
|
||||
pub fn build(self) -> Flow {
|
||||
self.flow
|
||||
}
|
||||
}
|
||||
|
||||
/// Convenience function to create a new step builder
|
||||
pub fn new_step(name: &str) -> StepBuilder {
|
||||
StepBuilder::new(name)
|
||||
}
|
||||
|
||||
/// Convenience function to create a new flow builder
|
||||
pub fn new_flow(name: &str) -> FlowBuilder {
|
||||
FlowBuilder::new(name)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_step_builder() {
|
||||
let step = new_step("test_step")
|
||||
.script("print('hello world');")
|
||||
.timeout(10)
|
||||
.retries(3)
|
||||
.build();
|
||||
|
||||
assert_eq!(step.name, "test_step");
|
||||
assert_eq!(step.script, "print('hello world');");
|
||||
assert_eq!(step.timeout_seconds, 10);
|
||||
assert_eq!(step.max_retries, 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_flow_builder() {
|
||||
let step1 = new_step("step1").script("let x = 1;").build();
|
||||
let step2 = new_step("step2").script("let y = 2;").build();
|
||||
|
||||
let flow = new_flow("test_flow")
|
||||
.add_step(step1)
|
||||
.add_step(step2)
|
||||
.build();
|
||||
|
||||
assert_eq!(flow.name, "test_flow");
|
||||
assert_eq!(flow.steps.len(), 2);
|
||||
assert_eq!(flow.steps[0].name, "step1");
|
||||
assert_eq!(flow.steps[1].name, "step2");
|
||||
}
|
||||
}
|
243
rhailib/src/flow/src/executor.rs
Normal file
243
rhailib/src/flow/src/executor.rs
Normal file
@@ -0,0 +1,243 @@
|
||||
//! Simple flow executor with non-blocking step execution
|
||||
|
||||
use crate::types::{Flow, Step, FlowStatus, StepStatus};
|
||||
use crate::state::{FlowState, StepState, StateManager};
|
||||
use crate::mock_api::MockAPI;
|
||||
use rhai_dispatcher::RhaiDispatcherBuilder;
|
||||
use std::sync::Arc;
|
||||
use tokio::time::{timeout, Duration};
|
||||
|
||||
/// Simple flow executor
|
||||
pub struct FlowExecutor {
|
||||
state_manager: Arc<StateManager>,
|
||||
mock_api: Arc<MockAPI>,
|
||||
redis_url: String,
|
||||
}
|
||||
|
||||
impl FlowExecutor {
|
||||
pub async fn new(redis_url: &str) -> Result<Self, Box<dyn std::error::Error>> {
|
||||
let state_manager = Arc::new(StateManager::new(redis_url).await?);
|
||||
let mock_api = Arc::new(MockAPI::default());
|
||||
|
||||
Ok(Self {
|
||||
state_manager,
|
||||
mock_api,
|
||||
redis_url: redis_url.to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Execute a flow non-blocking
|
||||
pub async fn execute_flow(&self, flow: Flow) -> Result<String, Box<dyn std::error::Error>> {
|
||||
// Initialize flow state
|
||||
let mut flow_state = FlowState::new(flow.id.clone());
|
||||
flow_state.status = FlowStatus::Running;
|
||||
self.state_manager.save_flow_state(&flow_state).await?;
|
||||
|
||||
// Initialize step states
|
||||
for step in &flow.steps {
|
||||
let step_state = StepState::new(step.id.clone());
|
||||
self.state_manager.save_step_state(&flow.id, &step_state).await?;
|
||||
}
|
||||
|
||||
// Spawn flow execution in background
|
||||
let flow_id = flow.id.clone();
|
||||
let state_manager = self.state_manager.clone();
|
||||
let mock_api = self.mock_api.clone();
|
||||
let redis_url = self.redis_url.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = Self::execute_flow_steps(flow, state_manager, mock_api, redis_url).await {
|
||||
eprintln!("Flow execution error: {}", e);
|
||||
}
|
||||
});
|
||||
|
||||
Ok(format!("flow_execution_started:{}", flow_id))
|
||||
}
|
||||
|
||||
/// Execute all steps in a flow
|
||||
async fn execute_flow_steps(
|
||||
flow: Flow,
|
||||
state_manager: Arc<StateManager>,
|
||||
mock_api: Arc<MockAPI>,
|
||||
redis_url: String,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut flow_state = state_manager.load_flow_state(&flow.id).await?
|
||||
.ok_or("Flow state not found")?;
|
||||
|
||||
// Execute steps sequentially
|
||||
for step in &flow.steps {
|
||||
flow_state.current_step = Some(step.id.clone());
|
||||
state_manager.save_flow_state(&flow_state).await?;
|
||||
|
||||
match Self::execute_step_with_retries(
|
||||
step,
|
||||
&flow.id,
|
||||
state_manager.clone(),
|
||||
mock_api.clone(),
|
||||
redis_url.clone(),
|
||||
).await {
|
||||
Ok(_) => {
|
||||
flow_state.completed_steps.push(step.id.clone());
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Step {} failed: {}", step.name, e);
|
||||
flow_state.status = FlowStatus::Failed;
|
||||
state_manager.save_flow_state(&flow_state).await?;
|
||||
return Err(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Mark flow as completed
|
||||
flow_state.status = FlowStatus::Completed;
|
||||
flow_state.current_step = None;
|
||||
state_manager.save_flow_state(&flow_state).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Execute a single step with retry logic
|
||||
async fn execute_step_with_retries(
|
||||
step: &Step,
|
||||
flow_id: &str,
|
||||
state_manager: Arc<StateManager>,
|
||||
mock_api: Arc<MockAPI>,
|
||||
redis_url: String,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut step_state = state_manager.load_step_state(flow_id, &step.id).await?
|
||||
.ok_or("Step state not found")?;
|
||||
|
||||
let max_attempts = step.max_retries + 1;
|
||||
|
||||
for attempt in 0..max_attempts {
|
||||
step_state.attempt_count = attempt + 1;
|
||||
step_state.status = StepStatus::Running;
|
||||
state_manager.save_step_state(flow_id, &step_state).await?;
|
||||
|
||||
match Self::execute_single_step(step, &mock_api, &redis_url).await {
|
||||
Ok(output) => {
|
||||
step_state.status = StepStatus::Completed;
|
||||
step_state.output = Some(output);
|
||||
state_manager.save_step_state(flow_id, &step_state).await?;
|
||||
return Ok(());
|
||||
}
|
||||
Err(e) => {
|
||||
if attempt + 1 >= max_attempts {
|
||||
step_state.status = StepStatus::Failed;
|
||||
state_manager.save_step_state(flow_id, &step_state).await?;
|
||||
return Err(e);
|
||||
}
|
||||
// Wait before retry
|
||||
tokio::time::sleep(Duration::from_millis(1000)).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err("Max retries exceeded".into())
|
||||
}
|
||||
|
||||
/// Execute a single step
|
||||
async fn execute_single_step(
|
||||
step: &Step,
|
||||
mock_api: &MockAPI,
|
||||
redis_url: &str,
|
||||
) -> Result<String, Box<dyn std::error::Error>> {
|
||||
// Execute with timeout
|
||||
let result = timeout(step.timeout(), async {
|
||||
// For demo, we'll use mock API calls instead of real Rhai execution
|
||||
// In real implementation, this would execute the Rhai script
|
||||
if step.script.contains("mock_api_call") {
|
||||
// Extract endpoint from script (simple parsing)
|
||||
let endpoint = if step.script.contains("stripe_config") {
|
||||
"stripe_config"
|
||||
} else if step.script.contains("create_product") {
|
||||
"create_product"
|
||||
} else {
|
||||
"default_endpoint"
|
||||
};
|
||||
|
||||
mock_api.call(endpoint).await
|
||||
} else {
|
||||
// For non-mock scripts, simulate Rhai execution via dispatcher
|
||||
Self::execute_rhai_script(&step.script, redis_url).await
|
||||
}
|
||||
}).await;
|
||||
|
||||
match result {
|
||||
Ok(Ok(output)) => Ok(output),
|
||||
Ok(Err(e)) => Err(e.into()),
|
||||
Err(_) => Err("Step execution timed out".into()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Execute Rhai script using dispatcher (simplified)
|
||||
async fn execute_rhai_script(
|
||||
script: &str,
|
||||
redis_url: &str,
|
||||
) -> Result<String, Box<dyn std::error::Error>> {
|
||||
let dispatcher = RhaiDispatcherBuilder::new()
|
||||
.caller_id("flow_executor")
|
||||
.redis_url(redis_url)
|
||||
.build()?;
|
||||
|
||||
let result = dispatcher
|
||||
.new_play_request()
|
||||
.worker_id("flow_worker")
|
||||
.script(script)
|
||||
.timeout(Duration::from_secs(30))
|
||||
.await_response()
|
||||
.await;
|
||||
|
||||
match result {
|
||||
Ok(task_details) => {
|
||||
if task_details.status == "completed" {
|
||||
Ok(task_details.output.unwrap_or_default())
|
||||
} else {
|
||||
Err(format!("Script execution failed: {:?}", task_details.error).into())
|
||||
}
|
||||
}
|
||||
Err(e) => Err(format!("Dispatcher error: {}", e).into()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get flow status
|
||||
pub async fn get_flow_status(&self, flow_id: &str) -> Result<Option<FlowState>, Box<dyn std::error::Error>> {
|
||||
self.state_manager.load_flow_state(flow_id).await
|
||||
}
|
||||
|
||||
/// Get step status
|
||||
pub async fn get_step_status(&self, flow_id: &str, step_id: &str) -> Result<Option<StepState>, Box<dyn std::error::Error>> {
|
||||
self.state_manager.load_step_state(flow_id, step_id).await
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::builder::{new_step, new_flow};
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_flow_execution() {
|
||||
// This test requires Redis to be running
|
||||
// Skip if Redis is not available
|
||||
if std::env::var("SKIP_REDIS_TESTS").is_ok() {
|
||||
return;
|
||||
}
|
||||
|
||||
let executor = FlowExecutor::new("redis://127.0.0.1/").await.unwrap();
|
||||
|
||||
let step1 = new_step("test_step")
|
||||
.script("mock_api_call stripe_config")
|
||||
.timeout(5)
|
||||
.retries(1)
|
||||
.build();
|
||||
|
||||
let flow = new_flow("test_flow")
|
||||
.add_step(step1)
|
||||
.build();
|
||||
|
||||
let result = executor.execute_flow(flow).await;
|
||||
assert!(result.is_ok());
|
||||
assert!(result.unwrap().starts_with("flow_execution_started:"));
|
||||
}
|
||||
}
|
20
rhailib/src/flow/src/lib.rs
Normal file
20
rhailib/src/flow/src/lib.rs
Normal file
@@ -0,0 +1,20 @@
|
||||
//! Simple Flow Manager for Rhai Scripts
|
||||
//!
|
||||
//! Provides a minimal flow execution system with builder patterns:
|
||||
//! - `new_step("name").script("script").timeout(5).retries(2)`
|
||||
//! - `new_flow("name").add_step(step1).add_step(step2)`
|
||||
|
||||
pub mod types;
|
||||
pub mod builder;
|
||||
pub mod executor;
|
||||
pub mod state;
|
||||
pub mod mock_api;
|
||||
|
||||
pub use types::{Flow, Step, FlowStatus, StepStatus};
|
||||
pub use builder::{StepBuilder, FlowBuilder, new_step, new_flow};
|
||||
pub use executor::FlowExecutor;
|
||||
pub use state::{FlowState, StepState, StateManager};
|
||||
pub use mock_api::MockAPI;
|
||||
|
||||
// Re-export for convenience
|
||||
pub use rhai_dispatcher::RhaiDispatcherBuilder;
|
144
rhailib/src/flow/src/mock_api.rs
Normal file
144
rhailib/src/flow/src/mock_api.rs
Normal file
@@ -0,0 +1,144 @@
|
||||
//! Simple mock API for testing different response types and durations
|
||||
|
||||
use serde::{Serialize, Deserialize};
|
||||
use std::time::Duration;
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Mock API response types
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum MockResponseType {
|
||||
Success,
|
||||
Failure,
|
||||
Timeout,
|
||||
}
|
||||
|
||||
/// Mock API scenario configuration
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct MockScenario {
|
||||
pub response_type: MockResponseType,
|
||||
pub delay_ms: u64,
|
||||
pub response_data: String,
|
||||
}
|
||||
|
||||
impl MockScenario {
|
||||
pub fn success(delay_ms: u64, data: &str) -> Self {
|
||||
Self {
|
||||
response_type: MockResponseType::Success,
|
||||
delay_ms,
|
||||
response_data: data.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn failure(delay_ms: u64, error: &str) -> Self {
|
||||
Self {
|
||||
response_type: MockResponseType::Failure,
|
||||
delay_ms,
|
||||
response_data: error.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn timeout(delay_ms: u64) -> Self {
|
||||
Self {
|
||||
response_type: MockResponseType::Timeout,
|
||||
delay_ms,
|
||||
response_data: "Request timed out".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Simple mock API for testing
|
||||
pub struct MockAPI {
|
||||
scenarios: HashMap<String, MockScenario>,
|
||||
}
|
||||
|
||||
impl MockAPI {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
scenarios: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a mock scenario for an endpoint
|
||||
pub fn add_scenario(&mut self, endpoint: &str, scenario: MockScenario) {
|
||||
self.scenarios.insert(endpoint.to_string(), scenario);
|
||||
}
|
||||
|
||||
/// Call a mock endpoint
|
||||
pub async fn call(&self, endpoint: &str) -> Result<String, String> {
|
||||
match self.scenarios.get(endpoint) {
|
||||
Some(scenario) => {
|
||||
// Simulate delay
|
||||
tokio::time::sleep(Duration::from_millis(scenario.delay_ms)).await;
|
||||
|
||||
match scenario.response_type {
|
||||
MockResponseType::Success => Ok(scenario.response_data.clone()),
|
||||
MockResponseType::Failure => Err(scenario.response_data.clone()),
|
||||
MockResponseType::Timeout => {
|
||||
// For timeout, we just return an error after the delay
|
||||
Err("Request timed out".to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
None => Err(format!("Unknown endpoint: {}", endpoint)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Setup common test scenarios
|
||||
pub fn setup_test_scenarios(&mut self) {
|
||||
// Fast success
|
||||
self.add_scenario("stripe_config", MockScenario::success(100, r#"{"status": "configured"}"#));
|
||||
|
||||
// Slow success
|
||||
self.add_scenario("create_product", MockScenario::success(2000, r#"{"id": "prod_123", "name": "Test Product"}"#));
|
||||
|
||||
// Fast failure
|
||||
self.add_scenario("invalid_endpoint", MockScenario::failure(50, "Invalid API key"));
|
||||
|
||||
// Timeout scenario
|
||||
self.add_scenario("slow_endpoint", MockScenario::timeout(5000));
|
||||
|
||||
// Variable responses for testing retries
|
||||
self.add_scenario("flaky_endpoint", MockScenario::failure(500, "Temporary server error"));
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for MockAPI {
|
||||
fn default() -> Self {
|
||||
let mut api = Self::new();
|
||||
api.setup_test_scenarios();
|
||||
api
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_mock_api_success() {
|
||||
let mut api = MockAPI::new();
|
||||
api.add_scenario("test", MockScenario::success(10, "success"));
|
||||
|
||||
let result = api.call("test").await;
|
||||
assert!(result.is_ok());
|
||||
assert_eq!(result.unwrap(), "success");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_mock_api_failure() {
|
||||
let mut api = MockAPI::new();
|
||||
api.add_scenario("test", MockScenario::failure(10, "error"));
|
||||
|
||||
let result = api.call("test").await;
|
||||
assert!(result.is_err());
|
||||
assert_eq!(result.unwrap_err(), "error");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_mock_api_unknown_endpoint() {
|
||||
let api = MockAPI::new();
|
||||
let result = api.call("unknown").await;
|
||||
assert!(result.is_err());
|
||||
assert!(result.unwrap_err().contains("Unknown endpoint"));
|
||||
}
|
||||
}
|
100
rhailib/src/flow/src/state.rs
Normal file
100
rhailib/src/flow/src/state.rs
Normal file
@@ -0,0 +1,100 @@
|
||||
//! Simple state management for flows and steps
|
||||
|
||||
use serde::{Serialize, Deserialize};
|
||||
use crate::types::{FlowStatus, StepStatus};
|
||||
|
||||
/// Minimal flow state tracking
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct FlowState {
|
||||
pub flow_id: String,
|
||||
pub status: FlowStatus,
|
||||
pub current_step: Option<String>,
|
||||
pub completed_steps: Vec<String>,
|
||||
}
|
||||
|
||||
impl FlowState {
|
||||
pub fn new(flow_id: String) -> Self {
|
||||
Self {
|
||||
flow_id,
|
||||
status: FlowStatus::Created,
|
||||
current_step: None,
|
||||
completed_steps: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Minimal step state tracking
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct StepState {
|
||||
pub step_id: String,
|
||||
pub status: StepStatus,
|
||||
pub attempt_count: u32,
|
||||
pub output: Option<String>,
|
||||
}
|
||||
|
||||
impl StepState {
|
||||
pub fn new(step_id: String) -> Self {
|
||||
Self {
|
||||
step_id,
|
||||
status: StepStatus::Pending,
|
||||
attempt_count: 0,
|
||||
output: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Simple Redis-based state manager
|
||||
pub struct StateManager {
|
||||
redis_client: redis::Client,
|
||||
}
|
||||
|
||||
impl StateManager {
|
||||
pub async fn new(redis_url: &str) -> Result<Self, Box<dyn std::error::Error>> {
|
||||
let client = redis::Client::open(redis_url)?;
|
||||
Ok(Self {
|
||||
redis_client: client,
|
||||
})
|
||||
}
|
||||
|
||||
/// Save flow state to Redis
|
||||
pub async fn save_flow_state(&self, state: &FlowState) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut conn = self.redis_client.get_async_connection().await?;
|
||||
let key = format!("flow:{}", state.flow_id);
|
||||
let json = serde_json::to_string(state)?;
|
||||
redis::cmd("SET").arg(&key).arg(&json).query_async(&mut conn).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Load flow state from Redis
|
||||
pub async fn load_flow_state(&self, flow_id: &str) -> Result<Option<FlowState>, Box<dyn std::error::Error>> {
|
||||
let mut conn = self.redis_client.get_async_connection().await?;
|
||||
let key = format!("flow:{}", flow_id);
|
||||
let result: Option<String> = redis::cmd("GET").arg(&key).query_async(&mut conn).await?;
|
||||
|
||||
match result {
|
||||
Some(json) => Ok(Some(serde_json::from_str(&json)?)),
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
/// Save step state to Redis
|
||||
pub async fn save_step_state(&self, flow_id: &str, state: &StepState) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut conn = self.redis_client.get_async_connection().await?;
|
||||
let key = format!("step:{}:{}", flow_id, state.step_id);
|
||||
let json = serde_json::to_string(state)?;
|
||||
redis::cmd("SET").arg(&key).arg(&json).query_async(&mut conn).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Load step state from Redis
|
||||
pub async fn load_step_state(&self, flow_id: &str, step_id: &str) -> Result<Option<StepState>, Box<dyn std::error::Error>> {
|
||||
let mut conn = self.redis_client.get_async_connection().await?;
|
||||
let key = format!("step:{}:{}", flow_id, step_id);
|
||||
let result: Option<String> = redis::cmd("GET").arg(&key).query_async(&mut conn).await?;
|
||||
|
||||
match result {
|
||||
Some(json) => Ok(Some(serde_json::from_str(&json)?)),
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
}
|
66
rhailib/src/flow/src/types.rs
Normal file
66
rhailib/src/flow/src/types.rs
Normal file
@@ -0,0 +1,66 @@
|
||||
//! Core types for the flow manager
|
||||
|
||||
use serde::{Serialize, Deserialize};
|
||||
use std::time::Duration;
|
||||
|
||||
/// Simple flow status enumeration
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub enum FlowStatus {
|
||||
Created,
|
||||
Running,
|
||||
Completed,
|
||||
Failed,
|
||||
}
|
||||
|
||||
/// Simple step status enumeration
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub enum StepStatus {
|
||||
Pending,
|
||||
Running,
|
||||
Completed,
|
||||
Failed,
|
||||
}
|
||||
|
||||
/// A single step in a flow
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Step {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub script: String,
|
||||
pub timeout_seconds: u64,
|
||||
pub max_retries: u32,
|
||||
}
|
||||
|
||||
impl Step {
|
||||
pub fn new(name: &str) -> Self {
|
||||
Self {
|
||||
id: uuid::Uuid::new_v4().to_string(),
|
||||
name: name.to_string(),
|
||||
script: String::new(),
|
||||
timeout_seconds: 30, // default 30 seconds
|
||||
max_retries: 0, // default no retries
|
||||
}
|
||||
}
|
||||
|
||||
pub fn timeout(&self) -> Duration {
|
||||
Duration::from_secs(self.timeout_seconds)
|
||||
}
|
||||
}
|
||||
|
||||
/// A flow containing multiple steps
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Flow {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub steps: Vec<Step>,
|
||||
}
|
||||
|
||||
impl Flow {
|
||||
pub fn new(name: &str) -> Self {
|
||||
Self {
|
||||
id: uuid::Uuid::new_v4().to_string(),
|
||||
name: name.to_string(),
|
||||
steps: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user