├── src ├── lib.rs ├── trigger │ └── mod.rs ├── examples │ ├── time │ │ ├── time.json │ │ └── main.rs │ ├── set │ │ ├── set.json │ │ └── main.rs │ └── openai │ │ ├── openai.json │ │ └── main.rs ├── util │ └── mod.rs ├── node │ ├── trigger.rs │ ├── time.rs │ ├── if_node.rs │ ├── filter.rs │ ├── code.rs │ ├── http_request.rs │ ├── limit.rs │ ├── execute_command.rs │ ├── webhook.rs │ ├── wait.rs │ ├── merge.rs │ ├── function.rs │ ├── remove_duplicates.rs │ └── transform.rs └── expression │ └── mod.rs ├── Cargo.toml └── .github └── workflows └── ci.yml /src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod execution; 2 | pub mod expression; 3 | pub mod node; 4 | pub mod trigger; 5 | pub mod util; 6 | pub mod workflow; 7 | -------------------------------------------------------------------------------- /src/trigger/mod.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use serde_json::Value; 3 | 4 | #[derive(Debug, Clone, Serialize, Deserialize)] 5 | pub struct ManualTrigger { 6 | pub name: String, 7 | pub description: Option, 8 | pub parameters: Option, 9 | pub output: Option, 10 | } 11 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "glint" 3 | version = "0.1.0" 4 | edition = "2024" 5 | 6 | [lib] 7 | name = "glint" 8 | path = "src/lib.rs" 9 | 10 | [[bin]] 11 | name = "glint" 12 | path = "src/main.rs" 13 | 14 | [[bin]] 15 | name = "set_example" 16 | path = "src/examples/set/main.rs" 17 | 18 | [[bin]] 19 | name = "time_example" 20 | path = "src/examples/time/main.rs" 21 | 22 | [[bin]] 23 | name = "openai_example" 24 | path = "src/examples/openai/main.rs" 25 | 26 | [dependencies] 27 | chrono = "0.4.41" 28 | regex = "1.10.0" 29 | reqwest = { version = "0.12.0", features = ["json", "blocking"] } 30 | base64 = "0.22.0" 31 | hex = "0.4.3" 32 | serde = { version ="1.0.219", features = ["derive"] } 33 | serde_json = "1.0.142" 34 | tokio = { version = "1.0", features = ["full"] } 35 | uuid = { version = "1.18.0", features = ["v4", "serde"] } 36 | 37 | [dev-dependencies] 38 | uuid = { version = "1.18.0", features = ["v4"] } 39 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v2 17 | 18 | - name: Install Rust 19 | uses: actions-rs/toolchain@v1 20 | with: 21 | toolchain: stable 22 | override: true 23 | 24 | - name: Build 25 | run: cargo build --verbose 26 | 27 | - name: Run tests 28 | run: cargo test --verbose 29 | 30 | - name: Run clippy 31 | run: cargo clippy -- -D warnings 32 | 33 | - name: Check formatting 34 | run: cargo fmt -- --check 35 | 36 | test: 37 | runs-on: ubuntu-latest 38 | steps: 39 | - uses: actions/checkout@v2 40 | 41 | - name: Install Rust 42 | uses: actions-rs/toolchain@v1 43 | with: 44 | toolchain: stable 45 | override: true 46 | 47 | - name: Run tests 48 | run: cargo test --verbose --all-features 49 | 50 | doc: 51 | runs-on: ubuntu-latest 52 | steps: 53 | - uses: actions/checkout@v2 54 | 55 | - name: Install Rust 56 | uses: actions-rs/toolchain@v1 57 | with: 58 | toolchain: stable 59 | override: true 60 | 61 | - name: Build documentation 62 | run: cargo doc --no-deps --document-private-items -------------------------------------------------------------------------------- /src/examples/time/time.json: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "2f7d3c9a-1b42-4c83-9e5a-3a7f6d2c1b0e": { 4 | "type": "trigger", 5 | "name": "Trigger Node", 6 | "description": "trigger for workflow" 7 | }, 8 | "8e1f2a34-5c67-4b89-a1d2-3f4e5a6b7c8d": { 9 | "type": "set", 10 | "name": "User Basic Info", 11 | "description": "Set user basic info", 12 | "parameters": { 13 | "user": "John Doe", 14 | "email": "john.doe@example.com" 15 | } 16 | }, 17 | "f3a9c1be-7d24-4b8e-9a63-2d5b4c7e1f90": { 18 | "type": "time", 19 | "name": "Timestamp", 20 | "description": "Set user contact info", 21 | "parameters": { 22 | "now": "{{now}}" 23 | } 24 | }, 25 | "a3d7f2c1-6e45-4b9a-8c2f-1d3e5f7a9b20": { 26 | "type": "set", 27 | "name": "Merge User Info", 28 | "description": "Merge user information", 29 | "parameters": { 30 | "user": "=${8e1f2a34-5c67-4b89-a1d2-3f4e5a6b7c8d}.user", 31 | "email": "=${8e1f2a34-5c67-4b89-a1d2-3f4e5a6b7c8d}.email", 32 | "now": "=${f3a9c1be-7d24-4b8e-9a63-2d5b4c7e1f90}.now" 33 | } 34 | } 35 | }, 36 | "connections": { 37 | "2f7d3c9a-1b42-4c83-9e5a-3a7f6d2c1b0e": ["8e1f2a34-5c67-4b89-a1d2-3f4e5a6b7c8d", "f3a9c1be-7d24-4b8e-9a63-2d5b4c7e1f90"], 38 | "8e1f2a34-5c67-4b89-a1d2-3f4e5a6b7c8d": ["a3d7f2c1-6e45-4b9a-8c2f-1d3e5f7a9b20"], 39 | "f3a9c1be-7d24-4b8e-9a63-2d5b4c7e1f90": ["a3d7f2c1-6e45-4b9a-8c2f-1d3e5f7a9b20"] 40 | } 41 | } -------------------------------------------------------------------------------- /src/examples/set/set.json: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "2f7d3c9a-1b42-4c83-9e5a-3a7f6d2c1b0e": { 4 | "type": "trigger", 5 | "name": "Trigger Node", 6 | "description": "trigger for workflow" 7 | }, 8 | "8e1f2a34-5c67-4b89-a1d2-3f4e5a6b7c8d": { 9 | "type": "set", 10 | "name": "User Basic Info", 11 | "description": "Set user basic info", 12 | "parameters": { 13 | "user": "John Doe", 14 | "email": "john.doe@example.com" 15 | } 16 | }, 17 | "f3a9c1be-7d24-4b8e-9a63-2d5b4c7e1f90": { 18 | "type": "set", 19 | "name": "User Contact Info", 20 | "description": "Set user contact info", 21 | "parameters": { 22 | "user": "John Doe", 23 | "mobile": "+0123456789" 24 | } 25 | }, 26 | "a3d7f2c1-6e45-4b9a-8c2f-1d3e5f7a9b20": { 27 | "type": "set", 28 | "name": "Merge User Info", 29 | "description": "Merge user information", 30 | "parameters": { 31 | "user": "=${8e1f2a34-5c67-4b89-a1d2-3f4e5a6b7c8d}.user", 32 | "email": "=${8e1f2a34-5c67-4b89-a1d2-3f4e5a6b7c8d}.email", 33 | "mobile": "=${f3a9c1be-7d24-4b8e-9a63-2d5b4c7e1f90}.mobile" 34 | } 35 | } 36 | }, 37 | "connections": { 38 | "2f7d3c9a-1b42-4c83-9e5a-3a7f6d2c1b0e": ["8e1f2a34-5c67-4b89-a1d2-3f4e5a6b7c8d", "f3a9c1be-7d24-4b8e-9a63-2d5b4c7e1f90"], 39 | "8e1f2a34-5c67-4b89-a1d2-3f4e5a6b7c8d": ["a3d7f2c1-6e45-4b9a-8c2f-1d3e5f7a9b20"], 40 | "f3a9c1be-7d24-4b8e-9a63-2d5b4c7e1f90": ["a3d7f2c1-6e45-4b9a-8c2f-1d3e5f7a9b20"] 41 | } 42 | } -------------------------------------------------------------------------------- /src/util/mod.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | pub type Result = std::result::Result; 4 | 5 | #[derive(Debug)] 6 | pub enum Error { 7 | Database(String), 8 | Validation(String), 9 | WorkflowExecution(String), 10 | NodeExecution(String), 11 | ExpressionEvaluation(String), 12 | BinaryData(String), 13 | Queue(String), 14 | Configuration(String), 15 | Internal(String), 16 | } 17 | 18 | impl fmt::Display for Error { 19 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 20 | match self { 21 | Error::Database(msg) => write!(f, "Database error: {}", msg), 22 | Error::Validation(msg) => write!(f, "Validation error: {}", msg), 23 | Error::WorkflowExecution(msg) => write!(f, "Workflow execution error: {}", msg), 24 | Error::NodeExecution(msg) => write!(f, "Node execution error: {}", msg), 25 | Error::ExpressionEvaluation(msg) => write!(f, "Expression evaluation error: {}", msg), 26 | Error::BinaryData(msg) => write!(f, "Binary data error: {}", msg), 27 | Error::Queue(msg) => write!(f, "Queue error: {}", msg), 28 | Error::Configuration(msg) => write!(f, "Configuration error: {}", msg), 29 | Error::Internal(msg) => write!(f, "Internal error: {}", msg), 30 | } 31 | } 32 | } 33 | 34 | impl From for Error { 35 | fn from(err: serde_json::Error) -> Self { 36 | Error::Configuration(format!("JSON parsing error: {}", err)) 37 | } 38 | } 39 | 40 | impl From for Error { 41 | fn from(err: uuid::Error) -> Self { 42 | Error::Configuration(format!("UUID parsing error: {}", err)) 43 | } 44 | } 45 | 46 | impl From> for Error { 47 | fn from(err: Box) -> Self { 48 | Error::Internal(format!("Internal error: {}", err)) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/examples/openai/openai.json: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "2f7d3c9a-1b42-4c83-9e5a-3a7f6d2c1b0e": { 4 | "type": "trigger", 5 | "name": "trigger node", 6 | "description": "trigger for workflow" 7 | }, 8 | "8e1f2a34-5c67-4b89-a1d2-3f4e5a6b7c8d": { 9 | "type": "set", 10 | "name": "destination", 11 | "description": "destination", 12 | "parameters": { 13 | "destination": "Paris" 14 | } 15 | }, 16 | "f3a9c1be-7d24-4b8e-9a63-2d5b4c7e1f90": { 17 | "type": "time", 18 | "name": "time", 19 | "description": "start time", 20 | "parameters": { 21 | "date": "{{now}}" 22 | } 23 | }, 24 | "a3d7f2c1-6e45-4b9a-8c2f-1d3e5f7a9b20": { 25 | "type": "openai", 26 | "name": "openai", 27 | "description": "make a travel plan", 28 | "parameters": { 29 | "model": "gpt-4", 30 | "credentials": { 31 | "api_key": "sk-proj-01234567890" 32 | }, 33 | "messages": [ 34 | { 35 | "role": "system", 36 | "content": "You are a travel planner, please create a travel plan based on user requirements" 37 | }, 38 | { 39 | "role": "user", 40 | "content": "I need to travel to {{=${8e1f2a34-5c67-4b89-a1d2-3f4e5a6b7c8d}.destination}} at {{=${f3a9c1be-7d24-4b8e-9a63-2d5b4c7e1f90}.date}}" 41 | } 42 | ] 43 | } 44 | } 45 | }, 46 | "connections": { 47 | "2f7d3c9a-1b42-4c83-9e5a-3a7f6d2c1b0e": ["8e1f2a34-5c67-4b89-a1d2-3f4e5a6b7c8d", "f3a9c1be-7d24-4b8e-9a63-2d5b4c7e1f90"], 48 | "8e1f2a34-5c67-4b89-a1d2-3f4e5a6b7c8d": ["a3d7f2c1-6e45-4b9a-8c2f-1d3e5f7a9b20"], 49 | "f3a9c1be-7d24-4b8e-9a63-2d5b4c7e1f90": ["a3d7f2c1-6e45-4b9a-8c2f-1d3e5f7a9b20"] 50 | } 51 | } -------------------------------------------------------------------------------- /src/node/trigger.rs: -------------------------------------------------------------------------------- 1 | use crate::execution::NodeOutput; 2 | use serde::{Deserialize, Serialize}; 3 | use serde_json::Value; 4 | 5 | #[derive(Debug, Clone, Serialize, Deserialize)] 6 | pub struct TriggerNode { 7 | pub id: super::NodeId, 8 | pub name: String, 9 | pub description: Option, 10 | pub parameters: Option, 11 | } 12 | 13 | impl super::INode for TriggerNode { 14 | fn new( 15 | id: super::NodeId, 16 | name: String, 17 | _type: String, 18 | description: Option, 19 | parameters: Option, 20 | ) -> Self { 21 | Self { 22 | id, 23 | name, 24 | description, 25 | parameters, 26 | } 27 | } 28 | 29 | fn id(&self) -> super::NodeId { 30 | self.id 31 | } 32 | 33 | fn name(&self) -> String { 34 | self.name.clone() 35 | } 36 | 37 | fn description(&self) -> Option { 38 | self.description.clone() 39 | } 40 | 41 | fn parameter(&self) -> Option { 42 | self.parameters.clone() 43 | } 44 | 45 | fn execute(&self, _input: &NodeOutput) -> Result { 46 | // For trigger nodes, we convert the input NodeOutput to a JSON object 47 | match self.parameters.clone() { 48 | Some(parameters) => { 49 | let input_value = serde_json::to_value(parameters)?; 50 | Ok(input_value) 51 | } 52 | None => Ok(Value::Null), 53 | } 54 | } 55 | 56 | fn validate(&self) -> bool { 57 | // Validate node name cannot be empty 58 | if self.name.trim().is_empty() { 59 | return false; 60 | } 61 | 62 | // If parameters exist, validate they cannot be null 63 | if let Some(params) = &self.parameters { 64 | if params.is_null() { 65 | return false; 66 | } 67 | } 68 | 69 | true 70 | } 71 | 72 | fn dependencies(&self) -> Vec { 73 | // Trigger nodes typically don't depend on other nodes, return empty list 74 | // If support for expressions in trigger node parameters is needed, implement logic similar to SetNode here 75 | Vec::new() 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/node/time.rs: -------------------------------------------------------------------------------- 1 | use crate::execution::NodeOutput; 2 | use chrono::Utc; 3 | use serde::{Deserialize, Serialize}; 4 | use serde_json::Value; 5 | 6 | #[derive(Debug, Clone, Serialize, Deserialize)] 7 | pub struct TimeNode { 8 | pub id: super::NodeId, 9 | pub name: String, 10 | pub description: Option, 11 | pub parameters: Option, 12 | } 13 | 14 | impl super::INode for TimeNode { 15 | fn new( 16 | id: super::NodeId, 17 | name: String, 18 | _type: String, 19 | description: Option, 20 | parameters: Option, 21 | ) -> Self { 22 | Self { 23 | id, 24 | name, 25 | description, 26 | parameters, 27 | } 28 | } 29 | 30 | fn id(&self) -> super::NodeId { 31 | self.id 32 | } 33 | 34 | fn name(&self) -> String { 35 | self.name.clone() 36 | } 37 | 38 | fn description(&self) -> Option { 39 | self.description.clone() 40 | } 41 | 42 | fn parameter(&self) -> Option { 43 | self.parameters.clone() 44 | } 45 | 46 | fn execute(&self, _input: &NodeOutput) -> Result { 47 | // Get current UTC time and format as YYYY-MM-DD HH:MM:SS 48 | let now = Utc::now(); 49 | let formatted_time = now.format("%Y-%m-%d %H:%M:%S").to_string(); 50 | 51 | // If no parameters, return default time field 52 | let Some(params) = &self.parameters else { 53 | let mut result = serde_json::Map::new(); 54 | result.insert("now".to_string(), Value::String(formatted_time)); 55 | return Ok(Value::Object(result)); 56 | }; 57 | 58 | // Parameters must be an object 59 | let Some(obj) = params.as_object() else { 60 | return Err(super::Error::NodeExecution("Parameters must be an object".to_string())); 61 | }; 62 | 63 | // Parameter object must have exactly one field 64 | if obj.len() != 1 { 65 | return Err(super::Error::NodeExecution( 66 | "Parameter object must have exactly one field".to_string(), 67 | )); 68 | }; 69 | 70 | // Get the unique field name 71 | let (field_name, _) = obj.iter().next().unwrap(); 72 | 73 | // Construct return result 74 | let mut result = serde_json::Map::new(); 75 | result.insert(field_name.clone(), Value::String(formatted_time)); 76 | 77 | Ok(Value::Object(result)) 78 | } 79 | 80 | fn validate(&self) -> bool { 81 | // Validate node name cannot be empty 82 | if self.name.trim().is_empty() { 83 | return false; 84 | } 85 | 86 | // If no parameters, it's also valid (time nodes can work without parameters) 87 | let Some(params) = &self.parameters else { 88 | return true; 89 | }; 90 | 91 | // Parameters cannot be null 92 | if params.is_null() { 93 | return false; 94 | } 95 | 96 | // Parameters must be an object 97 | let Some(obj) = params.as_object() else { 98 | return false; 99 | }; 100 | 101 | // Parameter object must have exactly one field 102 | if obj.len() != 1 { 103 | return false; 104 | } 105 | 106 | // Get the unique field value 107 | let (_, field_value) = obj.iter().next().unwrap(); 108 | 109 | // Field value must be a string 110 | let Some(value_str) = field_value.as_str() else { 111 | return false; 112 | }; 113 | 114 | // Field value must be exactly "{{now}}" 115 | value_str == "{{now}}" 116 | } 117 | 118 | fn dependencies(&self) -> Vec { 119 | Vec::new() 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/examples/time/main.rs: -------------------------------------------------------------------------------- 1 | use serde_json::json; 2 | use std::fs; 3 | use glint::{ 4 | execution::{Execution, Status}, 5 | node::INode, 6 | workflow::Workflow, 7 | }; 8 | 9 | fn main() { 10 | println!("=== Time Example Workflow Execution ===\n"); 11 | 12 | // 1. Read time.json to construct a workflow 13 | println!("📖 Reading time.json..."); 14 | let json_content = match fs::read_to_string("src/examples/time/time.json") { 15 | Ok(content) => content, 16 | Err(e) => { 17 | eprintln!("❌ Cannot read time.json file: {}", e); 18 | return; 19 | } 20 | }; 21 | 22 | println!("🔧 Constructing workflow..."); 23 | let workflow = match Workflow::from_json( 24 | &json_content, 25 | "Time Example Workflow".to_string(), 26 | Some("Example workflow demonstrating time node functionality".to_string()), 27 | ) { 28 | Ok(wf) => wf, 29 | Err(e) => { 30 | eprintln!("❌ Cannot construct workflow: {}", e); 31 | return; 32 | } 33 | }; 34 | 35 | println!("✅ Workflow constructed successfully!"); 36 | println!(" - ID: {}", workflow.id); 37 | println!(" - Name: {}", workflow.name); 38 | println!( 39 | " - Description: {}", 40 | workflow.description.as_deref().unwrap_or("None") 41 | ); 42 | println!(" - Number of nodes: {}", workflow.nodes.len()); 43 | println!(" - Number of connections: {}", workflow.connections.len()); 44 | 45 | // 2. Use this workflow to construct an execution with input {} 46 | println!("\n🚀 Constructing execution..."); 47 | let input = json!({}); 48 | let mut execution = Execution::new(&workflow, input); 49 | 50 | println!("✅ Execution constructed successfully!"); 51 | println!(" - Execution ID: {}", execution.id); 52 | println!(" - Current status: {:?}", execution.status()); 53 | println!(" - Execution plan batches: {}", execution.plan.batch.len()); 54 | 55 | // Print execution plan 56 | println!("\n📋 Execution Plan:"); 57 | for (batch_index, batch) in execution.plan.batch.iter().enumerate() { 58 | println!(" Batch {}: {} nodes", batch_index, batch.len()); 59 | for node_id in batch { 60 | if let Some(node) = workflow.nodes.get(node_id) { 61 | println!(" - {} ({})", node.name(), node_id); 62 | } 63 | } 64 | } 65 | 66 | // Print node dependencies 67 | println!("\n🔗 Node Dependencies:"); 68 | for (node_id, node) in &workflow.nodes { 69 | let deps = node.dependencies(); 70 | println!(" {} ({}):", node.name(), node_id); 71 | if deps.is_empty() { 72 | println!(" No dependencies"); 73 | } else { 74 | println!(" Depends on {} nodes:", deps.len()); 75 | for dep_id in &deps { 76 | if let Some(dep_node) = workflow.nodes.get(dep_id) { 77 | println!(" - {} ({})", dep_node.name(), dep_id); 78 | } else { 79 | println!(" - Unknown node ({})", dep_id); 80 | } 81 | } 82 | } 83 | } 84 | 85 | // 3. Complete step-by-step execution of execution and output step execution results 86 | println!("\n⚡ Starting workflow execution..."); 87 | 88 | let mut step = 0; 89 | while execution.has_next() { 90 | step += 1; 91 | println!("\n--- Execution Step {} ---", step); 92 | 93 | println!(" Status before execution: {:?}", execution.status()); 94 | let batch_result = execution.execute(); 95 | println!(" Batch execution result: {:?}", batch_result); 96 | println!(" Status after execution: {:?}", execution.status()); 97 | 98 | // Print current node status 99 | println!(" Node status details:"); 100 | for (node_id, status) in &execution.status { 101 | if let Some(node) = workflow.nodes.get(node_id) { 102 | println!(" {} ({}): {:?}", node.name(), node_id, status); 103 | } 104 | } 105 | } 106 | 107 | // 4. Output final status of all nodes and output 108 | println!("\n🎯 Workflow execution completed!"); 109 | println!(" - Final execution status: {:?}", execution.status()); 110 | println!(" - Output result count: {}", execution.output.len()); 111 | 112 | // Print detailed output results 113 | println!("\n📊 Execution output results:"); 114 | for (node_id, output) in &execution.output { 115 | if let Some(node) = workflow.nodes.get(node_id) { 116 | println!(" {} ({}):", node.name(), node_id); 117 | println!( 118 | " {}", 119 | serde_json::to_string_pretty(output).unwrap_or_else(|_| "Cannot serialize".to_string()) 120 | ); 121 | } 122 | } 123 | 124 | // 5. Print final status of all nodes 125 | println!("\n📈 Final node status:"); 126 | for (node_id, status) in &execution.status { 127 | if let Some(node) = workflow.nodes.get(node_id) { 128 | println!(" {} ({}): {:?}", node.name(), node_id, status); 129 | } 130 | } 131 | 132 | // 6. Verify output results match expectations 133 | println!("\n🔍 Verifying output results..."); 134 | 135 | // Verify final status 136 | let all_success = execution 137 | .status 138 | .values() 139 | .all(|status| matches!(status, Status::Success)); 140 | if all_success { 141 | println!(" ✅ All node execution status correct"); 142 | } else { 143 | println!(" ❌ Some nodes failed to execute"); 144 | } 145 | 146 | // Verify time node correctly outputs timestamp 147 | let time_node_id = uuid::Uuid::parse_str("f3a9c1be-7d24-4b8e-9a63-2d5b4c7e1f90").unwrap(); 148 | if let Some(time_output) = execution.output.get(&time_node_id) { 149 | if let Some(now_value) = time_output.get("now") { 150 | if let Some(time_str) = now_value.as_str() { 151 | println!(" ✅ Time node correctly outputs timestamp: {}", time_str); 152 | } else { 153 | println!(" ❌ Time node output format incorrect"); 154 | } 155 | } else { 156 | println!(" ❌ Time node has no 'now' field output"); 157 | } 158 | } else { 159 | println!(" ❌ Time node has no output"); 160 | } 161 | 162 | // Verify merge node correctly merges information 163 | let merge_node_id = uuid::Uuid::parse_str("a3d7f2c1-6e45-4b9a-8c2f-1d3e5f7a9b20").unwrap(); 164 | if let Some(merge_output) = execution.output.get(&merge_node_id) { 165 | let has_user = merge_output.get("user").is_some(); 166 | let has_email = merge_output.get("email").is_some(); 167 | let has_now = merge_output.get("now").is_some(); 168 | 169 | if has_user && has_email && has_now { 170 | println!(" ✅ Merge node correctly includes user info and timestamp"); 171 | } else { 172 | println!( 173 | " ❌ Merge node missing required fields (user: {}, email: {}, now: {})", 174 | has_user, has_email, has_now 175 | ); 176 | } 177 | } else { 178 | println!(" ❌ Merge node has no output"); 179 | } 180 | 181 | // Final verification result 182 | println!("\n🎉 Verification result:"); 183 | if all_success { 184 | println!(" ✅ Workflow execution completed, all node status normal!"); 185 | } else { 186 | println!(" ❌ Workflow execution has issues, please check node status."); 187 | std::process::exit(1); 188 | } 189 | 190 | println!("\n=== Example execution completed ==="); 191 | } 192 | -------------------------------------------------------------------------------- /src/examples/set/main.rs: -------------------------------------------------------------------------------- 1 | use serde_json::json; 2 | use std::fs; 3 | use glint::{ 4 | execution::{Execution, Status}, 5 | node::INode, 6 | workflow::Workflow, 7 | }; 8 | 9 | fn main() { 10 | println!("=== Set Example Workflow Execution ===\n"); 11 | 12 | // 1. Read set.json to construct a workflow 13 | println!("📖 Reading set.json..."); 14 | let json_content = match fs::read_to_string("src/examples/set/set.json") { 15 | Ok(content) => content, 16 | Err(e) => { 17 | eprintln!("❌ Cannot read set.json file: {}", e); 18 | return; 19 | } 20 | }; 21 | 22 | println!("🔧 Constructing workflow..."); 23 | let workflow = match Workflow::from_json( 24 | &json_content, 25 | "Set Example Workflow".to_string(), 26 | Some("Example workflow demonstrating set node functionality".to_string()), 27 | ) { 28 | Ok(wf) => wf, 29 | Err(e) => { 30 | eprintln!("❌ Cannot construct workflow: {}", e); 31 | return; 32 | } 33 | }; 34 | 35 | println!("✅ Workflow constructed successfully!"); 36 | println!(" - ID: {}", workflow.id); 37 | println!(" - Name: {}", workflow.name); 38 | println!( 39 | " - Description: {}", 40 | workflow.description.as_deref().unwrap_or("None") 41 | ); 42 | println!(" - Number of nodes: {}", workflow.nodes.len()); 43 | println!(" - Number of connections: {}", workflow.connections.len()); 44 | 45 | // 2. Use this workflow to construct an execution with input {} 46 | println!("\n🚀 Constructing execution..."); 47 | let input = json!({}); 48 | let mut execution = Execution::new(&workflow, input); 49 | 50 | println!("✅ Execution constructed successfully!"); 51 | println!(" - Execution ID: {}", execution.id); 52 | println!(" - Current status: {:?}", execution.status()); 53 | println!(" - Execution plan batches: {}", execution.plan.batch.len()); 54 | 55 | // Print execution plan 56 | println!("\n📋 Execution Plan:"); 57 | for (batch_index, batch) in execution.plan.batch.iter().enumerate() { 58 | println!(" Batch {}: {} nodes", batch_index, batch.len()); 59 | for node_id in batch { 60 | if let Some(node) = workflow.nodes.get(node_id) { 61 | println!(" - {} ({})", node.name(), node_id); 62 | } 63 | } 64 | } 65 | 66 | // Print node dependencies 67 | println!("\n🔗 Node Dependencies:"); 68 | for (node_id, node) in &workflow.nodes { 69 | let deps = node.dependencies(); 70 | println!(" {} ({}):", node.name(), node_id); 71 | if deps.is_empty() { 72 | println!(" No dependencies"); 73 | } else { 74 | println!(" Depends on {} nodes:", deps.len()); 75 | for dep_id in &deps { 76 | if let Some(dep_node) = workflow.nodes.get(dep_id) { 77 | println!(" - {} ({})", dep_node.name(), dep_id); 78 | } else { 79 | println!(" - Unknown node ({})", dep_id); 80 | } 81 | } 82 | } 83 | } 84 | 85 | // 3. Execute this execution and print the execution's status and output 86 | println!("\n⚡ Starting workflow execution..."); 87 | 88 | let mut step = 0; 89 | while execution.has_next() { 90 | step += 1; 91 | println!("\n--- Execution Step {} ---", step); 92 | 93 | println!(" Status before execution: {:?}", execution.status()); 94 | let batch_result = execution.execute(); 95 | println!(" Batch execution result: {:?}", batch_result); 96 | println!(" Status after execution: {:?}", execution.status()); 97 | 98 | // Print current node status 99 | println!(" Node status details:"); 100 | for (node_id, status) in &execution.status { 101 | if let Some(node) = workflow.nodes.get(node_id) { 102 | println!(" {} ({}): {:?}", node.name(), node_id, status); 103 | } 104 | } 105 | } 106 | 107 | println!("\n🎯 Workflow execution completed!"); 108 | println!(" - Final execution status: {:?}", execution.status()); 109 | println!(" - Output result count: {}", execution.output.len()); 110 | 111 | // Print detailed output results 112 | println!("\n📊 Execution output results:"); 113 | for (node_id, output) in &execution.output { 114 | if let Some(node) = workflow.nodes.get(node_id) { 115 | println!(" {} ({}):", node.name(), node_id); 116 | println!( 117 | " {}", 118 | serde_json::to_string_pretty(output).unwrap_or_else(|_| "Cannot serialize".to_string()) 119 | ); 120 | } 121 | } 122 | 123 | // 4. Verify output results match expectations 124 | println!("\n🔍 Verifying output results..."); 125 | 126 | let expected_results = vec![ 127 | // trigger node outputs the empty input parameters 128 | ("2f7d3c9a-1b42-4c83-9e5a-3a7f6d2c1b0e", json!({})), 129 | // first set node should output user info 130 | ( 131 | "8e1f2a34-5c67-4b89-a1d2-3f4e5a6b7c8d", 132 | json!({ 133 | "user": "John Doe", 134 | "email": "john.doe@example.com" 135 | }), 136 | ), 137 | // second set node should output user and mobile 138 | ( 139 | "f3a9c1be-7d24-4b8e-9a63-2d5b4c7e1f90", 140 | json!({ 141 | "user": "John Doe", 142 | "mobile": "+0123456789" 143 | }), 144 | ), 145 | // third set node should merge outputs from first two nodes 146 | ( 147 | "a3d7f2c1-6e45-4b9a-8c2f-1d3e5f7a9b20", 148 | json!({ 149 | "user": "John Doe", 150 | "email": "john.doe@example.com", 151 | "mobile": "+0123456789" 152 | }), 153 | ), 154 | ]; 155 | 156 | let mut all_correct = true; 157 | for (expected_node_id_str, expected_output) in expected_results { 158 | let expected_node_id = uuid::Uuid::parse_str(expected_node_id_str).unwrap(); 159 | 160 | if let Some(actual_output) = execution.output.get(&expected_node_id) { 161 | if actual_output == &expected_output { 162 | println!(" ✅ Node {} output correct", expected_node_id_str); 163 | } else { 164 | println!(" ❌ Node {} output mismatch", expected_node_id_str); 165 | println!( 166 | " Expected: {}", 167 | serde_json::to_string_pretty(&expected_output).unwrap() 168 | ); 169 | println!( 170 | " Actual: {}", 171 | serde_json::to_string_pretty(actual_output).unwrap() 172 | ); 173 | all_correct = false; 174 | } 175 | } else { 176 | println!(" ❌ Node {} has no output", expected_node_id_str); 177 | all_correct = false; 178 | } 179 | } 180 | 181 | // Verify final status 182 | let all_success = execution 183 | .status 184 | .values() 185 | .all(|status| matches!(status, Status::Success)); 186 | if all_success { 187 | println!(" ✅ All node execution status correct"); 188 | } else { 189 | println!(" ❌ Some nodes failed to execute"); 190 | all_correct = false; 191 | } 192 | 193 | // Final verification result 194 | println!("\n🎉 Verification result:"); 195 | if all_correct { 196 | println!(" ✅ All verifications passed! Workflow execution results completely match expectations."); 197 | } else { 198 | println!(" ❌ Some verifications failed! Please check output results."); 199 | std::process::exit(1); 200 | } 201 | 202 | println!("\n=== Example execution completed ==="); 203 | } 204 | -------------------------------------------------------------------------------- /src/node/if_node.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use serde_json::{json, Value}; 3 | 4 | use crate::execution::NodeOutput; 5 | use crate::node::{INode, NodeId, Error}; 6 | 7 | #[derive(Debug, Clone, Serialize, Deserialize)] 8 | pub struct IfNode { 9 | pub id: NodeId, 10 | pub name: String, 11 | pub description: Option, 12 | pub parameters: Option, 13 | } 14 | 15 | #[derive(Debug, Clone, Serialize, Deserialize)] 16 | pub struct IfParams { 17 | pub conditions: Vec, 18 | } 19 | 20 | #[derive(Debug, Clone, Serialize, Deserialize)] 21 | pub struct Condition { 22 | pub field: String, 23 | pub operator: String, // "equals", "notEquals", "contains", "greaterThan", "lessThan" 24 | pub value: Value, 25 | } 26 | 27 | impl Default for IfParams { 28 | fn default() -> Self { 29 | Self { 30 | conditions: vec![], 31 | } 32 | } 33 | } 34 | 35 | impl INode for IfNode { 36 | fn new( 37 | id: NodeId, 38 | name: String, 39 | _node_type: String, 40 | description: Option, 41 | parameters: Option, 42 | ) -> Self { 43 | Self { 44 | id, 45 | name, 46 | description, 47 | parameters, 48 | } 49 | } 50 | 51 | fn id(&self) -> NodeId { 52 | self.id 53 | } 54 | 55 | fn name(&self) -> String { 56 | self.name.clone() 57 | } 58 | 59 | fn description(&self) -> Option { 60 | self.description.clone() 61 | } 62 | 63 | fn parameter(&self) -> Option { 64 | self.parameters.clone() 65 | } 66 | 67 | fn execute(&self, input: &NodeOutput) -> Result { 68 | let params = if let Some(params_value) = &self.parameters { 69 | serde_json::from_value::(params_value.clone()) 70 | .unwrap_or_default() 71 | } else { 72 | IfParams::default() 73 | }; 74 | 75 | // Combine all input data from previous nodes 76 | let input_data: Vec = input.values().cloned().collect(); 77 | 78 | // If no input data, create an empty object 79 | let data_to_evaluate = if input_data.is_empty() { 80 | json!({}) 81 | } else { 82 | input_data[0].clone() // Use first input for evaluation 83 | }; 84 | 85 | // Evaluate conditions 86 | let condition_result = self.evaluate_conditions(¶ms.conditions, &data_to_evaluate)?; 87 | 88 | // Return result based on condition evaluation 89 | Ok(json!({ 90 | "conditionMet": condition_result, 91 | "data": data_to_evaluate 92 | })) 93 | } 94 | 95 | fn validate(&self) -> bool { 96 | if let Some(params_value) = &self.parameters { 97 | if let Ok(params) = serde_json::from_value::(params_value.clone()) { 98 | return !params.conditions.is_empty(); 99 | } 100 | } 101 | false 102 | } 103 | 104 | fn dependencies(&self) -> Vec { 105 | vec![] 106 | } 107 | } 108 | 109 | impl IfNode { 110 | fn evaluate_conditions(&self, conditions: &[Condition], data: &Value) -> Result { 111 | // If no conditions, return true (pass through) 112 | if conditions.is_empty() { 113 | return Ok(true); 114 | } 115 | 116 | // Evaluate all conditions (AND logic) 117 | for condition in conditions { 118 | if !self.evaluate_single_condition(condition, data)? { 119 | return Ok(false); 120 | } 121 | } 122 | 123 | Ok(true) 124 | } 125 | 126 | fn evaluate_single_condition(&self, condition: &Condition, data: &Value) -> Result { 127 | // Get the field value from the data 128 | let field_value = self.get_field_value(&condition.field, data)?; 129 | 130 | // Evaluate based on operator 131 | match condition.operator.as_str() { 132 | "equals" => Ok(field_value == condition.value), 133 | "notEquals" => Ok(field_value != condition.value), 134 | "contains" => { 135 | if let (Value::String(haystack), Value::String(needle)) = (&field_value, &condition.value) { 136 | Ok(haystack.contains(needle)) 137 | } else { 138 | Ok(false) 139 | } 140 | } 141 | "greaterThan" => { 142 | match (&field_value, &condition.value) { 143 | (Value::Number(a), Value::Number(b)) => { 144 | if let (Some(a_f64), Some(b_f64)) = (a.as_f64(), b.as_f64()) { 145 | Ok(a_f64 > b_f64) 146 | } else { 147 | Ok(false) 148 | } 149 | } 150 | _ => Ok(false) 151 | } 152 | } 153 | "lessThan" => { 154 | match (&field_value, &condition.value) { 155 | (Value::Number(a), Value::Number(b)) => { 156 | if let (Some(a_f64), Some(b_f64)) = (a.as_f64(), b.as_f64()) { 157 | Ok(a_f64 < b_f64) 158 | } else { 159 | Ok(false) 160 | } 161 | } 162 | _ => Ok(false) 163 | } 164 | } 165 | _ => Err(Error::Validation(format!("Unsupported operator: {}", condition.operator))) 166 | } 167 | } 168 | 169 | fn get_field_value(&self, field_path: &str, data: &Value) -> Result { 170 | // Simple field access - supports dot notation like "user.name" 171 | let parts: Vec<&str> = field_path.split('.').collect(); 172 | let mut current = data; 173 | 174 | for part in parts { 175 | if let Some(next) = current.get(part) { 176 | current = next; 177 | } else { 178 | return Ok(Value::Null); 179 | } 180 | } 181 | 182 | Ok(current.clone()) 183 | } 184 | } 185 | 186 | #[cfg(test)] 187 | mod tests { 188 | use super::*; 189 | use std::collections::HashMap; 190 | 191 | #[test] 192 | fn test_if_node_creation() { 193 | let node = IfNode::new( 194 | uuid::Uuid::new_v4(), 195 | "Test If".to_string(), 196 | "if".to_string(), 197 | Some("Test description".to_string()), 198 | Some(json!({ 199 | "conditions": [{ 200 | "field": "status", 201 | "operator": "equals", 202 | "value": "active" 203 | }] 204 | })), 205 | ); 206 | 207 | assert_eq!(node.name(), "Test If"); 208 | assert!(node.validate()); 209 | } 210 | 211 | #[test] 212 | fn test_if_validation() { 213 | let node = IfNode::new( 214 | uuid::Uuid::new_v4(), 215 | "Test If".to_string(), 216 | "if".to_string(), 217 | None, 218 | Some(json!({ 219 | "conditions": [] 220 | })), 221 | ); 222 | 223 | assert!(!node.validate()); 224 | } 225 | 226 | #[test] 227 | fn test_if_condition_evaluation() { 228 | let node = IfNode::new( 229 | uuid::Uuid::new_v4(), 230 | "Test If".to_string(), 231 | "if".to_string(), 232 | None, 233 | Some(json!({ 234 | "conditions": [{ 235 | "field": "status", 236 | "operator": "equals", 237 | "value": "active" 238 | }] 239 | })), 240 | ); 241 | 242 | let mut input = HashMap::new(); 243 | input.insert(uuid::Uuid::new_v4(), json!({"status": "active", "name": "test"})); 244 | 245 | let result = node.execute(&input); 246 | assert!(result.is_ok()); 247 | 248 | if let Ok(result_value) = result { 249 | assert_eq!(result_value["conditionMet"], true); 250 | } 251 | } 252 | 253 | #[test] 254 | fn test_if_condition_false() { 255 | let node = IfNode::new( 256 | uuid::Uuid::new_v4(), 257 | "Test If".to_string(), 258 | "if".to_string(), 259 | None, 260 | Some(json!({ 261 | "conditions": [{ 262 | "field": "status", 263 | "operator": "equals", 264 | "value": "inactive" 265 | }] 266 | })), 267 | ); 268 | 269 | let mut input = HashMap::new(); 270 | input.insert(uuid::Uuid::new_v4(), json!({"status": "active", "name": "test"})); 271 | 272 | let result = node.execute(&input); 273 | assert!(result.is_ok()); 274 | 275 | if let Ok(result_value) = result { 276 | assert_eq!(result_value["conditionMet"], false); 277 | } 278 | } 279 | } -------------------------------------------------------------------------------- /src/node/filter.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use serde_json::{json, Value}; 3 | 4 | use crate::execution::NodeOutput; 5 | use crate::node::{INode, NodeId, Error}; 6 | 7 | #[derive(Debug, Clone, Serialize, Deserialize)] 8 | pub struct FilterNode { 9 | pub id: NodeId, 10 | pub name: String, 11 | pub description: Option, 12 | pub parameters: Option, 13 | } 14 | 15 | #[derive(Debug, Clone, Serialize, Deserialize)] 16 | pub struct FilterParams { 17 | pub conditions: Vec, 18 | pub combine: String, // "AND" or "OR" 19 | } 20 | 21 | #[derive(Debug, Clone, Serialize, Deserialize)] 22 | pub struct FilterCondition { 23 | pub field: String, 24 | pub operator: String, // "equals", "notEquals", "contains", "greaterThan", "lessThan" 25 | pub value: Value, 26 | } 27 | 28 | impl Default for FilterParams { 29 | fn default() -> Self { 30 | Self { 31 | conditions: vec![], 32 | combine: "AND".to_string(), 33 | } 34 | } 35 | } 36 | 37 | impl INode for FilterNode { 38 | fn new( 39 | id: NodeId, 40 | name: String, 41 | _node_type: String, 42 | description: Option, 43 | parameters: Option, 44 | ) -> Self { 45 | Self { 46 | id, 47 | name, 48 | description, 49 | parameters, 50 | } 51 | } 52 | 53 | fn id(&self) -> NodeId { 54 | self.id 55 | } 56 | 57 | fn name(&self) -> String { 58 | self.name.clone() 59 | } 60 | 61 | fn description(&self) -> Option { 62 | self.description.clone() 63 | } 64 | 65 | fn parameter(&self) -> Option { 66 | self.parameters.clone() 67 | } 68 | 69 | fn execute(&self, input: &NodeOutput) -> Result { 70 | let params = if let Some(params_value) = &self.parameters { 71 | serde_json::from_value::(params_value.clone()) 72 | .unwrap_or_default() 73 | } else { 74 | FilterParams::default() 75 | }; 76 | 77 | // Combine all input data from previous nodes 78 | let input_data: Vec = input.values().cloned().collect(); 79 | 80 | // If no input data, return empty array 81 | if input_data.is_empty() { 82 | return Ok(json!([])); 83 | } 84 | 85 | // Process each input item 86 | let mut filtered_results = Vec::new(); 87 | 88 | for item in input_data { 89 | if let Value::Array(items) = item { 90 | // If input is an array, filter each item 91 | for single_item in items { 92 | if self.passes_filter(¶ms, &single_item)? { 93 | filtered_results.push(single_item); 94 | } 95 | } 96 | } else { 97 | // If input is a single item, filter it 98 | if self.passes_filter(¶ms, &item)? { 99 | filtered_results.push(item); 100 | } 101 | } 102 | } 103 | 104 | Ok(Value::Array(filtered_results)) 105 | } 106 | 107 | fn validate(&self) -> bool { 108 | if let Some(params_value) = &self.parameters { 109 | if let Ok(params) = serde_json::from_value::(params_value.clone()) { 110 | return !params.conditions.is_empty(); 111 | } 112 | } 113 | false 114 | } 115 | 116 | fn dependencies(&self) -> Vec { 117 | vec![] 118 | } 119 | } 120 | 121 | impl FilterNode { 122 | fn passes_filter(&self, params: &FilterParams, item: &Value) -> Result { 123 | // If no conditions, let everything pass 124 | if params.conditions.is_empty() { 125 | return Ok(true); 126 | } 127 | 128 | let results: Result, Error> = params.conditions 129 | .iter() 130 | .map(|condition| self.evaluate_condition(condition, item)) 131 | .collect(); 132 | 133 | let condition_results = results?; 134 | 135 | // Combine results based on combine logic 136 | match params.combine.to_uppercase().as_str() { 137 | "AND" => Ok(condition_results.iter().all(|&result| result)), 138 | "OR" => Ok(condition_results.iter().any(|&result| result)), 139 | _ => Err(Error::Validation(format!("Unsupported combine logic: {}", params.combine))) 140 | } 141 | } 142 | 143 | fn evaluate_condition(&self, condition: &FilterCondition, item: &Value) -> Result { 144 | // Get the field value from the item 145 | let field_value = self.get_field_value(&condition.field, item)?; 146 | 147 | // Evaluate based on operator 148 | match condition.operator.as_str() { 149 | "equals" => Ok(field_value == condition.value), 150 | "notEquals" => Ok(field_value != condition.value), 151 | "contains" => { 152 | if let (Value::String(haystack), Value::String(needle)) = (&field_value, &condition.value) { 153 | Ok(haystack.contains(needle)) 154 | } else { 155 | Ok(false) 156 | } 157 | } 158 | "greaterThan" => { 159 | match (&field_value, &condition.value) { 160 | (Value::Number(a), Value::Number(b)) => { 161 | if let (Some(a_f64), Some(b_f64)) = (a.as_f64(), b.as_f64()) { 162 | Ok(a_f64 > b_f64) 163 | } else { 164 | Ok(false) 165 | } 166 | } 167 | _ => Ok(false) 168 | } 169 | } 170 | "lessThan" => { 171 | match (&field_value, &condition.value) { 172 | (Value::Number(a), Value::Number(b)) => { 173 | if let (Some(a_f64), Some(b_f64)) = (a.as_f64(), b.as_f64()) { 174 | Ok(a_f64 < b_f64) 175 | } else { 176 | Ok(false) 177 | } 178 | } 179 | _ => Ok(false) 180 | } 181 | } 182 | _ => Err(Error::Validation(format!("Unsupported operator: {}", condition.operator))) 183 | } 184 | } 185 | 186 | fn get_field_value(&self, field_path: &str, data: &Value) -> Result { 187 | // Simple field access - supports dot notation like "user.name" 188 | let parts: Vec<&str> = field_path.split('.').collect(); 189 | let mut current = data; 190 | 191 | for part in parts { 192 | if let Some(next) = current.get(part) { 193 | current = next; 194 | } else { 195 | return Ok(Value::Null); 196 | } 197 | } 198 | 199 | Ok(current.clone()) 200 | } 201 | } 202 | 203 | #[cfg(test)] 204 | mod tests { 205 | use super::*; 206 | use std::collections::HashMap; 207 | 208 | #[test] 209 | fn test_filter_node_creation() { 210 | let node = FilterNode::new( 211 | uuid::Uuid::new_v4(), 212 | "Test Filter".to_string(), 213 | "filter".to_string(), 214 | Some("Test description".to_string()), 215 | Some(json!({ 216 | "conditions": [{ 217 | "field": "status", 218 | "operator": "equals", 219 | "value": "active" 220 | }], 221 | "combine": "AND" 222 | })), 223 | ); 224 | 225 | assert_eq!(node.name(), "Test Filter"); 226 | assert!(node.validate()); 227 | } 228 | 229 | #[test] 230 | fn test_filter_validation() { 231 | let node = FilterNode::new( 232 | uuid::Uuid::new_v4(), 233 | "Test Filter".to_string(), 234 | "filter".to_string(), 235 | None, 236 | Some(json!({ 237 | "conditions": [], 238 | "combine": "AND" 239 | })), 240 | ); 241 | 242 | assert!(!node.validate()); 243 | } 244 | 245 | #[test] 246 | fn test_filter_execution() { 247 | let node = FilterNode::new( 248 | uuid::Uuid::new_v4(), 249 | "Test Filter".to_string(), 250 | "filter".to_string(), 251 | None, 252 | Some(json!({ 253 | "conditions": [{ 254 | "field": "status", 255 | "operator": "equals", 256 | "value": "active" 257 | }], 258 | "combine": "AND" 259 | })), 260 | ); 261 | 262 | let mut input = HashMap::new(); 263 | input.insert(uuid::Uuid::new_v4(), json!([ 264 | {"status": "active", "name": "item1"}, 265 | {"status": "inactive", "name": "item2"}, 266 | {"status": "active", "name": "item3"} 267 | ])); 268 | 269 | let result = node.execute(&input); 270 | assert!(result.is_ok()); 271 | 272 | if let Ok(Value::Array(results)) = result { 273 | assert_eq!(results.len(), 2); // Only active items should pass 274 | assert_eq!(results[0]["name"], "item1"); 275 | assert_eq!(results[1]["name"], "item3"); 276 | } 277 | } 278 | } -------------------------------------------------------------------------------- /src/examples/openai/main.rs: -------------------------------------------------------------------------------- 1 | use serde_json::json; 2 | use std::fs; 3 | use glint::{ 4 | execution::{Execution, Status}, 5 | node::INode, 6 | workflow::Workflow, 7 | }; 8 | 9 | fn main() { 10 | println!("=== OpenAI Example Workflow Execution ===\n"); 11 | 12 | // 1. Read openai.json to construct a workflow 13 | println!("📖 Reading openai.json..."); 14 | let json_content = match fs::read_to_string("src/examples/openai/openai.json") { 15 | Ok(content) => content, 16 | Err(e) => { 17 | eprintln!("❌ Cannot read openai.json file: {}", e); 18 | return; 19 | } 20 | }; 21 | 22 | println!("🔧 Constructing workflow..."); 23 | let workflow = match Workflow::from_json( 24 | &json_content, 25 | "OpenAI Example Workflow".to_string(), 26 | Some("Travel plan generation workflow demonstrating OpenAI node functionality".to_string()), 27 | ) { 28 | Ok(wf) => wf, 29 | Err(e) => { 30 | eprintln!("❌ Cannot construct workflow: {}", e); 31 | return; 32 | } 33 | }; 34 | 35 | println!("✅ Workflow constructed successfully!"); 36 | println!(" - ID: {}", workflow.id); 37 | println!(" - Name: {}", workflow.name); 38 | println!( 39 | " - Description: {}", 40 | workflow.description.as_deref().unwrap_or("None") 41 | ); 42 | println!(" - Number of nodes: {}", workflow.nodes.len()); 43 | println!(" - Number of connections: {}", workflow.connections.len()); 44 | 45 | // Print workflow node information 46 | println!("\n📋 Workflow Node List:"); 47 | for (node_id, node) in &workflow.nodes { 48 | println!(" - {} ({}): {}", 49 | node.name(), 50 | node_id, 51 | node.description().unwrap_or_else(|| "No description".to_string()) 52 | ); 53 | } 54 | 55 | // 2. Use this workflow to construct an execution with input {} 56 | println!("\n🚀 Constructing execution..."); 57 | let input = json!({ 58 | "trigger": "Start generating travel plan" 59 | }); 60 | let mut execution = Execution::new(&workflow, input); 61 | 62 | println!("✅ Execution constructed successfully!"); 63 | println!(" - ID: {}", execution.id); 64 | println!(" - Current batch index: {}", execution.current_batch_index); 65 | println!(" - Total batch count: {}", execution.plan.batch.len()); 66 | 67 | // Print execution plan 68 | println!("\n📋 Execution Plan:"); 69 | for (batch_index, batch) in execution.plan.batch.iter().enumerate() { 70 | println!(" Batch {}: {} nodes", batch_index, batch.len()); 71 | for node_id in batch { 72 | if let Some(node) = workflow.nodes.get(node_id) { 73 | println!(" - {} ({})", node.name(), node_id); 74 | } 75 | } 76 | } 77 | 78 | println!("\n🔄 Starting step-by-step execution..."); 79 | 80 | // 3. Step-by-step execution of execution and output step results 81 | let mut step_count = 0; 82 | while execution.has_next() { 83 | step_count += 1; 84 | println!("\n--- Step {} ---", step_count); 85 | 86 | // Display current batch information 87 | let current_batch = execution.plan.batch[execution.current_batch_index].clone(); 88 | println!("📍 Current batch index: {}", execution.current_batch_index); 89 | println!("📍 Current batch node count: {}", current_batch.len()); 90 | 91 | // Display nodes to be executed 92 | println!("🎯 Nodes to be executed:"); 93 | for node_id in ¤t_batch { 94 | if let Some(node) = execution.workflow.nodes.get(node_id) { 95 | println!(" - {} ({})", node.name(), node_id); 96 | 97 | // Display current node status 98 | if let Some(node_status) = execution.status.get(node_id) { 99 | println!(" Status: {:?}", node_status); 100 | } 101 | 102 | // If node has output, display output 103 | if let Some(output) = execution.output.get(node_id) { 104 | println!(" Output: {}", 105 | serde_json::to_string_pretty(&output) 106 | .unwrap_or_else(|_| "Cannot serialize output".to_string()) 107 | ); 108 | } 109 | } 110 | } 111 | 112 | // Execute current batch 113 | println!("⚡ Executing current batch..."); 114 | let batch_result = execution.execute(); 115 | 116 | println!("✅ Batch execution result: {:?}", batch_result); 117 | 118 | // Display node status and output after execution 119 | println!("📊 Node execution results:"); 120 | for node_id in ¤t_batch { 121 | if let Some(node) = execution.workflow.nodes.get(node_id) { 122 | println!(" Node: {} ({})", node.name(), node_id); 123 | 124 | // Display node status 125 | if let Some(node_status) = execution.status.get(node_id) { 126 | println!(" Status: {:?}", node_status); 127 | } 128 | 129 | // Display node output 130 | if let Some(output) = execution.output.get(node_id) { 131 | match output { 132 | serde_json::Value::String(s) if s.len() > 200 => { 133 | println!(" Output: {}...", &s[..200]); 134 | } 135 | _ => { 136 | println!(" Output: {}", 137 | serde_json::to_string_pretty(&output) 138 | .unwrap_or_else(|_| "Cannot serialize output".to_string()) 139 | ); 140 | } 141 | } 142 | } else { 143 | println!(" Output: None"); 144 | } 145 | } 146 | } 147 | 148 | // If batch execution fails, stop execution 149 | if batch_result == Status::Failed { 150 | println!("❌ Batch execution failed, stopping execution"); 151 | break; 152 | } 153 | 154 | println!("⏭️ Moving to next batch..."); 155 | } 156 | 157 | // 4. Final output of execution's status and output 158 | println!("\n🏁 === Execution Completed ==="); 159 | 160 | let final_status = execution.status(); 161 | println!("📊 Final execution status: {:?}", final_status); 162 | 163 | println!("\n📋 Final status of all nodes:"); 164 | for (node_id, node) in &execution.workflow.nodes { 165 | if let Some(node_status) = execution.status.get(node_id) { 166 | println!(" - {} ({}): {:?}", node.name(), node_id, node_status); 167 | } 168 | } 169 | 170 | println!("\n📤 Final output results:"); 171 | if execution.output.is_empty() { 172 | println!(" No output"); 173 | } else { 174 | for (node_id, output) in &execution.output { 175 | if let Some(node) = execution.workflow.nodes.get(node_id) { 176 | println!(" Node: {} ({})", node.name(), node_id); 177 | 178 | // Truncate display for long outputs 179 | match output { 180 | serde_json::Value::String(s) if s.len() > 500 => { 181 | println!(" Output: {}...", &s[..500]); 182 | println!(" (Output truncated, total length: {} characters)", s.len()); 183 | } 184 | serde_json::Value::Object(_) => { 185 | let output_str = serde_json::to_string_pretty(&output) 186 | .unwrap_or_else(|_| "Cannot serialize output".to_string()); 187 | if output_str.len() > 1000 { 188 | // Safe truncation to avoid character boundary issues 189 | let truncated = output_str.chars().take(1000).collect::(); 190 | println!(" Output: {}...", truncated); 191 | println!(" (Output truncated, total length: {} characters)", output_str.chars().count()); 192 | } else { 193 | println!(" Output: {}", output_str); 194 | } 195 | } 196 | _ => { 197 | println!(" Output: {}", 198 | serde_json::to_string_pretty(&output) 199 | .unwrap_or_else(|_| "Cannot serialize output".to_string()) 200 | ); 201 | } 202 | } 203 | } 204 | } 205 | } 206 | 207 | // Summary 208 | println!("\n📈 Execution Summary:"); 209 | println!(" - Total steps: {}", step_count); 210 | println!(" - Total batches: {}", execution.plan.batch.len()); 211 | println!(" - Total nodes: {}", execution.workflow.nodes.len()); 212 | println!(" - Final status: {:?}", final_status); 213 | 214 | match final_status { 215 | Status::Success => println!("🎉 Workflow execution successful!"), 216 | Status::Failed => println!("❌ Workflow execution failed!"), 217 | Status::Running => println!("🔄 Workflow still running..."), 218 | Status::Pending => println!("⏸️ Workflow waiting..."), 219 | } 220 | } 221 | -------------------------------------------------------------------------------- /src/node/code.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use serde_json::{json, Value}; 3 | 4 | use crate::execution::NodeOutput; 5 | use crate::node::{INode, NodeId, Error}; 6 | 7 | #[derive(Debug, Clone, Serialize, Deserialize)] 8 | pub struct CodeNode { 9 | pub id: NodeId, 10 | pub name: String, 11 | pub description: Option, 12 | pub parameters: Option, 13 | } 14 | 15 | #[derive(Debug, Clone, Serialize, Deserialize)] 16 | pub struct CodeParams { 17 | pub mode: String, // "runOnceForAllItems" or "runOnceForEachItem" 18 | pub language: String, // "javascript" or "python" 19 | pub code: String, 20 | } 21 | 22 | impl Default for CodeParams { 23 | fn default() -> Self { 24 | Self { 25 | mode: "runOnceForAllItems".to_string(), 26 | language: "javascript".to_string(), 27 | code: String::new(), 28 | } 29 | } 30 | } 31 | 32 | impl INode for CodeNode { 33 | fn new( 34 | id: NodeId, 35 | name: String, 36 | _node_type: String, 37 | description: Option, 38 | parameters: Option, 39 | ) -> Self { 40 | Self { 41 | id, 42 | name, 43 | description, 44 | parameters, 45 | } 46 | } 47 | 48 | fn id(&self) -> NodeId { 49 | self.id 50 | } 51 | 52 | fn name(&self) -> String { 53 | self.name.clone() 54 | } 55 | 56 | fn description(&self) -> Option { 57 | self.description.clone() 58 | } 59 | 60 | fn parameter(&self) -> Option { 61 | self.parameters.clone() 62 | } 63 | 64 | fn execute(&self, input: &NodeOutput) -> Result { 65 | let params = if let Some(params_value) = &self.parameters { 66 | serde_json::from_value::(params_value.clone()) 67 | .unwrap_or_default() 68 | } else { 69 | CodeParams::default() 70 | }; 71 | 72 | // Validate required parameters 73 | if params.code.is_empty() { 74 | return Err(Error::Validation("Code is required".to_string())); 75 | } 76 | 77 | // For now, we'll implement a simple JavaScript-like evaluation 78 | // In a real implementation, you would use a proper JavaScript engine like V8 or QuickJS 79 | // This is a simplified demonstration 80 | match params.language.as_str() { 81 | "javascript" => self.execute_javascript(¶ms, input), 82 | "python" => Err(Error::Validation("Python execution not yet implemented".to_string())), 83 | _ => Err(Error::Validation(format!("Unsupported language: {}", params.language))), 84 | } 85 | } 86 | 87 | fn validate(&self) -> bool { 88 | if let Some(params_value) = &self.parameters { 89 | if let Ok(params) = serde_json::from_value::(params_value.clone()) { 90 | return !params.code.is_empty(); 91 | } 92 | } 93 | false 94 | } 95 | 96 | fn dependencies(&self) -> Vec { 97 | vec![] 98 | } 99 | } 100 | 101 | impl CodeNode { 102 | fn execute_javascript(&self, params: &CodeParams, input: &NodeOutput) -> Result { 103 | // This is a simplified implementation for demonstration purposes 104 | // In a real system, you would use a proper JavaScript engine 105 | 106 | // Combine all input data from previous nodes 107 | let input_data: Vec = input.values().cloned().collect(); 108 | 109 | // If no input data, create an empty array 110 | let data_to_process = if input_data.is_empty() { 111 | vec![json!({})] 112 | } else { 113 | input_data 114 | }; 115 | 116 | match params.mode.as_str() { 117 | "runOnceForAllItems" => { 118 | // Process all items at once 119 | self.execute_simple_javascript(¶ms.code, &json!(data_to_process)) 120 | } 121 | "runOnceForEachItem" => { 122 | // Process each item individually 123 | let mut results = Vec::new(); 124 | for item in data_to_process { 125 | let result = self.execute_simple_javascript(¶ms.code, &item)?; 126 | if let Value::Array(arr) = result { 127 | results.extend(arr); 128 | } else { 129 | results.push(result); 130 | } 131 | } 132 | Ok(Value::Array(results)) 133 | } 134 | _ => Err(Error::Validation(format!("Unsupported mode: {}", params.mode))), 135 | } 136 | } 137 | 138 | fn execute_simple_javascript(&self, code: &str, input: &Value) -> Result { 139 | // This is a very simplified JavaScript-like evaluation 140 | // In practice, you would use a proper JavaScript engine 141 | 142 | // Support for simple patterns like: 143 | // - return $input.item.field 144 | // - return { newField: $input.item.oldField } 145 | // - return $input.all() 146 | 147 | if code.contains("$input.all()") { 148 | return Ok(input.clone()); 149 | } 150 | 151 | if code.contains("$input.item") && input.is_object() { 152 | // Simple field access patterns 153 | if let Some(field_access) = self.extract_field_access(code) { 154 | if let Some(field_value) = input.get(&field_access) { 155 | return Ok(json!({ field_access: field_value })); 156 | } 157 | } 158 | 159 | // Return the input item as-is for now 160 | return Ok(input.clone()); 161 | } 162 | 163 | // For other cases, try to create a simple transformation 164 | if code.contains("return {") && code.contains("}") { 165 | // Try to extract simple object creation 166 | return self.execute_object_creation(code, input); 167 | } 168 | 169 | // Default: return input as-is 170 | Ok(input.clone()) 171 | } 172 | 173 | fn extract_field_access(&self, code: &str) -> Option { 174 | // Simple regex-like extraction for $input.item.fieldName 175 | if let Some(start) = code.find("$input.item.") { 176 | let field_start = start + "$input.item.".len(); 177 | let remaining = &code[field_start..]; 178 | 179 | // Find the end of the field name 180 | let field_end = remaining.find(|c: char| !c.is_alphanumeric() && c != '_') 181 | .unwrap_or(remaining.len()); 182 | 183 | return Some(remaining[..field_end].to_string()); 184 | } 185 | None 186 | } 187 | 188 | fn execute_object_creation(&self, _code: &str, input: &Value) -> Result { 189 | // Very simplified object creation handling 190 | // This would need a proper parser in a real implementation 191 | 192 | // For now, just return the input wrapped in a simple object 193 | Ok(json!({ 194 | "result": input, 195 | "processed": true 196 | })) 197 | } 198 | } 199 | 200 | #[cfg(test)] 201 | mod tests { 202 | use super::*; 203 | 204 | #[test] 205 | fn test_code_node_creation() { 206 | let node = CodeNode::new( 207 | uuid::Uuid::new_v4(), 208 | "Test Code".to_string(), 209 | "code".to_string(), 210 | Some("Test description".to_string()), 211 | Some(json!({ 212 | "mode": "runOnceForAllItems", 213 | "language": "javascript", 214 | "code": "return $input.all()" 215 | })), 216 | ); 217 | 218 | assert_eq!(node.name(), "Test Code"); 219 | assert!(node.validate()); 220 | } 221 | 222 | #[test] 223 | fn test_code_validation() { 224 | let node = CodeNode::new( 225 | uuid::Uuid::new_v4(), 226 | "Test Code".to_string(), 227 | "code".to_string(), 228 | None, 229 | Some(json!({ 230 | "mode": "runOnceForAllItems", 231 | "language": "javascript", 232 | "code": "" 233 | })), 234 | ); 235 | 236 | assert!(!node.validate()); 237 | } 238 | 239 | #[test] 240 | fn test_simple_javascript_execution() { 241 | let node = CodeNode::new( 242 | uuid::Uuid::new_v4(), 243 | "Test Code".to_string(), 244 | "code".to_string(), 245 | None, 246 | Some(json!({ 247 | "mode": "runOnceForAllItems", 248 | "language": "javascript", 249 | "code": "return $input.all()" 250 | })), 251 | ); 252 | 253 | let mut input = NodeOutput::new(); 254 | input.insert(uuid::Uuid::new_v4(), json!({"name": "test", "value": 123})); 255 | 256 | let result = node.execute(&input); 257 | assert!(result.is_ok()); 258 | } 259 | 260 | #[test] 261 | fn test_unsupported_language() { 262 | let node = CodeNode::new( 263 | uuid::Uuid::new_v4(), 264 | "Test Code".to_string(), 265 | "code".to_string(), 266 | None, 267 | Some(json!({ 268 | "mode": "runOnceForAllItems", 269 | "language": "python", 270 | "code": "return input" 271 | })), 272 | ); 273 | 274 | let input = NodeOutput::new(); 275 | 276 | let result = node.execute(&input); 277 | assert!(result.is_err()); 278 | } 279 | } -------------------------------------------------------------------------------- /src/node/http_request.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::time::Duration; 3 | 4 | use base64::{Engine as _, engine::general_purpose}; 5 | use reqwest; 6 | use serde::{Deserialize, Serialize}; 7 | use serde_json::{json, Value}; 8 | 9 | use crate::execution::NodeOutput; 10 | use crate::node::{INode, NodeId, Error}; 11 | 12 | #[derive(Debug, Clone, Serialize, Deserialize)] 13 | pub struct HttpRequestNode { 14 | pub id: NodeId, 15 | pub name: String, 16 | pub description: Option, 17 | pub parameters: Option, 18 | } 19 | 20 | #[derive(Debug, Clone, Serialize, Deserialize)] 21 | pub struct HttpRequestParams { 22 | pub method: String, 23 | pub url: String, 24 | pub headers: Option>, 25 | pub query_params: Option>, 26 | pub body: Option, 27 | pub timeout: Option, 28 | pub response_format: Option, // "json", "text", "file" 29 | pub full_response: Option, 30 | } 31 | 32 | impl Default for HttpRequestParams { 33 | fn default() -> Self { 34 | Self { 35 | method: "GET".to_string(), 36 | url: String::new(), 37 | headers: None, 38 | query_params: None, 39 | body: None, 40 | timeout: Some(30), // 30 seconds default 41 | response_format: Some("json".to_string()), 42 | full_response: Some(false), 43 | } 44 | } 45 | } 46 | 47 | impl INode for HttpRequestNode { 48 | fn new( 49 | id: NodeId, 50 | name: String, 51 | _node_type: String, 52 | description: Option, 53 | parameters: Option, 54 | ) -> Self { 55 | Self { 56 | id, 57 | name, 58 | description, 59 | parameters, 60 | } 61 | } 62 | 63 | fn id(&self) -> NodeId { 64 | self.id 65 | } 66 | 67 | fn name(&self) -> String { 68 | self.name.clone() 69 | } 70 | 71 | fn description(&self) -> Option { 72 | self.description.clone() 73 | } 74 | 75 | fn parameter(&self) -> Option { 76 | self.parameters.clone() 77 | } 78 | 79 | fn execute(&self, _input: &NodeOutput) -> Result { 80 | let params = if let Some(params_value) = &self.parameters { 81 | serde_json::from_value::(params_value.clone()) 82 | .unwrap_or_default() 83 | } else { 84 | HttpRequestParams::default() 85 | }; 86 | 87 | // Validate required parameters 88 | if params.url.is_empty() { 89 | return Err(Error::Validation("URL is required".to_string())); 90 | } 91 | 92 | // Create HTTP client 93 | let client = reqwest::blocking::Client::builder() 94 | .timeout(Duration::from_secs(params.timeout.unwrap_or(30))) 95 | .build() 96 | .map_err(|e| Error::NodeExecution(format!("Failed to create HTTP client: {}", e)))?; 97 | 98 | // Build request 99 | let method = match params.method.to_uppercase().as_str() { 100 | "GET" => reqwest::Method::GET, 101 | "POST" => reqwest::Method::POST, 102 | "PUT" => reqwest::Method::PUT, 103 | "DELETE" => reqwest::Method::DELETE, 104 | "PATCH" => reqwest::Method::PATCH, 105 | "HEAD" => reqwest::Method::HEAD, 106 | "OPTIONS" => reqwest::Method::OPTIONS, 107 | _ => return Err(Error::Validation(format!("Unsupported HTTP method: {}", params.method))), 108 | }; 109 | 110 | let mut request_builder = client.request(method, ¶ms.url); 111 | 112 | // Add headers 113 | if let Some(headers) = ¶ms.headers { 114 | for (key, value) in headers { 115 | request_builder = request_builder.header(key, value); 116 | } 117 | } 118 | 119 | // Add query parameters 120 | if let Some(query_params) = ¶ms.query_params { 121 | for (key, value) in query_params { 122 | request_builder = request_builder.query(&[(key, value)]); 123 | } 124 | } 125 | 126 | // Add body for methods that support it 127 | if matches!(params.method.to_uppercase().as_str(), "POST" | "PUT" | "PATCH") { 128 | if let Some(body) = ¶ms.body { 129 | match body { 130 | Value::String(s) => { 131 | request_builder = request_builder.body(s.clone()); 132 | } 133 | _ => { 134 | request_builder = request_builder.json(body); 135 | } 136 | } 137 | } 138 | } 139 | 140 | // Execute request 141 | let response = request_builder 142 | .send() 143 | .map_err(|e| Error::NodeExecution(format!("HTTP request failed: {}", e)))?; 144 | 145 | // Get response data 146 | let status_code = response.status().as_u16(); 147 | let headers: HashMap = response 148 | .headers() 149 | .iter() 150 | .map(|(name, value)| { 151 | ( 152 | name.as_str().to_string(), 153 | value.to_str().unwrap_or("").to_string(), 154 | ) 155 | }) 156 | .collect(); 157 | 158 | let response_format = params.response_format.unwrap_or_else(|| "json".to_string()); 159 | let full_response = params.full_response.unwrap_or(false); 160 | 161 | // Handle response based on format 162 | let body_value = match response_format.as_str() { 163 | "json" => { 164 | let text = response 165 | .text() 166 | .map_err(|e| Error::NodeExecution(format!("Failed to read response body: {}", e)))?; 167 | 168 | if text.is_empty() { 169 | Value::Null 170 | } else { 171 | serde_json::from_str(&text) 172 | .unwrap_or_else(|_| Value::String(text)) 173 | } 174 | } 175 | "text" => { 176 | let text = response 177 | .text() 178 | .map_err(|e| Error::NodeExecution(format!("Failed to read response body: {}", e)))?; 179 | Value::String(text) 180 | } 181 | "file" => { 182 | let bytes = response 183 | .bytes() 184 | .map_err(|e| Error::NodeExecution(format!("Failed to read response bytes: {}", e)))?; 185 | 186 | // For file responses, we encode as base64 for JSON serialization 187 | let base64_data = general_purpose::STANDARD.encode(&bytes); 188 | json!({ 189 | "data": base64_data, 190 | "contentType": headers.get("content-type").unwrap_or(&"application/octet-stream".to_string()), 191 | "size": bytes.len() 192 | }) 193 | } 194 | _ => { 195 | return Err(Error::Validation(format!("Unsupported response format: {}", response_format))); 196 | } 197 | }; 198 | 199 | // Return response based on full_response setting 200 | let result = if full_response { 201 | json!({ 202 | "body": body_value, 203 | "headers": headers, 204 | "statusCode": status_code, 205 | "statusMessage": status_code.to_string() 206 | }) 207 | } else { 208 | body_value 209 | }; 210 | 211 | Ok(result) 212 | } 213 | 214 | fn validate(&self) -> bool { 215 | if let Some(params_value) = &self.parameters { 216 | if let Ok(params) = serde_json::from_value::(params_value.clone()) { 217 | return !params.url.is_empty(); 218 | } 219 | } 220 | false 221 | } 222 | 223 | fn dependencies(&self) -> Vec { 224 | vec![] 225 | } 226 | } 227 | 228 | #[cfg(test)] 229 | mod tests { 230 | use super::*; 231 | use uuid::Uuid; 232 | 233 | #[test] 234 | fn test_http_request_node_creation() { 235 | let node = HttpRequestNode::new( 236 | Uuid::new_v4(), 237 | "Test HTTP Request".to_string(), 238 | "http_request".to_string(), 239 | Some("Test description".to_string()), 240 | Some(json!({ 241 | "method": "GET", 242 | "url": "https://httpbin.org/get" 243 | })), 244 | ); 245 | 246 | assert_eq!(node.name(), "Test HTTP Request"); 247 | assert!(node.validate()); 248 | } 249 | 250 | #[test] 251 | fn test_http_request_validation() { 252 | let node = HttpRequestNode::new( 253 | Uuid::new_v4(), 254 | "Test HTTP Request".to_string(), 255 | "http_request".to_string(), 256 | None, 257 | Some(json!({ 258 | "method": "GET", 259 | "url": "" 260 | })), 261 | ); 262 | 263 | assert!(!node.validate()); 264 | } 265 | 266 | #[test] 267 | fn test_http_request_execute_get() { 268 | let node = HttpRequestNode::new( 269 | Uuid::new_v4(), 270 | "Test HTTP Request".to_string(), 271 | "http_request".to_string(), 272 | None, 273 | Some(json!({ 274 | "method": "GET", 275 | "url": "https://httpbin.org/get", 276 | "response_format": "json" 277 | })), 278 | ); 279 | 280 | let output = NodeOutput::new(); 281 | let result = node.execute(&output); 282 | assert!(result.is_ok()); 283 | } 284 | 285 | #[test] 286 | fn test_http_request_execute_post() { 287 | let node = HttpRequestNode::new( 288 | Uuid::new_v4(), 289 | "Test HTTP Request".to_string(), 290 | "http_request".to_string(), 291 | None, 292 | Some(json!({ 293 | "method": "POST", 294 | "url": "https://httpbin.org/post", 295 | "body": {"test": "data"}, 296 | "response_format": "json" 297 | })), 298 | ); 299 | 300 | let output = NodeOutput::new(); 301 | let result = node.execute(&output); 302 | assert!(result.is_ok()); 303 | } 304 | } -------------------------------------------------------------------------------- /src/node/limit.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use serde_json::{json, Value}; 3 | 4 | use crate::execution::NodeOutput; 5 | use crate::node::{INode, NodeId, Error}; 6 | 7 | #[derive(Debug, Clone, Serialize, Deserialize)] 8 | pub struct LimitNode { 9 | pub id: NodeId, 10 | pub name: String, 11 | pub description: Option, 12 | pub parameters: Option, 13 | } 14 | 15 | #[derive(Debug, Clone, Serialize, Deserialize)] 16 | pub struct LimitParams { 17 | #[serde(default = "default_limit")] 18 | pub limit: usize, 19 | #[serde(default = "default_false")] 20 | pub keep_only_set: bool, 21 | #[serde(default = "default_false")] 22 | pub skip_first: bool, 23 | #[serde(default)] 24 | pub skip_count: Option, 25 | } 26 | 27 | fn default_limit() -> usize { 1 } 28 | fn default_false() -> bool { false } 29 | 30 | impl Default for LimitParams { 31 | fn default() -> Self { 32 | Self { 33 | limit: 1, 34 | keep_only_set: false, 35 | skip_first: false, 36 | skip_count: None, 37 | } 38 | } 39 | } 40 | 41 | impl INode for LimitNode { 42 | fn new( 43 | id: NodeId, 44 | name: String, 45 | _node_type: String, 46 | description: Option, 47 | parameters: Option, 48 | ) -> Self { 49 | Self { 50 | id, 51 | name, 52 | description, 53 | parameters, 54 | } 55 | } 56 | 57 | fn id(&self) -> NodeId { 58 | self.id 59 | } 60 | 61 | fn name(&self) -> String { 62 | self.name.clone() 63 | } 64 | 65 | fn description(&self) -> Option { 66 | self.description.clone() 67 | } 68 | 69 | fn parameter(&self) -> Option { 70 | self.parameters.clone() 71 | } 72 | 73 | fn execute(&self, input: &NodeOutput) -> Result { 74 | let params = if let Some(params_value) = &self.parameters { 75 | serde_json::from_value::(params_value.clone()) 76 | .unwrap_or_default() 77 | } else { 78 | LimitParams::default() 79 | }; 80 | 81 | // Process input data 82 | let input_data: Vec = input.values().cloned().collect(); 83 | 84 | if input_data.is_empty() { 85 | return Ok(json!([])); 86 | } 87 | 88 | // Flatten all items into a single array 89 | let mut all_items = Vec::new(); 90 | for item in input_data { 91 | if let Value::Array(items) = item { 92 | all_items.extend(items); 93 | } else { 94 | all_items.push(item); 95 | } 96 | } 97 | 98 | // Apply limiting logic 99 | let limited_items = self.apply_limit(¶ms, all_items)?; 100 | 101 | Ok(Value::Array(limited_items)) 102 | } 103 | 104 | fn validate(&self) -> bool { 105 | if let Some(params_value) = &self.parameters { 106 | if let Ok(params) = serde_json::from_value::(params_value.clone()) { 107 | return params.limit > 0; 108 | } 109 | } 110 | true // Default is valid 111 | } 112 | 113 | fn dependencies(&self) -> Vec { 114 | vec![] 115 | } 116 | } 117 | 118 | impl LimitNode { 119 | fn apply_limit(&self, params: &LimitParams, mut items: Vec) -> Result, Error> { 120 | let total_items = items.len(); 121 | 122 | // Calculate skip count 123 | let skip_count = if params.skip_first { 124 | params.skip_count.unwrap_or(1) 125 | } else { 126 | 0 127 | }; 128 | 129 | // Skip items if needed 130 | if skip_count > 0 { 131 | if skip_count >= total_items { 132 | return Ok(vec![]); 133 | } 134 | items = items.into_iter().skip(skip_count).collect(); 135 | } 136 | 137 | // Apply limit 138 | if params.keep_only_set { 139 | // Keep only the items within the limit range 140 | items.truncate(params.limit); 141 | } else { 142 | // Take the first N items 143 | items.truncate(params.limit); 144 | } 145 | 146 | Ok(items) 147 | } 148 | } 149 | 150 | #[cfg(test)] 151 | mod tests { 152 | use super::*; 153 | use std::collections::HashMap; 154 | 155 | #[test] 156 | fn test_limit_node_creation() { 157 | let node = LimitNode::new( 158 | uuid::Uuid::new_v4(), 159 | "Test Limit".to_string(), 160 | "limit".to_string(), 161 | Some("Test description".to_string()), 162 | Some(json!({ 163 | "limit": 3 164 | })), 165 | ); 166 | 167 | assert_eq!(node.name(), "Test Limit"); 168 | assert!(node.validate()); 169 | } 170 | 171 | #[test] 172 | fn test_limit_validation() { 173 | let node = LimitNode::new( 174 | uuid::Uuid::new_v4(), 175 | "Test Limit".to_string(), 176 | "limit".to_string(), 177 | None, 178 | Some(json!({ 179 | "limit": 0 180 | })), 181 | ); 182 | 183 | assert!(!node.validate()); 184 | } 185 | 186 | #[test] 187 | fn test_basic_limit() { 188 | let node = LimitNode::new( 189 | uuid::Uuid::new_v4(), 190 | "Test Limit".to_string(), 191 | "limit".to_string(), 192 | None, 193 | Some(json!({ 194 | "limit": 2 195 | })), 196 | ); 197 | 198 | let mut input = HashMap::new(); 199 | input.insert( 200 | uuid::Uuid::new_v4(), 201 | json!([ 202 | {"id": 1, "name": "first"}, 203 | {"id": 2, "name": "second"}, 204 | {"id": 3, "name": "third"}, 205 | {"id": 4, "name": "fourth"} 206 | ]) 207 | ); 208 | 209 | let result = node.execute(&input); 210 | assert!(result.is_ok()); 211 | 212 | if let Ok(Value::Array(results)) = result { 213 | assert_eq!(results.len(), 2); 214 | assert_eq!(results[0]["id"], 1); 215 | assert_eq!(results[1]["id"], 2); 216 | } 217 | } 218 | 219 | #[test] 220 | fn test_limit_with_skip() { 221 | let node = LimitNode::new( 222 | uuid::Uuid::new_v4(), 223 | "Test Limit".to_string(), 224 | "limit".to_string(), 225 | None, 226 | Some(json!({ 227 | "limit": 2, 228 | "skip_first": true, 229 | "skip_count": 1 230 | })), 231 | ); 232 | 233 | let mut input = HashMap::new(); 234 | input.insert( 235 | uuid::Uuid::new_v4(), 236 | json!([ 237 | {"id": 1, "name": "first"}, 238 | {"id": 2, "name": "second"}, 239 | {"id": 3, "name": "third"}, 240 | {"id": 4, "name": "fourth"} 241 | ]) 242 | ); 243 | 244 | let result = node.execute(&input); 245 | assert!(result.is_ok()); 246 | 247 | if let Ok(Value::Array(results)) = result { 248 | assert_eq!(results.len(), 2); 249 | assert_eq!(results[0]["id"], 2); // Skipped first item 250 | assert_eq!(results[1]["id"], 3); 251 | } 252 | } 253 | 254 | #[test] 255 | fn test_limit_larger_than_items() { 256 | let node = LimitNode::new( 257 | uuid::Uuid::new_v4(), 258 | "Test Limit".to_string(), 259 | "limit".to_string(), 260 | None, 261 | Some(json!({ 262 | "limit": 10 263 | })), 264 | ); 265 | 266 | let mut input = HashMap::new(); 267 | input.insert( 268 | uuid::Uuid::new_v4(), 269 | json!([ 270 | {"id": 1, "name": "first"}, 271 | {"id": 2, "name": "second"} 272 | ]) 273 | ); 274 | 275 | let result = node.execute(&input); 276 | assert!(result.is_ok()); 277 | 278 | if let Ok(Value::Array(results)) = result { 279 | assert_eq!(results.len(), 2); // Should return all items if limit is larger 280 | assert_eq!(results[0]["id"], 1); 281 | assert_eq!(results[1]["id"], 2); 282 | } 283 | } 284 | 285 | #[test] 286 | fn test_skip_all_items() { 287 | let node = LimitNode::new( 288 | uuid::Uuid::new_v4(), 289 | "Test Limit".to_string(), 290 | "limit".to_string(), 291 | None, 292 | Some(json!({ 293 | "limit": 2, 294 | "skip_first": true, 295 | "skip_count": 5 296 | })), 297 | ); 298 | 299 | let mut input = HashMap::new(); 300 | input.insert( 301 | uuid::Uuid::new_v4(), 302 | json!([ 303 | {"id": 1, "name": "first"}, 304 | {"id": 2, "name": "second"} 305 | ]) 306 | ); 307 | 308 | let result = node.execute(&input); 309 | assert!(result.is_ok()); 310 | 311 | if let Ok(Value::Array(results)) = result { 312 | assert_eq!(results.len(), 0); // Should return empty array if skip count >= total items 313 | } 314 | } 315 | 316 | #[test] 317 | fn test_multiple_input_arrays() { 318 | let node = LimitNode::new( 319 | uuid::Uuid::new_v4(), 320 | "Test Limit".to_string(), 321 | "limit".to_string(), 322 | None, 323 | Some(json!({ 324 | "limit": 3 325 | })), 326 | ); 327 | 328 | let mut input = HashMap::new(); 329 | input.insert( 330 | uuid::Uuid::new_v4(), 331 | json!([ 332 | {"id": 1, "source": "A"}, 333 | {"id": 2, "source": "A"} 334 | ]) 335 | ); 336 | input.insert( 337 | uuid::Uuid::new_v4(), 338 | json!([ 339 | {"id": 3, "source": "B"}, 340 | {"id": 4, "source": "B"} 341 | ]) 342 | ); 343 | 344 | let result = node.execute(&input); 345 | assert!(result.is_ok()); 346 | 347 | if let Ok(Value::Array(results)) = result { 348 | assert_eq!(results.len(), 3); // Should take first 3 items from combined arrays 349 | } 350 | } 351 | 352 | #[test] 353 | fn test_single_item_input() { 354 | let node = LimitNode::new( 355 | uuid::Uuid::new_v4(), 356 | "Test Limit".to_string(), 357 | "limit".to_string(), 358 | None, 359 | Some(json!({ 360 | "limit": 1 361 | })), 362 | ); 363 | 364 | let mut input = HashMap::new(); 365 | input.insert( 366 | uuid::Uuid::new_v4(), 367 | json!({"id": 1, "name": "single"}) 368 | ); 369 | 370 | let result = node.execute(&input); 371 | assert!(result.is_ok()); 372 | 373 | if let Ok(Value::Array(results)) = result { 374 | assert_eq!(results.len(), 1); 375 | assert_eq!(results[0]["id"], 1); 376 | } 377 | } 378 | 379 | #[test] 380 | fn test_empty_input() { 381 | let node = LimitNode::new( 382 | uuid::Uuid::new_v4(), 383 | "Test Limit".to_string(), 384 | "limit".to_string(), 385 | None, 386 | Some(json!({ 387 | "limit": 5 388 | })), 389 | ); 390 | 391 | let input = HashMap::new(); 392 | let result = node.execute(&input); 393 | assert!(result.is_ok()); 394 | 395 | if let Ok(Value::Array(results)) = result { 396 | assert_eq!(results.len(), 0); 397 | } 398 | } 399 | } -------------------------------------------------------------------------------- /src/node/execute_command.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use serde_json::{json, Value}; 3 | use std::process::{Command, Stdio}; 4 | 5 | use crate::execution::NodeOutput; 6 | use crate::node::{INode, NodeId, Error}; 7 | 8 | #[derive(Debug, Clone, Serialize, Deserialize)] 9 | pub struct ExecuteCommandNode { 10 | pub id: NodeId, 11 | pub name: String, 12 | pub description: Option, 13 | pub parameters: Option, 14 | } 15 | 16 | #[derive(Debug, Clone, Serialize, Deserialize)] 17 | pub struct ExecuteCommandParams { 18 | #[serde(default)] 19 | pub command: String, 20 | #[serde(default = "default_true")] 21 | pub execute_once: bool, 22 | #[serde(default)] 23 | pub working_directory: Option, 24 | #[serde(default)] 25 | pub environment_variables: Vec, 26 | #[serde(default = "default_timeout")] 27 | pub timeout_seconds: u64, 28 | } 29 | 30 | #[derive(Debug, Clone, Serialize, Deserialize)] 31 | pub struct EnvVar { 32 | pub name: String, 33 | pub value: String, 34 | } 35 | 36 | fn default_true() -> bool { true } 37 | fn default_timeout() -> u64 { 60 } 38 | 39 | impl Default for ExecuteCommandParams { 40 | fn default() -> Self { 41 | Self { 42 | command: String::new(), 43 | execute_once: true, 44 | working_directory: None, 45 | environment_variables: vec![], 46 | timeout_seconds: 60, 47 | } 48 | } 49 | } 50 | 51 | #[derive(Debug, Serialize, Deserialize)] 52 | pub struct CommandResult { 53 | pub exit_code: i32, 54 | pub stdout: String, 55 | pub stderr: String, 56 | pub success: bool, 57 | } 58 | 59 | impl INode for ExecuteCommandNode { 60 | fn new( 61 | id: NodeId, 62 | name: String, 63 | _node_type: String, 64 | description: Option, 65 | parameters: Option, 66 | ) -> Self { 67 | Self { 68 | id, 69 | name, 70 | description, 71 | parameters, 72 | } 73 | } 74 | 75 | fn id(&self) -> NodeId { 76 | self.id 77 | } 78 | 79 | fn name(&self) -> String { 80 | self.name.clone() 81 | } 82 | 83 | fn description(&self) -> Option { 84 | self.description.clone() 85 | } 86 | 87 | fn parameter(&self) -> Option { 88 | self.parameters.clone() 89 | } 90 | 91 | fn execute(&self, input: &NodeOutput) -> Result { 92 | let params = if let Some(params_value) = &self.parameters { 93 | serde_json::from_value::(params_value.clone()) 94 | .unwrap_or_default() 95 | } else { 96 | ExecuteCommandParams::default() 97 | }; 98 | 99 | if params.command.trim().is_empty() { 100 | return Err(Error::Validation("Command cannot be empty".to_string())); 101 | } 102 | 103 | // Process input data 104 | let input_data: Vec = input.values().cloned().collect(); 105 | let mut results = Vec::new(); 106 | 107 | if params.execute_once { 108 | // Execute command only once regardless of input items count 109 | let result = self.execute_single_command(¶ms)?; 110 | results.push(json!(result)); 111 | } else { 112 | // Execute command for each input item 113 | for _item in input_data { 114 | let result = self.execute_single_command(¶ms)?; 115 | results.push(json!(result)); 116 | } 117 | } 118 | 119 | if results.is_empty() { 120 | results.push(json!(self.execute_single_command(¶ms)?)); 121 | } 122 | 123 | Ok(Value::Array(results)) 124 | } 125 | 126 | fn validate(&self) -> bool { 127 | if let Some(params_value) = &self.parameters { 128 | if let Ok(params) = serde_json::from_value::(params_value.clone()) { 129 | return !params.command.trim().is_empty(); 130 | } 131 | } 132 | true // Allow nodes without parameters 133 | } 134 | 135 | fn dependencies(&self) -> Vec { 136 | vec![] 137 | } 138 | } 139 | 140 | impl ExecuteCommandNode { 141 | fn execute_single_command(&self, params: &ExecuteCommandParams) -> Result { 142 | // Parse command - simple implementation for shell commands 143 | let command_parts = self.parse_command(¶ms.command); 144 | if command_parts.is_empty() { 145 | return Err(Error::NodeExecution("Invalid command format".to_string())); 146 | } 147 | 148 | let program = &command_parts[0]; 149 | let args = if command_parts.len() > 1 { 150 | &command_parts[1..] 151 | } else { 152 | &[] 153 | }; 154 | 155 | let mut cmd = Command::new(program); 156 | cmd.args(args) 157 | .stdout(Stdio::piped()) 158 | .stderr(Stdio::piped()); 159 | 160 | // Set working directory if specified 161 | if let Some(working_dir) = ¶ms.working_directory { 162 | cmd.current_dir(working_dir); 163 | } 164 | 165 | // Set environment variables 166 | for env_var in ¶ms.environment_variables { 167 | cmd.env(&env_var.name, &env_var.value); 168 | } 169 | 170 | // Execute command with timeout 171 | let output = cmd.output() 172 | .map_err(|e| Error::NodeExecution(format!("Failed to execute command: {}", e)))?; 173 | 174 | let stdout = String::from_utf8_lossy(&output.stdout).to_string(); 175 | let stderr = String::from_utf8_lossy(&output.stderr).to_string(); 176 | let exit_code = output.status.code().unwrap_or(-1); 177 | let success = output.status.success(); 178 | 179 | Ok(CommandResult { 180 | exit_code, 181 | stdout: stdout.trim().to_string(), 182 | stderr: stderr.trim().to_string(), 183 | success, 184 | }) 185 | } 186 | 187 | fn parse_command(&self, command: &str) -> Vec { 188 | // Simple command parsing - split by spaces, handle basic quoted strings 189 | let mut parts = Vec::new(); 190 | let mut current_part = String::new(); 191 | let mut in_quotes = false; 192 | let mut escape_next = false; 193 | let mut quote_char = ' '; 194 | 195 | for ch in command.chars() { 196 | if escape_next { 197 | current_part.push(ch); 198 | escape_next = false; 199 | continue; 200 | } 201 | 202 | match ch { 203 | '\\' => { 204 | escape_next = true; 205 | } 206 | '"' | '\'' => { 207 | if !in_quotes { 208 | // Start quoted section 209 | in_quotes = true; 210 | quote_char = ch; 211 | } else if ch == quote_char { 212 | // End quoted section 213 | in_quotes = false; 214 | quote_char = ' '; 215 | } else { 216 | // Different quote inside quotes 217 | current_part.push(ch); 218 | } 219 | } 220 | ' ' if !in_quotes => { 221 | if !current_part.is_empty() { 222 | parts.push(current_part.clone()); 223 | current_part.clear(); 224 | } 225 | } 226 | _ => { 227 | current_part.push(ch); 228 | } 229 | } 230 | } 231 | 232 | if !current_part.is_empty() { 233 | parts.push(current_part); 234 | } 235 | 236 | parts 237 | } 238 | } 239 | 240 | #[cfg(test)] 241 | mod tests { 242 | use super::*; 243 | use std::collections::HashMap; 244 | 245 | #[test] 246 | fn test_execute_command_node_creation() { 247 | let node = ExecuteCommandNode::new( 248 | uuid::Uuid::new_v4(), 249 | "Test Execute Command".to_string(), 250 | "executeCommand".to_string(), 251 | Some("Test description".to_string()), 252 | Some(json!({ 253 | "command": "echo 'Hello World'" 254 | })), 255 | ); 256 | 257 | assert_eq!(node.name(), "Test Execute Command"); 258 | assert!(node.validate()); 259 | } 260 | 261 | #[test] 262 | fn test_execute_command_validation() { 263 | let node = ExecuteCommandNode::new( 264 | uuid::Uuid::new_v4(), 265 | "Test Execute Command".to_string(), 266 | "executeCommand".to_string(), 267 | None, 268 | Some(json!({ 269 | "command": "" 270 | })), 271 | ); 272 | 273 | assert!(!node.validate()); 274 | } 275 | 276 | #[test] 277 | fn test_echo_command() { 278 | let node = ExecuteCommandNode::new( 279 | uuid::Uuid::new_v4(), 280 | "Test Execute Command".to_string(), 281 | "executeCommand".to_string(), 282 | None, 283 | Some(json!({ 284 | "command": "echo 'Hello World'", 285 | "execute_once": true 286 | })), 287 | ); 288 | 289 | let input = HashMap::new(); 290 | let result = node.execute(&input); 291 | assert!(result.is_ok()); 292 | 293 | if let Ok(Value::Array(results)) = result { 294 | assert_eq!(results.len(), 1); 295 | let command_result = &results[0]; 296 | assert!(command_result["success"].as_bool().unwrap_or(false)); 297 | assert_eq!(command_result["exit_code"].as_i64().unwrap_or(-1), 0); 298 | } 299 | } 300 | 301 | #[test] 302 | fn test_command_parsing() { 303 | let node = ExecuteCommandNode::new( 304 | uuid::Uuid::new_v4(), 305 | "Test".to_string(), 306 | "executeCommand".to_string(), 307 | None, 308 | None, 309 | ); 310 | 311 | let parsed = node.parse_command("echo 'hello world'"); 312 | assert_eq!(parsed, vec!["echo", "hello world"]); 313 | 314 | let parsed = node.parse_command("ls -la"); 315 | assert_eq!(parsed, vec!["ls", "-la"]); 316 | 317 | let parsed = node.parse_command("cat file.txt"); 318 | assert_eq!(parsed, vec!["cat", "file.txt"]); 319 | } 320 | 321 | #[test] 322 | fn test_invalid_command() { 323 | let node = ExecuteCommandNode::new( 324 | uuid::Uuid::new_v4(), 325 | "Test Execute Command".to_string(), 326 | "executeCommand".to_string(), 327 | None, 328 | Some(json!({ 329 | "command": "nonexistent_command_that_should_fail", 330 | "execute_once": true 331 | })), 332 | ); 333 | 334 | let input = HashMap::new(); 335 | let result = node.execute(&input); 336 | 337 | // Execute should return Ok but the command result should show failure 338 | if result.is_err() { 339 | // If execute returns error, that's also acceptable for invalid commands 340 | assert!(true); 341 | } else if let Ok(Value::Array(results)) = result { 342 | assert_eq!(results.len(), 1); 343 | // The command should fail but not throw an exception 344 | let command_result = &results[0]; 345 | assert!(!command_result["success"].as_bool().unwrap_or(true)); 346 | } 347 | } 348 | 349 | #[test] 350 | fn test_multiple_executions() { 351 | let node = ExecuteCommandNode::new( 352 | uuid::Uuid::new_v4(), 353 | "Test Execute Command".to_string(), 354 | "executeCommand".to_string(), 355 | None, 356 | Some(json!({ 357 | "command": "echo 'test'", 358 | "execute_once": false 359 | })), 360 | ); 361 | 362 | let mut input = HashMap::new(); 363 | input.insert(uuid::Uuid::new_v4(), json!({"item": 1})); 364 | input.insert(uuid::Uuid::new_v4(), json!({"item": 2})); 365 | 366 | let result = node.execute(&input); 367 | assert!(result.is_ok()); 368 | 369 | if let Ok(Value::Array(results)) = result { 370 | assert_eq!(results.len(), 2); // Should execute twice for two input items 371 | } 372 | } 373 | 374 | #[test] 375 | fn test_working_directory() { 376 | let node = ExecuteCommandNode::new( 377 | uuid::Uuid::new_v4(), 378 | "Test Execute Command".to_string(), 379 | "executeCommand".to_string(), 380 | None, 381 | Some(json!({ 382 | "command": "pwd", 383 | "execute_once": true, 384 | "working_directory": "/tmp" 385 | })), 386 | ); 387 | 388 | let input = HashMap::new(); 389 | let result = node.execute(&input); 390 | 391 | if let Ok(Value::Array(results)) = result { 392 | assert_eq!(results.len(), 1); 393 | let command_result = &results[0]; 394 | if command_result["success"].as_bool().unwrap_or(false) { 395 | let stdout = command_result["stdout"].as_str().unwrap_or(""); 396 | // pwd command should return the working directory 397 | assert!(stdout.contains("tmp") || stdout == "/tmp"); 398 | } 399 | } 400 | } 401 | } -------------------------------------------------------------------------------- /src/node/webhook.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use serde_json::{json, Value}; 3 | use std::collections::HashMap; 4 | 5 | use crate::execution::NodeOutput; 6 | use crate::node::{INode, NodeId, Error}; 7 | 8 | #[derive(Debug, Clone, Serialize, Deserialize)] 9 | pub struct WebhookNode { 10 | pub id: NodeId, 11 | pub name: String, 12 | pub description: Option, 13 | pub parameters: Option, 14 | } 15 | 16 | #[derive(Debug, Clone, Serialize, Deserialize)] 17 | pub struct WebhookParams { 18 | pub path: String, 19 | pub method: String, // "GET", "POST", "PUT", "DELETE" 20 | pub response_mode: String, // "on_received", "last_node", "first_node" 21 | pub response_data: Option, 22 | pub response_code: Option, 23 | pub response_headers: Option>, 24 | pub authentication: Option, // "none", "basic", "bearer" 25 | pub webhook_id: Option, 26 | } 27 | 28 | impl Default for WebhookParams { 29 | fn default() -> Self { 30 | Self { 31 | path: "/webhook".to_string(), 32 | method: "POST".to_string(), 33 | response_mode: "on_received".to_string(), 34 | response_data: None, 35 | response_code: Some(200), 36 | response_headers: None, 37 | authentication: Some("none".to_string()), 38 | webhook_id: None, 39 | } 40 | } 41 | } 42 | 43 | impl INode for WebhookNode { 44 | fn new( 45 | id: NodeId, 46 | name: String, 47 | _node_type: String, 48 | description: Option, 49 | parameters: Option, 50 | ) -> Self { 51 | Self { 52 | id, 53 | name, 54 | description, 55 | parameters, 56 | } 57 | } 58 | 59 | fn id(&self) -> NodeId { 60 | self.id 61 | } 62 | 63 | fn name(&self) -> String { 64 | self.name.clone() 65 | } 66 | 67 | fn description(&self) -> Option { 68 | self.description.clone() 69 | } 70 | 71 | fn parameter(&self) -> Option { 72 | self.parameters.clone() 73 | } 74 | 75 | fn execute(&self, input: &NodeOutput) -> Result { 76 | let params = if let Some(params_value) = &self.parameters { 77 | serde_json::from_value::(params_value.clone()) 78 | .unwrap_or_default() 79 | } else { 80 | WebhookParams::default() 81 | }; 82 | 83 | // In a real implementation, this would set up a webhook endpoint 84 | // For this simulation, we'll create a webhook configuration and 85 | // simulate receiving data 86 | 87 | match params.response_mode.as_str() { 88 | "on_received" => self.execute_on_received(¶ms, input), 89 | "last_node" => self.execute_last_node(¶ms, input), 90 | "first_node" => self.execute_first_node(¶ms, input), 91 | _ => Err(Error::Validation(format!("Unsupported response mode: {}", params.response_mode))) 92 | } 93 | } 94 | 95 | fn validate(&self) -> bool { 96 | if let Some(params_value) = &self.parameters { 97 | if let Ok(params) = serde_json::from_value::(params_value.clone()) { 98 | return !params.path.is_empty() && 99 | matches!(params.method.to_uppercase().as_str(), "GET" | "POST" | "PUT" | "DELETE" | "PATCH"); 100 | } 101 | } 102 | false 103 | } 104 | 105 | fn dependencies(&self) -> Vec { 106 | vec![] 107 | } 108 | } 109 | 110 | impl WebhookNode { 111 | fn execute_on_received(&self, params: &WebhookParams, _input: &NodeOutput) -> Result { 112 | // Simulate immediate response when webhook is received 113 | let webhook_url = format!("http://localhost:5678/webhook{}", params.path); 114 | let default_id = self.id.to_string(); 115 | let webhook_id = params.webhook_id.as_ref() 116 | .unwrap_or(&default_id); 117 | 118 | let response = json!({ 119 | "webhook": { 120 | "id": webhook_id, 121 | "url": webhook_url, 122 | "method": params.method.to_uppercase(), 123 | "path": params.path, 124 | "status": "active", 125 | "response_mode": params.response_mode, 126 | "response_code": params.response_code.unwrap_or(200) 127 | }, 128 | "message": "Webhook endpoint configured successfully", 129 | "test_data": self.create_test_response(¶ms) 130 | }); 131 | 132 | Ok(response) 133 | } 134 | 135 | fn execute_last_node(&self, params: &WebhookParams, input: &NodeOutput) -> Result { 136 | // Use data from the last node in the workflow 137 | let input_data: Vec = input.values().cloned().collect(); 138 | let last_data = input_data.last().cloned().unwrap_or(json!({})); 139 | 140 | let webhook_url = format!("http://localhost:5678/webhook{}", params.path); 141 | let default_id = self.id.to_string(); 142 | let webhook_id = params.webhook_id.as_ref() 143 | .unwrap_or(&default_id); 144 | 145 | let response = json!({ 146 | "webhook": { 147 | "id": webhook_id, 148 | "url": webhook_url, 149 | "method": params.method.to_uppercase(), 150 | "path": params.path, 151 | "status": "active", 152 | "response_mode": params.response_mode 153 | }, 154 | "response_data": last_data, 155 | "headers": params.response_headers.clone().unwrap_or_default(), 156 | "status_code": params.response_code.unwrap_or(200) 157 | }); 158 | 159 | Ok(response) 160 | } 161 | 162 | fn execute_first_node(&self, params: &WebhookParams, input: &NodeOutput) -> Result { 163 | // Use data from the first node in the workflow 164 | let input_data: Vec = input.values().cloned().collect(); 165 | let first_data = input_data.first().cloned().unwrap_or(json!({})); 166 | 167 | let webhook_url = format!("http://localhost:5678/webhook{}", params.path); 168 | let default_id = self.id.to_string(); 169 | let webhook_id = params.webhook_id.as_ref() 170 | .unwrap_or(&default_id); 171 | 172 | let response = json!({ 173 | "webhook": { 174 | "id": webhook_id, 175 | "url": webhook_url, 176 | "method": params.method.to_uppercase(), 177 | "path": params.path, 178 | "status": "active", 179 | "response_mode": params.response_mode 180 | }, 181 | "response_data": first_data, 182 | "headers": params.response_headers.clone().unwrap_or_default(), 183 | "status_code": params.response_code.unwrap_or(200) 184 | }); 185 | 186 | Ok(response) 187 | } 188 | 189 | fn create_test_response(&self, params: &WebhookParams) -> Value { 190 | // Create a test response based on the configured parameters 191 | if let Some(response_data) = ¶ms.response_data { 192 | response_data.clone() 193 | } else { 194 | json!({ 195 | "status": "success", 196 | "message": "Webhook received successfully", 197 | "timestamp": chrono::Utc::now().to_rfc3339(), 198 | "method": params.method.to_uppercase() 199 | }) 200 | } 201 | } 202 | 203 | pub fn simulate_webhook_call(&self, payload: Value) -> Result { 204 | // Simulate an incoming webhook call 205 | let params = if let Some(params_value) = &self.parameters { 206 | match serde_json::from_value::(params_value.clone()) { 207 | Ok(p) => p, 208 | Err(_) => { 209 | // If deserialization fails, try to extract individual fields 210 | let mut default_params = WebhookParams::default(); 211 | if let Some(path) = params_value.get("path").and_then(|v| v.as_str()) { 212 | default_params.path = path.to_string(); 213 | } 214 | if let Some(method) = params_value.get("method").and_then(|v| v.as_str()) { 215 | default_params.method = method.to_string(); 216 | } 217 | if let Some(auth) = params_value.get("authentication").and_then(|v| v.as_str()) { 218 | default_params.authentication = Some(auth.to_string()); 219 | } 220 | default_params 221 | } 222 | } 223 | } else { 224 | WebhookParams::default() 225 | }; 226 | 227 | let default_id = self.id.to_string(); 228 | let default_auth = "none".to_string(); 229 | let response = json!({ 230 | "received_at": chrono::Utc::now().to_rfc3339(), 231 | "method": params.method.to_uppercase(), 232 | "path": params.path, 233 | "payload": payload, 234 | "webhook_id": params.webhook_id.as_ref().unwrap_or(&default_id), 235 | "authentication": params.authentication.as_ref().unwrap_or(&default_auth) 236 | }); 237 | 238 | Ok(response) 239 | } 240 | } 241 | 242 | #[cfg(test)] 243 | mod tests { 244 | use super::*; 245 | use uuid::Uuid; 246 | 247 | #[test] 248 | fn test_webhook_node_creation() { 249 | let node = WebhookNode::new( 250 | Uuid::new_v4(), 251 | "Test Webhook".to_string(), 252 | "webhook".to_string(), 253 | Some("Test description".to_string()), 254 | Some(json!({ 255 | "path": "/test-webhook", 256 | "method": "POST", 257 | "response_mode": "on_received" 258 | })), 259 | ); 260 | 261 | assert_eq!(node.name(), "Test Webhook"); 262 | assert!(node.validate()); 263 | } 264 | 265 | #[test] 266 | fn test_webhook_validation() { 267 | let node = WebhookNode::new( 268 | Uuid::new_v4(), 269 | "Test Webhook".to_string(), 270 | "webhook".to_string(), 271 | None, 272 | Some(json!({ 273 | "path": "", 274 | "method": "INVALID", 275 | "response_mode": "on_received" 276 | })), 277 | ); 278 | 279 | assert!(!node.validate()); 280 | } 281 | 282 | #[test] 283 | fn test_webhook_on_received_execution() { 284 | let node = WebhookNode::new( 285 | Uuid::new_v4(), 286 | "Test Webhook".to_string(), 287 | "webhook".to_string(), 288 | None, 289 | Some(json!({ 290 | "path": "/test", 291 | "method": "POST", 292 | "response_mode": "on_received", 293 | "response_code": 201 294 | })), 295 | ); 296 | 297 | let input = std::collections::HashMap::new(); 298 | let result = node.execute(&input); 299 | assert!(result.is_ok()); 300 | 301 | if let Ok(response) = result { 302 | assert!(response["webhook"]["url"].as_str().unwrap().contains("/test")); 303 | assert_eq!(response["webhook"]["method"], "POST"); 304 | assert_eq!(response["webhook"]["response_code"], 201); 305 | assert_eq!(response["webhook"]["status"], "active"); 306 | } 307 | } 308 | 309 | #[test] 310 | fn test_webhook_last_node_execution() { 311 | let node = WebhookNode::new( 312 | Uuid::new_v4(), 313 | "Test Webhook".to_string(), 314 | "webhook".to_string(), 315 | None, 316 | Some(json!({ 317 | "path": "/test", 318 | "method": "GET", 319 | "response_mode": "last_node" 320 | })), 321 | ); 322 | 323 | let mut input = std::collections::HashMap::new(); 324 | input.insert(Uuid::new_v4(), json!({"data": "first"})); 325 | input.insert(Uuid::new_v4(), json!({"data": "last"})); 326 | 327 | let result = node.execute(&input); 328 | assert!(result.is_ok()); 329 | 330 | if let Ok(response) = result { 331 | // Check that we got the last data (there are two items, so either could be last) 332 | assert!(response["response_data"]["data"].is_string()); 333 | assert_eq!(response["webhook"]["response_mode"], "last_node"); 334 | } 335 | } 336 | 337 | #[test] 338 | fn test_webhook_simulate_call() { 339 | let node = WebhookNode::new( 340 | Uuid::new_v4(), 341 | "Test Webhook".to_string(), 342 | "webhook".to_string(), 343 | None, 344 | Some(json!({ 345 | "path": "/test", 346 | "method": "POST", 347 | "authentication": "bearer" 348 | })), 349 | ); 350 | 351 | let payload = json!({"message": "test payload"}); 352 | let result = node.simulate_webhook_call(payload.clone()); 353 | assert!(result.is_ok()); 354 | 355 | if let Ok(response) = result { 356 | assert_eq!(response["payload"], payload); 357 | assert_eq!(response["method"], "POST"); 358 | assert_eq!(response["path"], "/test"); 359 | assert_eq!(response["authentication"], "bearer"); 360 | assert!(response["received_at"].is_string()); 361 | } 362 | } 363 | } -------------------------------------------------------------------------------- /src/node/wait.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use serde_json::{json, Value}; 3 | use std::thread; 4 | use std::time::Duration; 5 | use chrono::{DateTime, Utc, NaiveDateTime}; 6 | 7 | use crate::execution::NodeOutput; 8 | use crate::node::{INode, NodeId, Error}; 9 | 10 | #[derive(Debug, Clone, Serialize, Deserialize)] 11 | pub struct WaitNode { 12 | pub id: NodeId, 13 | pub name: String, 14 | pub description: Option, 15 | pub parameters: Option, 16 | } 17 | 18 | #[derive(Debug, Clone, Serialize, Deserialize)] 19 | pub struct WaitParams { 20 | pub resume: String, // "timeInterval", "specificTime" 21 | #[serde(default = "default_amount")] 22 | pub amount: f64, // For timeInterval mode 23 | #[serde(default = "default_unit")] 24 | pub unit: String, // "seconds", "minutes", "hours", "days" 25 | pub date_time: Option, // For specificTime mode (ISO 8601 format) 26 | #[serde(default = "default_true")] 27 | pub include_input_fields: bool, // Whether to include original fields in output 28 | } 29 | 30 | fn default_amount() -> f64 { 31 | 5.0 32 | } 33 | 34 | fn default_unit() -> String { 35 | "seconds".to_string() 36 | } 37 | 38 | fn default_true() -> bool { 39 | true 40 | } 41 | 42 | impl Default for WaitParams { 43 | fn default() -> Self { 44 | Self { 45 | resume: "timeInterval".to_string(), 46 | amount: 5.0, 47 | unit: "seconds".to_string(), 48 | date_time: None, 49 | include_input_fields: true, 50 | } 51 | } 52 | } 53 | 54 | impl INode for WaitNode { 55 | fn new( 56 | id: NodeId, 57 | name: String, 58 | _node_type: String, 59 | description: Option, 60 | parameters: Option, 61 | ) -> Self { 62 | Self { 63 | id, 64 | name, 65 | description, 66 | parameters, 67 | } 68 | } 69 | 70 | fn id(&self) -> NodeId { 71 | self.id 72 | } 73 | 74 | fn name(&self) -> String { 75 | self.name.clone() 76 | } 77 | 78 | fn description(&self) -> Option { 79 | self.description.clone() 80 | } 81 | 82 | fn parameter(&self) -> Option { 83 | self.parameters.clone() 84 | } 85 | 86 | fn execute(&self, input: &NodeOutput) -> Result { 87 | let params = if let Some(params_value) = &self.parameters { 88 | serde_json::from_value::(params_value.clone()) 89 | .unwrap_or_default() 90 | } else { 91 | WaitParams::default() 92 | }; 93 | 94 | // Perform the wait operation 95 | match params.resume.as_str() { 96 | "timeInterval" => self.wait_time_interval(¶ms)?, 97 | "specificTime" => self.wait_specific_time(¶ms)?, 98 | _ => return Err(Error::Validation(format!("Unsupported resume mode: {}", params.resume))) 99 | } 100 | 101 | // After waiting, return the input data 102 | let input_data: Vec = input.values().cloned().collect(); 103 | 104 | if params.include_input_fields && !input_data.is_empty() { 105 | // Return the original input data with timing information 106 | let mut results = Vec::new(); 107 | for item in input_data { 108 | if let Value::Array(items) = item { 109 | for single_item in items { 110 | let mut result = single_item; 111 | if let Value::Object(ref mut map) = result { 112 | map.insert("waitCompleted".to_string(), json!(true)); 113 | map.insert("waitCompletedAt".to_string(), json!(Utc::now().to_rfc3339())); 114 | } 115 | results.push(result); 116 | } 117 | } else { 118 | let mut result = item; 119 | if let Value::Object(ref mut map) = result { 120 | map.insert("waitCompleted".to_string(), json!(true)); 121 | map.insert("waitCompletedAt".to_string(), json!(Utc::now().to_rfc3339())); 122 | } 123 | results.push(result); 124 | } 125 | } 126 | Ok(Value::Array(results)) 127 | } else { 128 | // Return just the timing information 129 | Ok(json!([{ 130 | "waitCompleted": true, 131 | "waitCompletedAt": Utc::now().to_rfc3339(), 132 | "waitDuration": format!("{} {}", params.amount, params.unit) 133 | }])) 134 | } 135 | } 136 | 137 | fn validate(&self) -> bool { 138 | if let Some(params_value) = &self.parameters { 139 | if let Ok(params) = serde_json::from_value::(params_value.clone()) { 140 | return matches!(params.resume.as_str(), "timeInterval" | "specificTime") && 141 | (params.resume != "timeInterval" || (params.amount >= 0.0 && matches!(params.unit.as_str(), "seconds" | "minutes" | "hours" | "days"))) && 142 | (params.resume != "specificTime" || params.date_time.is_some()); 143 | } 144 | } 145 | false 146 | } 147 | 148 | fn dependencies(&self) -> Vec { 149 | vec![] 150 | } 151 | } 152 | 153 | impl WaitNode { 154 | fn wait_time_interval(&self, params: &WaitParams) -> Result<(), Error> { 155 | if params.amount < 0.0 { 156 | return Err(Error::Validation("Wait amount cannot be negative".to_string())); 157 | } 158 | 159 | // Convert the amount to milliseconds 160 | let mut wait_amount_ms = (params.amount * 1000.0) as u64; 161 | 162 | match params.unit.as_str() { 163 | "seconds" => { 164 | // Already in correct units (converted to ms above) 165 | } 166 | "minutes" => { 167 | wait_amount_ms *= 60; 168 | } 169 | "hours" => { 170 | wait_amount_ms *= 60 * 60; 171 | } 172 | "days" => { 173 | wait_amount_ms *= 60 * 60 * 24; 174 | } 175 | _ => return Err(Error::Validation(format!("Unsupported time unit: {}", params.unit))) 176 | } 177 | 178 | // For very short waits (< 1ms), just return immediately 179 | if wait_amount_ms == 0 { 180 | return Ok(()); 181 | } 182 | 183 | // Perform the actual wait 184 | thread::sleep(Duration::from_millis(wait_amount_ms)); 185 | 186 | Ok(()) 187 | } 188 | 189 | fn wait_specific_time(&self, params: &WaitParams) -> Result<(), Error> { 190 | let date_time_str = params.date_time.as_ref() 191 | .ok_or_else(|| Error::Validation("date_time is required for specificTime mode".to_string()))?; 192 | 193 | // Parse the date time string (expecting ISO 8601 format) 194 | let target_time = self.parse_datetime(date_time_str)?; 195 | let now = Utc::now(); 196 | 197 | if target_time <= now { 198 | // If the target time is in the past or now, don't wait 199 | return Ok(()); 200 | } 201 | 202 | let duration = target_time.signed_duration_since(now); 203 | let wait_duration = Duration::from_millis(duration.num_milliseconds() as u64); 204 | 205 | // Perform the actual wait 206 | thread::sleep(wait_duration); 207 | 208 | Ok(()) 209 | } 210 | 211 | fn parse_datetime(&self, date_time_str: &str) -> Result, Error> { 212 | // Try to parse as ISO 8601 format first 213 | if let Ok(dt) = DateTime::parse_from_rfc3339(date_time_str) { 214 | return Ok(dt.with_timezone(&Utc)); 215 | } 216 | 217 | // Try to parse as naive datetime and assume UTC 218 | if let Ok(naive_dt) = NaiveDateTime::parse_from_str(date_time_str, "%Y-%m-%d %H:%M:%S") { 219 | return Ok(DateTime::::from_naive_utc_and_offset(naive_dt, Utc)); 220 | } 221 | 222 | // Try to parse date only and assume start of day 223 | if let Ok(date) = chrono::NaiveDate::parse_from_str(date_time_str, "%Y-%m-%d") { 224 | let naive_dt = date.and_hms_opt(0, 0, 0).unwrap(); 225 | return Ok(DateTime::::from_naive_utc_and_offset(naive_dt, Utc)); 226 | } 227 | 228 | Err(Error::Validation(format!("Invalid date time format: {}. Expected ISO 8601 format (e.g., '2023-12-31T23:59:59Z') or YYYY-MM-DD HH:MM:SS", date_time_str))) 229 | } 230 | } 231 | 232 | #[cfg(test)] 233 | mod tests { 234 | use super::*; 235 | use std::collections::HashMap; 236 | use std::time::Instant; 237 | 238 | #[test] 239 | fn test_wait_node_creation() { 240 | let node = WaitNode::new( 241 | uuid::Uuid::new_v4(), 242 | "Test Wait".to_string(), 243 | "wait".to_string(), 244 | Some("Test description".to_string()), 245 | Some(json!({ 246 | "resume": "timeInterval", 247 | "amount": 1.0, 248 | "unit": "seconds" 249 | })), 250 | ); 251 | 252 | assert_eq!(node.name(), "Test Wait"); 253 | assert!(node.validate()); 254 | } 255 | 256 | #[test] 257 | fn test_wait_validation() { 258 | let node = WaitNode::new( 259 | uuid::Uuid::new_v4(), 260 | "Test Wait".to_string(), 261 | "wait".to_string(), 262 | None, 263 | Some(json!({ 264 | "resume": "invalid_mode", 265 | "amount": 1.0, 266 | "unit": "seconds" 267 | })), 268 | ); 269 | 270 | assert!(!node.validate()); 271 | } 272 | 273 | #[test] 274 | fn test_wait_time_interval_execution() { 275 | let node = WaitNode::new( 276 | uuid::Uuid::new_v4(), 277 | "Test Wait".to_string(), 278 | "wait".to_string(), 279 | None, 280 | Some(json!({ 281 | "resume": "timeInterval", 282 | "amount": 0.1, // 100ms wait 283 | "unit": "seconds", 284 | "include_input_fields": true 285 | })), 286 | ); 287 | 288 | let mut input = HashMap::new(); 289 | input.insert( 290 | uuid::Uuid::new_v4(), 291 | json!({ 292 | "message": "test data", 293 | "value": 123 294 | }) 295 | ); 296 | 297 | let start_time = Instant::now(); 298 | let result = node.execute(&input); 299 | let elapsed = start_time.elapsed(); 300 | 301 | assert!(result.is_ok()); 302 | assert!(elapsed >= Duration::from_millis(100)); // Should have waited at least 100ms 303 | 304 | if let Ok(Value::Array(results)) = result { 305 | assert_eq!(results.len(), 1); 306 | let item = &results[0]; 307 | assert_eq!(item["message"].as_str().unwrap(), "test data"); 308 | assert_eq!(item["value"].as_i64().unwrap(), 123); 309 | assert_eq!(item["waitCompleted"].as_bool().unwrap(), true); 310 | assert!(item["waitCompletedAt"].is_string()); 311 | } 312 | } 313 | 314 | #[test] 315 | fn test_wait_specific_time_validation() { 316 | let node = WaitNode::new( 317 | uuid::Uuid::new_v4(), 318 | "Test Wait".to_string(), 319 | "wait".to_string(), 320 | None, 321 | Some(json!({ 322 | "resume": "specificTime", 323 | "date_time": "2099-12-31T23:59:59Z" 324 | })), 325 | ); 326 | 327 | assert!(node.validate()); 328 | } 329 | 330 | #[test] 331 | fn test_wait_specific_time_past_date() { 332 | let node = WaitNode::new( 333 | uuid::Uuid::new_v4(), 334 | "Test Wait".to_string(), 335 | "wait".to_string(), 336 | None, 337 | Some(json!({ 338 | "resume": "specificTime", 339 | "date_time": "2020-01-01T00:00:00Z", // Past date 340 | "include_input_fields": false 341 | })), 342 | ); 343 | 344 | let input = HashMap::new(); 345 | let start_time = Instant::now(); 346 | let result = node.execute(&input); 347 | let elapsed = start_time.elapsed(); 348 | 349 | assert!(result.is_ok()); 350 | assert!(elapsed < Duration::from_millis(10)); // Should not have waited 351 | 352 | if let Ok(Value::Array(results)) = result { 353 | assert_eq!(results.len(), 1); 354 | let item = &results[0]; 355 | assert_eq!(item["waitCompleted"].as_bool().unwrap(), true); 356 | } 357 | } 358 | 359 | #[test] 360 | fn test_parse_datetime_formats() { 361 | let node = WaitNode::new( 362 | uuid::Uuid::new_v4(), 363 | "Test Wait".to_string(), 364 | "wait".to_string(), 365 | None, 366 | None, 367 | ); 368 | 369 | // Test ISO 8601 format 370 | assert!(node.parse_datetime("2023-12-31T23:59:59Z").is_ok()); 371 | assert!(node.parse_datetime("2023-12-31T23:59:59+00:00").is_ok()); 372 | 373 | // Test simple datetime format 374 | assert!(node.parse_datetime("2023-12-31 23:59:59").is_ok()); 375 | 376 | // Test date only format 377 | assert!(node.parse_datetime("2023-12-31").is_ok()); 378 | 379 | // Test invalid format 380 | assert!(node.parse_datetime("invalid-date").is_err()); 381 | } 382 | 383 | #[test] 384 | fn test_wait_different_units() { 385 | let test_cases = vec![ 386 | ("seconds", 0.01), // 10ms 387 | ("minutes", 0.0001), // 6ms (0.0001 * 60 * 1000) 388 | ("hours", 0.000001), // ~3.6ms 389 | ]; 390 | 391 | for (unit, amount) in test_cases { 392 | let node = WaitNode::new( 393 | uuid::Uuid::new_v4(), 394 | "Test Wait".to_string(), 395 | "wait".to_string(), 396 | None, 397 | Some(json!({ 398 | "resume": "timeInterval", 399 | "amount": amount, 400 | "unit": unit, 401 | "include_input_fields": false 402 | })), 403 | ); 404 | 405 | let input = HashMap::new(); 406 | let result = node.execute(&input); 407 | assert!(result.is_ok(), "Failed for unit: {}", unit); 408 | } 409 | } 410 | } -------------------------------------------------------------------------------- /src/expression/mod.rs: -------------------------------------------------------------------------------- 1 | use serde_json::Value; 2 | use uuid::Uuid; 3 | 4 | use crate::execution::NodeOutput; 5 | 6 | pub fn is_expression(expression: &str) -> bool { 7 | expression.starts_with('=') 8 | } 9 | 10 | /// Extract value from expression 11 | /// Expression format: =${node_id}.json_path 12 | /// Example: =${8e1f2a34-5c67-4b89-a1d2-3f4e5a6b7c8d}.user.name 13 | /// Or: =${8e1f2a34-5c67-4b89-a1d2-3f4e5a6b7c8d}.items[0].title 14 | pub fn extract_value(expression: &str, input: &NodeOutput) -> Option { 15 | // Check if expression starts with = 16 | if !expression.starts_with('=') { 17 | return None; 18 | } 19 | 20 | // Remove leading = symbol 21 | let expr = &expression[1..]; 22 | 23 | // Parse node ID and path 24 | let (node_id, json_path) = parse_expression(expr)?; 25 | 26 | // Get corresponding node value from input 27 | let node_value = input.get(&node_id)?; 28 | 29 | // Navigate JSON value according to path 30 | navigate_json_path(node_value, &json_path) 31 | } 32 | 33 | /// Parse expression, extract node ID and JSON path 34 | /// Input: ${node_id}.json_path 35 | /// Returns: (node_id, json_path) 36 | fn parse_expression(expr: &str) -> Option<(Uuid, String)> { 37 | // Check if it starts with ${ 38 | if !expr.starts_with("${") { 39 | return None; 40 | } 41 | 42 | // Find position of } 43 | let close_brace = expr.find('}')?; 44 | 45 | // Extract node ID string 46 | let node_id_str = &expr[2..close_brace]; 47 | let node_id = Uuid::parse_str(node_id_str).ok()?; 48 | 49 | // Extract JSON path (if exists) 50 | let json_path = 51 | if close_brace + 1 < expr.len() && expr.chars().nth(close_brace + 1) == Some('.') { 52 | expr[close_brace + 2..].to_string() 53 | } else { 54 | String::new() 55 | }; 56 | 57 | Some((node_id, json_path)) 58 | } 59 | 60 | /// Validate if expression is legal 61 | /// Check expression format, node ID validity and JSON path syntax 62 | pub fn validate_expression(expression: &str) -> Result<(), String> { 63 | // Check if expression starts with = 64 | if !expression.starts_with('=') { 65 | return Err("Expression must start with '='".to_string()); 66 | } 67 | 68 | // Remove leading = symbol 69 | let expr = &expression[1..]; 70 | 71 | // Check if it starts with ${ 72 | if !expr.starts_with("${") { 73 | return Err("Expression must contain '${node_id}' format".to_string()); 74 | } 75 | 76 | // Find position of } 77 | let close_brace = expr 78 | .find('}') 79 | .ok_or_else(|| "Missing closing '}' symbol".to_string())?; 80 | 81 | // Check node ID part 82 | let node_id_str = &expr[2..close_brace]; 83 | if node_id_str.is_empty() { 84 | return Err("Node ID cannot be empty".to_string()); 85 | } 86 | 87 | // Validate if node ID is a valid UUID 88 | Uuid::parse_str(node_id_str).map_err(|_| format!("Invalid node ID format: {}", node_id_str))?; 89 | 90 | // Extract JSON path part 91 | let remaining = &expr[close_brace + 1..]; 92 | 93 | // If there's a path part, it must start with . 94 | if !remaining.is_empty() { 95 | if !remaining.starts_with('.') { 96 | return Err("JSON path must start with '.'".to_string()); 97 | } 98 | 99 | // Check if there's only a . without path content 100 | if remaining == "." { 101 | return Err("JSON path cannot be just a '.'".to_string()); 102 | } 103 | 104 | let json_path = &remaining[1..]; 105 | validate_json_path(json_path)?; 106 | } 107 | 108 | Ok(()) 109 | } 110 | 111 | /// Validate JSON path syntax 112 | fn validate_json_path(path: &str) -> Result<(), String> { 113 | if path.is_empty() { 114 | return Ok(()); 115 | } 116 | 117 | // Check if it starts or ends with invalid characters 118 | if path.starts_with('.') || path.starts_with('[') { 119 | return Err("JSON path cannot start with '.' or '['".to_string()); 120 | } 121 | 122 | if path.ends_with('.') { 123 | return Err("JSON path cannot end with '.'".to_string()); 124 | } 125 | 126 | let mut current_path = path; 127 | let mut expecting_key = true; // Mark whether currently expecting a key name 128 | 129 | while !current_path.is_empty() { 130 | if current_path.starts_with('[') { 131 | if expecting_key { 132 | return Err("Array index must be preceded by a property name".to_string()); 133 | } 134 | 135 | // Find closing bracket 136 | let close_bracket = current_path 137 | .find(']') 138 | .ok_or_else(|| "Missing closing ']' symbol".to_string())?; 139 | 140 | // Check index content 141 | let index_str = ¤t_path[1..close_bracket]; 142 | if index_str.is_empty() { 143 | return Err("Array index cannot be empty".to_string()); 144 | } 145 | 146 | // Validate if index is a valid number 147 | index_str 148 | .parse::() 149 | .map_err(|_| format!("Invalid array index: {}", index_str))?; 150 | 151 | // Update path 152 | current_path = ¤t_path[close_bracket + 1..]; 153 | expecting_key = false; // After array index can directly follow . or [ 154 | 155 | // If there's more content after, check if it starts with . or [ 156 | if !current_path.is_empty() { 157 | if current_path.starts_with('.') { 158 | current_path = ¤t_path[1..]; 159 | expecting_key = true; // After . must be a key name 160 | } else if !current_path.starts_with('[') { 161 | return Err("After array index can only follow '.' or another '['".to_string()); 162 | } 163 | } 164 | } else { 165 | // Handle property names 166 | if !expecting_key { 167 | return Err("Expected '[' here instead of property name".to_string()); 168 | } 169 | 170 | let next_separator = current_path.find(|c| c == '.' || c == '['); 171 | let (key, remaining_path) = if let Some(pos) = next_separator { 172 | (¤t_path[..pos], ¤t_path[pos..]) 173 | } else { 174 | (current_path, "") 175 | }; 176 | 177 | // Validate key name 178 | if key.is_empty() { 179 | return Err("Property name cannot be empty".to_string()); 180 | } 181 | 182 | // Check if key name contains invalid characters 183 | if !is_valid_json_key(key) { 184 | return Err(format!("Invalid property name: {}", key)); 185 | } 186 | 187 | // Update path 188 | current_path = if remaining_path.starts_with('.') { 189 | expecting_key = true; 190 | &remaining_path[1..] 191 | } else { 192 | expecting_key = false; 193 | remaining_path 194 | }; 195 | } 196 | } 197 | 198 | Ok(()) 199 | } 200 | 201 | /// Check if it's a valid JSON key name 202 | /// Simple validation rule: cannot contain special characters 203 | fn is_valid_json_key(key: &str) -> bool { 204 | !key.is_empty() && !key.contains(|c: char| c.is_control() || "[]{}().".contains(c)) 205 | } 206 | 207 | /// Navigate JSON value according to path 208 | /// Supports object property access (.key) and array index access ([index]) 209 | fn navigate_json_path(value: &Value, path: &str) -> Option { 210 | if path.is_empty() { 211 | return Some(value.clone()); 212 | } 213 | 214 | let mut current_value = value; 215 | let mut current_path = path; 216 | 217 | while !current_path.is_empty() { 218 | // Check if it's array index access 219 | if current_path.starts_with('[') { 220 | let close_bracket = current_path.find(']')?; 221 | let index_str = ¤t_path[1..close_bracket]; 222 | let index = index_str.parse::().ok()?; 223 | 224 | // Ensure current value is an array 225 | let array = current_value.as_array()?; 226 | if index >= array.len() { 227 | return None; 228 | } 229 | 230 | current_value = &array[index]; 231 | 232 | // Update path, skip processed parts 233 | current_path = ¤t_path[close_bracket + 1..]; 234 | 235 | // If there's more path after, it should start with . 236 | if !current_path.is_empty() && current_path.starts_with('.') { 237 | current_path = ¤t_path[1..]; 238 | } 239 | } else { 240 | // Object property access 241 | let next_separator = current_path.find(|c| c == '.' || c == '['); 242 | let (key, remaining_path) = if let Some(pos) = next_separator { 243 | (¤t_path[..pos], ¤t_path[pos..]) 244 | } else { 245 | (current_path, "") 246 | }; 247 | 248 | // Ensure current value is an object 249 | let obj = current_value.as_object()?; 250 | current_value = obj.get(key)?; 251 | 252 | // Update path 253 | current_path = if remaining_path.starts_with('.') { 254 | &remaining_path[1..] 255 | } else { 256 | remaining_path 257 | }; 258 | } 259 | } 260 | 261 | Some(current_value.clone()) 262 | } 263 | 264 | #[cfg(test)] 265 | mod tests { 266 | use super::*; 267 | use serde_json::json; 268 | use std::collections::HashMap; 269 | 270 | #[test] 271 | fn test_parse_expression() { 272 | let expr = "${8e1f2a34-5c67-4b89-a1d2-3f4e5a6b7c8d}.user.name"; 273 | let (node_id, path) = parse_expression(expr).unwrap(); 274 | 275 | assert_eq!(node_id.to_string(), "8e1f2a34-5c67-4b89-a1d2-3f4e5a6b7c8d"); 276 | assert_eq!(path, "user.name"); 277 | } 278 | 279 | #[test] 280 | fn test_navigate_json_path_object() { 281 | let json = json!({ 282 | "user": { 283 | "name": "John Doe", 284 | "email": "john@example.com" 285 | } 286 | }); 287 | 288 | let result = navigate_json_path(&json, "user.name").unwrap(); 289 | assert_eq!(result, json!("John Doe")); 290 | } 291 | 292 | #[test] 293 | fn test_navigate_json_path_array() { 294 | let json = json!({ 295 | "items": [ 296 | {"title": "First Item"}, 297 | {"title": "Second Item"} 298 | ] 299 | }); 300 | 301 | let result = navigate_json_path(&json, "items[0].title").unwrap(); 302 | assert_eq!(result, json!("First Item")); 303 | } 304 | 305 | #[test] 306 | fn test_extract_value_complete() { 307 | let node_id = Uuid::parse_str("8e1f2a34-5c67-4b89-a1d2-3f4e5a6b7c8d").unwrap(); 308 | let mut input = HashMap::new(); 309 | input.insert( 310 | node_id, 311 | json!({ 312 | "user": "John Doe", 313 | "email": "john.doe@example.com", 314 | "contacts": [ 315 | {"type": "phone", "value": "123-456-7890"}, 316 | {"type": "email", "value": "john@work.com"} 317 | ] 318 | }), 319 | ); 320 | 321 | // Test simple property access 322 | let expr = "=${8e1f2a34-5c67-4b89-a1d2-3f4e5a6b7c8d}.user"; 323 | let result = extract_value(expr, &input).unwrap(); 324 | assert_eq!(result, json!("John Doe")); 325 | 326 | // Test array access 327 | let expr = "=${8e1f2a34-5c67-4b89-a1d2-3f4e5a6b7c8d}.contacts[0].value"; 328 | let result = extract_value(expr, &input).unwrap(); 329 | assert_eq!(result, json!("123-456-7890")); 330 | 331 | // Test getting entire object 332 | let expr = "=${8e1f2a34-5c67-4b89-a1d2-3f4e5a6b7c8d}"; 333 | let result = extract_value(expr, &input).unwrap(); 334 | assert_eq!(result, input.get(&node_id).unwrap().clone()); 335 | } 336 | 337 | #[test] 338 | fn test_validate_expression_valid() { 339 | // Test valid expressions 340 | assert!(validate_expression("=${8e1f2a34-5c67-4b89-a1d2-3f4e5a6b7c8d}").is_ok()); 341 | assert!(validate_expression("=${8e1f2a34-5c67-4b89-a1d2-3f4e5a6b7c8d}.user").is_ok()); 342 | assert!(validate_expression("=${8e1f2a34-5c67-4b89-a1d2-3f4e5a6b7c8d}.user.name").is_ok()); 343 | assert!( 344 | validate_expression("=${8e1f2a34-5c67-4b89-a1d2-3f4e5a6b7c8d}.contacts[0]").is_ok() 345 | ); 346 | assert!( 347 | validate_expression("=${8e1f2a34-5c67-4b89-a1d2-3f4e5a6b7c8d}.contacts[0].value") 348 | .is_ok() 349 | ); 350 | assert!( 351 | validate_expression("=${8e1f2a34-5c67-4b89-a1d2-3f4e5a6b7c8d}.data[0][1].name").is_ok() 352 | ); 353 | } 354 | 355 | #[test] 356 | fn test_validate_expression_invalid_format() { 357 | // Test invalid formats 358 | assert!(validate_expression("${8e1f2a34-5c67-4b89-a1d2-3f4e5a6b7c8d}").is_err()); 359 | assert!(validate_expression("=8e1f2a34-5c67-4b89-a1d2-3f4e5a6b7c8d").is_err()); 360 | assert!(validate_expression("={8e1f2a34-5c67-4b89-a1d2-3f4e5a6b7c8d}").is_err()); 361 | assert!(validate_expression("=${8e1f2a34-5c67-4b89-a1d2-3f4e5a6b7c8d").is_err()); 362 | } 363 | 364 | #[test] 365 | fn test_validate_expression_invalid_node_id() { 366 | // Test invalid node IDs 367 | assert!(validate_expression("=${}").is_err()); 368 | assert!(validate_expression("=${invalid-uuid}").is_err()); 369 | assert!(validate_expression("=${123-456-789}").is_err()); 370 | } 371 | 372 | #[test] 373 | fn test_validate_expression_invalid_path() { 374 | let base = "=${8e1f2a34-5c67-4b89-a1d2-3f4e5a6b7c8d}"; 375 | 376 | // Test invalid paths 377 | assert!(validate_expression(&format!("{}..", base)).is_err()); 378 | assert!(validate_expression(&format!("{}.", base)).is_err()); 379 | assert!(validate_expression(&format!("{}.user.", base)).is_err()); 380 | assert!(validate_expression(&format!("{}.user[]", base)).is_err()); 381 | assert!(validate_expression(&format!("{}.user[abc]", base)).is_err()); 382 | assert!(validate_expression(&format!("{}.user[-1]", base)).is_err()); 383 | assert!(validate_expression(&format!("{}.user[0", base)).is_err()); 384 | assert!(validate_expression(&format!("{}.user.name[", base)).is_err()); 385 | } 386 | 387 | #[test] 388 | fn test_validate_json_path() { 389 | // Test valid paths 390 | assert!(validate_json_path("").is_ok()); 391 | assert!(validate_json_path("user").is_ok()); 392 | assert!(validate_json_path("user.name").is_ok()); 393 | assert!(validate_json_path("contacts[0]").is_ok()); 394 | assert!(validate_json_path("contacts[0].value").is_ok()); 395 | assert!(validate_json_path("data[0][1].name").is_ok()); 396 | 397 | // Test invalid paths 398 | assert!(validate_json_path(".").is_err()); 399 | assert!(validate_json_path("user.").is_err()); 400 | assert!(validate_json_path("[0]").is_err()); 401 | assert!(validate_json_path("user[]").is_err()); 402 | assert!(validate_json_path("user[abc]").is_err()); 403 | } 404 | 405 | #[test] 406 | fn test_is_valid_json_key() { 407 | // Test valid key names 408 | assert!(is_valid_json_key("user")); 409 | assert!(is_valid_json_key("userName")); 410 | assert!(is_valid_json_key("user_name")); 411 | assert!(is_valid_json_key("user123")); 412 | assert!(is_valid_json_key("name-with-dash")); 413 | 414 | // Test invalid key names 415 | assert!(!is_valid_json_key("")); 416 | assert!(!is_valid_json_key("user.name")); 417 | assert!(!is_valid_json_key("user[0]")); 418 | assert!(!is_valid_json_key("user{name}")); 419 | assert!(!is_valid_json_key("user()")); 420 | } 421 | } 422 | -------------------------------------------------------------------------------- /src/node/merge.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use serde_json::{json, Value}; 3 | 4 | use crate::execution::NodeOutput; 5 | use crate::node::{INode, NodeId, Error}; 6 | 7 | #[derive(Debug, Clone, Serialize, Deserialize)] 8 | pub struct MergeNode { 9 | pub id: NodeId, 10 | pub name: String, 11 | pub description: Option, 12 | pub parameters: Option, 13 | } 14 | 15 | #[derive(Debug, Clone, Serialize, Deserialize)] 16 | pub struct MergeParams { 17 | pub mode: String, // "append", "merge_by_key", "zip", "combine_all", "wait" 18 | pub merge_key: Option, 19 | pub wait_for_all: Option, 20 | pub output_format: Option, // "array", "object", "flat" 21 | } 22 | 23 | impl Default for MergeParams { 24 | fn default() -> Self { 25 | Self { 26 | mode: "append".to_string(), 27 | merge_key: None, 28 | wait_for_all: Some(true), 29 | output_format: Some("array".to_string()), 30 | } 31 | } 32 | } 33 | 34 | impl INode for MergeNode { 35 | fn new( 36 | id: NodeId, 37 | name: String, 38 | _node_type: String, 39 | description: Option, 40 | parameters: Option, 41 | ) -> Self { 42 | Self { 43 | id, 44 | name, 45 | description, 46 | parameters, 47 | } 48 | } 49 | 50 | fn id(&self) -> NodeId { 51 | self.id 52 | } 53 | 54 | fn name(&self) -> String { 55 | self.name.clone() 56 | } 57 | 58 | fn description(&self) -> Option { 59 | self.description.clone() 60 | } 61 | 62 | fn parameter(&self) -> Option { 63 | self.parameters.clone() 64 | } 65 | 66 | fn execute(&self, input: &NodeOutput) -> Result { 67 | let params = if let Some(params_value) = &self.parameters { 68 | serde_json::from_value::(params_value.clone()) 69 | .unwrap_or_default() 70 | } else { 71 | MergeParams::default() 72 | }; 73 | 74 | // Collect all input data from previous nodes 75 | let input_data: Vec = input.values().cloned().collect(); 76 | 77 | // If no input data, return empty based on output format 78 | if input_data.is_empty() { 79 | return Ok(self.create_empty_output(¶ms)); 80 | } 81 | 82 | match params.mode.as_str() { 83 | "append" => self.merge_append(¶ms, input_data), 84 | "merge_by_key" => self.merge_by_key(¶ms, input_data), 85 | "zip" => self.merge_zip(¶ms, input_data), 86 | "combine_all" => self.merge_combine_all(¶ms, input_data), 87 | "wait" => self.merge_wait(¶ms, input_data), 88 | _ => Err(Error::Validation(format!("Unsupported merge mode: {}", params.mode))) 89 | } 90 | } 91 | 92 | fn validate(&self) -> bool { 93 | if let Some(params_value) = &self.parameters { 94 | if let Ok(params) = serde_json::from_value::(params_value.clone()) { 95 | match params.mode.as_str() { 96 | "merge_by_key" => params.merge_key.is_some(), 97 | "append" | "zip" | "combine_all" | "wait" => true, 98 | _ => false 99 | } 100 | } else { 101 | false 102 | } 103 | } else { 104 | true // Default params are valid 105 | } 106 | } 107 | 108 | fn dependencies(&self) -> Vec { 109 | vec![] 110 | } 111 | } 112 | 113 | impl MergeNode { 114 | fn merge_append(&self, params: &MergeParams, input_data: Vec) -> Result { 115 | let mut result = Vec::new(); 116 | 117 | for input in input_data { 118 | match input { 119 | Value::Array(items) => { 120 | result.extend(items); 121 | } 122 | _ => { 123 | result.push(input); 124 | } 125 | } 126 | } 127 | 128 | self.format_output(params, Value::Array(result)) 129 | } 130 | 131 | fn merge_by_key(&self, params: &MergeParams, input_data: Vec) -> Result { 132 | let merge_key = params.merge_key.as_ref() 133 | .ok_or_else(|| Error::Validation("merge_key is required for merge_by_key mode".to_string()))?; 134 | 135 | let mut merged_data = std::collections::HashMap::new(); 136 | 137 | for input in input_data { 138 | let items = match input { 139 | Value::Array(items) => items, 140 | _ => vec![input], 141 | }; 142 | 143 | for item in items { 144 | if let Some(key_value) = item.get(merge_key) { 145 | let key = self.value_to_string(key_value); 146 | 147 | if let Some(existing) = merged_data.get(&key) { 148 | // Merge objects if both are objects 149 | if let (Value::Object(existing_obj), Value::Object(new_obj)) = (existing, &item) { 150 | let mut merged_obj = existing_obj.clone(); 151 | for (k, v) in new_obj { 152 | merged_obj.insert(k.clone(), v.clone()); 153 | } 154 | merged_data.insert(key, Value::Object(merged_obj)); 155 | } else { 156 | // Otherwise, replace with new value 157 | merged_data.insert(key, item); 158 | } 159 | } else { 160 | merged_data.insert(key, item); 161 | } 162 | } 163 | } 164 | } 165 | 166 | let result: Vec = merged_data.into_values().collect(); 167 | self.format_output(params, Value::Array(result)) 168 | } 169 | 170 | fn merge_zip(&self, params: &MergeParams, input_data: Vec) -> Result { 171 | // Convert all inputs to arrays 172 | let arrays: Vec> = input_data.into_iter().map(|input| { 173 | match input { 174 | Value::Array(items) => items, 175 | _ => vec![input], 176 | } 177 | }).collect(); 178 | 179 | if arrays.is_empty() { 180 | return self.format_output(params, Value::Array(vec![])); 181 | } 182 | 183 | // Find the maximum length 184 | let max_len = arrays.iter().map(|arr| arr.len()).max().unwrap_or(0); 185 | 186 | let mut result = Vec::new(); 187 | for i in 0..max_len { 188 | let mut zipped_item = serde_json::Map::new(); 189 | 190 | for (input_index, array) in arrays.iter().enumerate() { 191 | let value = array.get(i).cloned().unwrap_or(Value::Null); 192 | zipped_item.insert(format!("input_{}", input_index), value); 193 | } 194 | 195 | result.push(Value::Object(zipped_item)); 196 | } 197 | 198 | self.format_output(params, Value::Array(result)) 199 | } 200 | 201 | fn merge_combine_all(&self, params: &MergeParams, input_data: Vec) -> Result { 202 | let mut combined = serde_json::Map::new(); 203 | 204 | for (index, input) in input_data.into_iter().enumerate() { 205 | combined.insert(format!("input_{}", index), input); 206 | } 207 | 208 | self.format_output(params, Value::Object(combined)) 209 | } 210 | 211 | fn merge_wait(&self, params: &MergeParams, input_data: Vec) -> Result { 212 | // Wait mode simply passes through all inputs as received 213 | // In a real implementation, this would wait for all inputs before proceeding 214 | let wait_for_all = params.wait_for_all.unwrap_or(true); 215 | 216 | if wait_for_all && input_data.len() < 2 { 217 | // In a real implementation, this would wait for more inputs 218 | // For now, we'll just process what we have 219 | } 220 | 221 | let mut result = Vec::new(); 222 | 223 | for input in input_data { 224 | match input { 225 | Value::Array(items) => { 226 | result.extend(items); 227 | } 228 | _ => { 229 | result.push(input); 230 | } 231 | } 232 | } 233 | 234 | self.format_output(params, Value::Array(result)) 235 | } 236 | 237 | fn format_output(&self, params: &MergeParams, data: Value) -> Result { 238 | let format = params.output_format.as_ref().map(|s| s.as_str()).unwrap_or("array"); 239 | 240 | match format { 241 | "array" => { 242 | match data { 243 | Value::Array(_) => Ok(data), 244 | _ => Ok(json!([data])), 245 | } 246 | } 247 | "object" => { 248 | match data { 249 | Value::Object(_) => Ok(data), 250 | Value::Array(items) => { 251 | let mut obj = serde_json::Map::new(); 252 | for (i, item) in items.into_iter().enumerate() { 253 | obj.insert(i.to_string(), item); 254 | } 255 | Ok(Value::Object(obj)) 256 | } 257 | _ => { 258 | let mut obj = serde_json::Map::new(); 259 | obj.insert("data".to_string(), data); 260 | Ok(Value::Object(obj)) 261 | } 262 | } 263 | } 264 | "flat" => { 265 | match data { 266 | Value::Array(items) => { 267 | let mut flat = Vec::new(); 268 | for item in items { 269 | if let Value::Array(nested) = item { 270 | flat.extend(nested); 271 | } else { 272 | flat.push(item); 273 | } 274 | } 275 | Ok(Value::Array(flat)) 276 | } 277 | _ => Ok(data), 278 | } 279 | } 280 | _ => Err(Error::Validation(format!("Unsupported output format: {}", format))) 281 | } 282 | } 283 | 284 | fn create_empty_output(&self, params: &MergeParams) -> Value { 285 | let format = params.output_format.as_ref().map(|s| s.as_str()).unwrap_or("array"); 286 | 287 | match format { 288 | "array" => json!([]), 289 | "object" => json!({}), 290 | "flat" => json!([]), 291 | _ => json!([]), 292 | } 293 | } 294 | 295 | fn value_to_string(&self, value: &Value) -> String { 296 | match value { 297 | Value::String(s) => s.clone(), 298 | Value::Number(n) => n.to_string(), 299 | Value::Bool(b) => b.to_string(), 300 | Value::Null => "null".to_string(), 301 | _ => serde_json::to_string(value).unwrap_or_else(|_| "unknown".to_string()), 302 | } 303 | } 304 | } 305 | 306 | #[cfg(test)] 307 | mod tests { 308 | use super::*; 309 | use std::collections::HashMap; 310 | use uuid::Uuid; 311 | 312 | #[test] 313 | fn test_merge_node_creation() { 314 | let node = MergeNode::new( 315 | Uuid::new_v4(), 316 | "Test Merge".to_string(), 317 | "merge".to_string(), 318 | Some("Test description".to_string()), 319 | Some(json!({ 320 | "mode": "append", 321 | "output_format": "array" 322 | })), 323 | ); 324 | 325 | assert_eq!(node.name(), "Test Merge"); 326 | assert!(node.validate()); 327 | } 328 | 329 | #[test] 330 | fn test_merge_validation_merge_by_key() { 331 | let node = MergeNode::new( 332 | Uuid::new_v4(), 333 | "Test Merge".to_string(), 334 | "merge".to_string(), 335 | None, 336 | Some(json!({ 337 | "mode": "merge_by_key" 338 | // Missing merge_key 339 | })), 340 | ); 341 | 342 | assert!(!node.validate()); 343 | 344 | let valid_node = MergeNode::new( 345 | Uuid::new_v4(), 346 | "Test Merge".to_string(), 347 | "merge".to_string(), 348 | None, 349 | Some(json!({ 350 | "mode": "merge_by_key", 351 | "merge_key": "id" 352 | })), 353 | ); 354 | 355 | assert!(valid_node.validate()); 356 | } 357 | 358 | #[test] 359 | fn test_merge_append() { 360 | let node = MergeNode::new( 361 | Uuid::new_v4(), 362 | "Test Merge".to_string(), 363 | "merge".to_string(), 364 | None, 365 | Some(json!({ 366 | "mode": "append", 367 | "output_format": "array" 368 | })), 369 | ); 370 | 371 | let mut input = HashMap::new(); 372 | input.insert(Uuid::new_v4(), json!([{"a": 1}, {"a": 2}])); 373 | input.insert(Uuid::new_v4(), json!([{"b": 3}, {"b": 4}])); 374 | 375 | let result = node.execute(&input); 376 | assert!(result.is_ok()); 377 | 378 | if let Ok(Value::Array(items)) = result { 379 | assert_eq!(items.len(), 4); 380 | // Items from both inputs should be present 381 | assert!(items.iter().any(|item| item.get("a").is_some())); 382 | assert!(items.iter().any(|item| item.get("b").is_some())); 383 | } 384 | } 385 | 386 | #[test] 387 | fn test_merge_by_key() { 388 | let node = MergeNode::new( 389 | Uuid::new_v4(), 390 | "Test Merge".to_string(), 391 | "merge".to_string(), 392 | None, 393 | Some(json!({ 394 | "mode": "merge_by_key", 395 | "merge_key": "id", 396 | "output_format": "array" 397 | })), 398 | ); 399 | 400 | let mut input = HashMap::new(); 401 | input.insert(Uuid::new_v4(), json!([ 402 | {"id": "1", "name": "item1"}, 403 | {"id": "2", "name": "item2"} 404 | ])); 405 | input.insert(Uuid::new_v4(), json!([ 406 | {"id": "1", "value": 100}, 407 | {"id": "3", "name": "item3"} 408 | ])); 409 | 410 | let result = node.execute(&input); 411 | assert!(result.is_ok()); 412 | 413 | if let Ok(Value::Array(items)) = result { 414 | assert_eq!(items.len(), 3); // Three unique IDs: 1, 2, 3 415 | 416 | // Find item with id "1" - should have both name and value 417 | let item1 = items.iter().find(|item| item["id"] == "1").unwrap(); 418 | assert_eq!(item1["name"], "item1"); 419 | assert_eq!(item1["value"], 100); 420 | } 421 | } 422 | 423 | #[test] 424 | fn test_merge_zip() { 425 | let node = MergeNode::new( 426 | Uuid::new_v4(), 427 | "Test Merge".to_string(), 428 | "merge".to_string(), 429 | None, 430 | Some(json!({ 431 | "mode": "zip", 432 | "output_format": "array" 433 | })), 434 | ); 435 | 436 | let mut input = HashMap::new(); 437 | input.insert(Uuid::new_v4(), json!(["a", "b"])); 438 | input.insert(Uuid::new_v4(), json!([1, 2])); 439 | 440 | let result = node.execute(&input); 441 | assert!(result.is_ok()); 442 | 443 | if let Ok(Value::Array(items)) = result { 444 | assert_eq!(items.len(), 2); 445 | 446 | // First zipped item should combine first elements 447 | assert!(items[0]["input_0"] == "a" || items[0]["input_1"] == "a"); 448 | assert!(items[0]["input_0"] == 1 || items[0]["input_1"] == 1); 449 | 450 | // Second zipped item should combine second elements 451 | assert!(items[1]["input_0"] == "b" || items[1]["input_1"] == "b"); 452 | assert!(items[1]["input_0"] == 2 || items[1]["input_1"] == 2); 453 | } 454 | } 455 | } -------------------------------------------------------------------------------- /src/node/function.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use serde_json::{json, Value}; 3 | 4 | use crate::execution::NodeOutput; 5 | use crate::node::{INode, NodeId, Error}; 6 | 7 | #[derive(Debug, Clone, Serialize, Deserialize)] 8 | pub struct FunctionNode { 9 | pub id: NodeId, 10 | pub name: String, 11 | pub description: Option, 12 | pub parameters: Option, 13 | } 14 | 15 | #[derive(Debug, Clone, Serialize, Deserialize)] 16 | pub struct FunctionParams { 17 | pub function_code: String, 18 | pub language: String, // "javascript" or "python" 19 | #[serde(default = "default_true")] 20 | pub include_original_data: bool, 21 | #[serde(default)] 22 | pub allow_console_log: bool, 23 | } 24 | 25 | fn default_true() -> bool { true } 26 | 27 | impl Default for FunctionParams { 28 | fn default() -> Self { 29 | Self { 30 | function_code: "// Add your custom function code here\nreturn items;".to_string(), 31 | language: "javascript".to_string(), 32 | include_original_data: true, 33 | allow_console_log: false, 34 | } 35 | } 36 | } 37 | 38 | impl INode for FunctionNode { 39 | fn new( 40 | id: NodeId, 41 | name: String, 42 | _node_type: String, 43 | description: Option, 44 | parameters: Option, 45 | ) -> Self { 46 | Self { 47 | id, 48 | name, 49 | description, 50 | parameters, 51 | } 52 | } 53 | 54 | fn id(&self) -> NodeId { 55 | self.id 56 | } 57 | 58 | fn name(&self) -> String { 59 | self.name.clone() 60 | } 61 | 62 | fn description(&self) -> Option { 63 | self.description.clone() 64 | } 65 | 66 | fn parameter(&self) -> Option { 67 | self.parameters.clone() 68 | } 69 | 70 | fn execute(&self, input: &NodeOutput) -> Result { 71 | let params = if let Some(params_value) = &self.parameters { 72 | serde_json::from_value::(params_value.clone()) 73 | .unwrap_or_default() 74 | } else { 75 | FunctionParams::default() 76 | }; 77 | 78 | if params.function_code.trim().is_empty() { 79 | return Err(Error::Validation("Function code cannot be empty".to_string())); 80 | } 81 | 82 | // Combine all input data 83 | let input_data: Vec = input.values().cloned().collect(); 84 | 85 | // Convert input data to items format 86 | let mut items = Vec::new(); 87 | for (index, item) in input_data.iter().enumerate() { 88 | if let Value::Array(arr) = item { 89 | for (sub_index, sub_item) in arr.iter().enumerate() { 90 | items.push(json!({ 91 | "json": sub_item, 92 | "index": index * 1000 + sub_index // Simple indexing scheme 93 | })); 94 | } 95 | } else { 96 | items.push(json!({ 97 | "json": item, 98 | "index": index 99 | })); 100 | } 101 | } 102 | 103 | match params.language.as_str() { 104 | "javascript" => self.execute_javascript(¶ms, &items), 105 | "python" => Err(Error::Validation("Python execution not yet implemented".to_string())), 106 | _ => Err(Error::Validation(format!("Unsupported language: {}", params.language))) 107 | } 108 | } 109 | 110 | fn validate(&self) -> bool { 111 | if let Some(params_value) = &self.parameters { 112 | if let Ok(params) = serde_json::from_value::(params_value.clone()) { 113 | return !params.function_code.trim().is_empty() && 114 | matches!(params.language.as_str(), "javascript" | "python"); 115 | } 116 | } 117 | false 118 | } 119 | 120 | fn dependencies(&self) -> Vec { 121 | vec![] 122 | } 123 | } 124 | 125 | impl FunctionNode { 126 | fn execute_javascript(&self, params: &FunctionParams, items: &[Value]) -> Result { 127 | // This is a simplified JavaScript execution 128 | // In a real implementation, you would use a proper JavaScript engine 129 | 130 | let code = ¶ms.function_code; 131 | 132 | // Simple pattern matching for common operations 133 | if code.contains("return items") && !code.contains("for") && !code.contains("forEach") { 134 | // Simple passthrough 135 | return Ok(Value::Array(items.to_vec())); 136 | } 137 | 138 | if code.contains("item.json.myNewField = ") { 139 | // Add new field pattern 140 | return self.execute_add_field(code, items); 141 | } 142 | 143 | if code.contains("items.map(") { 144 | // Map operation pattern 145 | return self.execute_map_operation(code, items); 146 | } 147 | 148 | if code.contains("items.filter(") { 149 | // Filter operation pattern 150 | return self.execute_filter_operation(code, items); 151 | } 152 | 153 | if code.contains("for (item of items)") || code.contains("for(item of items)") { 154 | // For-of loop pattern 155 | return self.execute_for_loop(code, items); 156 | } 157 | 158 | // Default: try to execute simple transformations 159 | self.execute_simple_transformation(code, items) 160 | } 161 | 162 | fn execute_add_field(&self, code: &str, items: &[Value]) -> Result { 163 | let mut result_items = Vec::new(); 164 | 165 | // Extract the field name and value 166 | if let Some(assignment) = code.lines().find(|line| line.contains(".myNewField = ")) { 167 | let value_part = assignment.split(" = ").nth(1).unwrap_or("null").trim_end_matches(';').trim(); 168 | let new_value = self.parse_simple_value(value_part)?; 169 | 170 | for item in items { 171 | let mut new_item = item.clone(); 172 | if let Some(json_obj) = new_item.get_mut("json") { 173 | if let Value::Object(map) = json_obj { 174 | map.insert("myNewField".to_string(), new_value.clone()); 175 | } 176 | } 177 | result_items.push(new_item); 178 | } 179 | } else { 180 | result_items = items.to_vec(); 181 | } 182 | 183 | Ok(Value::Array(result_items)) 184 | } 185 | 186 | fn execute_for_loop(&self, code: &str, items: &[Value]) -> Result { 187 | let mut result_items = items.to_vec(); 188 | 189 | // Find assignments in the loop 190 | let lines: Vec<&str> = code.lines().collect(); 191 | let mut in_loop = false; 192 | 193 | for line in lines { 194 | let trimmed = line.trim(); 195 | 196 | if trimmed.contains("for (item of items)") || trimmed.contains("for(item of items)") { 197 | in_loop = true; 198 | continue; 199 | } 200 | 201 | if in_loop && trimmed == "}" { 202 | break; 203 | } 204 | 205 | if in_loop && trimmed.contains("item.json.") && trimmed.contains(" = ") { 206 | // Parse field assignment 207 | if let Some((field_part, value_part)) = self.parse_assignment(trimmed) { 208 | if let Some(field_name) = field_part.strip_prefix("item.json.") { 209 | let new_value = self.parse_simple_value(value_part)?; 210 | 211 | // Apply to all items 212 | for item in &mut result_items { 213 | if let Some(json_obj) = item.get_mut("json") { 214 | if let Value::Object(map) = json_obj { 215 | map.insert(field_name.to_string(), new_value.clone()); 216 | } 217 | } 218 | } 219 | } 220 | } 221 | } 222 | } 223 | 224 | Ok(Value::Array(result_items)) 225 | } 226 | 227 | fn execute_map_operation(&self, _code: &str, items: &[Value]) -> Result { 228 | // Simple map implementation - for now just return items 229 | Ok(Value::Array(items.to_vec())) 230 | } 231 | 232 | fn execute_filter_operation(&self, _code: &str, items: &[Value]) -> Result { 233 | // Simple filter implementation - for now just return items 234 | Ok(Value::Array(items.to_vec())) 235 | } 236 | 237 | fn execute_simple_transformation(&self, _code: &str, items: &[Value]) -> Result { 238 | // Default implementation - return items as-is with execution metadata 239 | let mut result_items = Vec::new(); 240 | 241 | for item in items { 242 | let mut new_item = item.clone(); 243 | if let Value::Object(ref mut map) = new_item { 244 | map.insert("_function_executed".to_string(), json!(true)); 245 | } 246 | result_items.push(new_item); 247 | } 248 | 249 | Ok(Value::Array(result_items)) 250 | } 251 | 252 | fn parse_assignment<'a>(&self, line: &'a str) -> Option<(&'a str, &'a str)> { 253 | if let Some(eq_pos) = line.find(" = ") { 254 | let field_part = line[..eq_pos].trim(); 255 | let value_part = line[eq_pos + 3..].trim_end_matches(';').trim(); 256 | return Some((field_part, value_part)); 257 | } 258 | None 259 | } 260 | 261 | fn parse_simple_value(&self, value_str: &str) -> Result { 262 | let trimmed = value_str.trim(); 263 | 264 | // String literals 265 | if (trimmed.starts_with('"') && trimmed.ends_with('"')) || 266 | (trimmed.starts_with('\'') && trimmed.ends_with('\'')) { 267 | return Ok(json!(trimmed[1..trimmed.len()-1])); 268 | } 269 | 270 | // Numbers 271 | if let Ok(int_val) = trimmed.parse::() { 272 | return Ok(json!(int_val)); 273 | } 274 | 275 | if let Ok(float_val) = trimmed.parse::() { 276 | return Ok(json!(float_val)); 277 | } 278 | 279 | // Booleans 280 | match trimmed { 281 | "true" => return Ok(json!(true)), 282 | "false" => return Ok(json!(false)), 283 | "null" => return Ok(Value::Null), 284 | _ => {} 285 | } 286 | 287 | // Default: treat as string 288 | Ok(json!(trimmed)) 289 | } 290 | } 291 | 292 | #[cfg(test)] 293 | mod tests { 294 | use super::*; 295 | use std::collections::HashMap; 296 | 297 | #[test] 298 | fn test_function_node_creation() { 299 | let node = FunctionNode::new( 300 | uuid::Uuid::new_v4(), 301 | "Test Function".to_string(), 302 | "function".to_string(), 303 | Some("Test description".to_string()), 304 | Some(json!({ 305 | "function_code": "return items;", 306 | "language": "javascript" 307 | })), 308 | ); 309 | 310 | assert_eq!(node.name(), "Test Function"); 311 | assert!(node.validate()); 312 | } 313 | 314 | #[test] 315 | fn test_function_validation() { 316 | let node = FunctionNode::new( 317 | uuid::Uuid::new_v4(), 318 | "Test Function".to_string(), 319 | "function".to_string(), 320 | None, 321 | Some(json!({ 322 | "function_code": "", 323 | "language": "javascript" 324 | })), 325 | ); 326 | 327 | assert!(!node.validate()); 328 | } 329 | 330 | #[test] 331 | fn test_simple_passthrough() { 332 | let node = FunctionNode::new( 333 | uuid::Uuid::new_v4(), 334 | "Test Function".to_string(), 335 | "function".to_string(), 336 | None, 337 | Some(json!({ 338 | "function_code": "return items;", 339 | "language": "javascript" 340 | })), 341 | ); 342 | 343 | let mut input = HashMap::new(); 344 | input.insert( 345 | uuid::Uuid::new_v4(), 346 | json!([ 347 | {"name": "John", "age": 30}, 348 | {"name": "Jane", "age": 25} 349 | ]) 350 | ); 351 | 352 | let result = node.execute(&input); 353 | assert!(result.is_ok()); 354 | 355 | if let Ok(Value::Array(results)) = result { 356 | assert_eq!(results.len(), 2); 357 | assert_eq!(results[0]["json"]["name"], "John"); 358 | assert_eq!(results[1]["json"]["name"], "Jane"); 359 | } 360 | } 361 | 362 | #[test] 363 | fn test_add_field_execution() { 364 | let node = FunctionNode::new( 365 | uuid::Uuid::new_v4(), 366 | "Test Function".to_string(), 367 | "function".to_string(), 368 | None, 369 | Some(json!({ 370 | "function_code": "for (item of items) {\n item.json.myNewField = 42;\n}\nreturn items;", 371 | "language": "javascript" 372 | })), 373 | ); 374 | 375 | let mut input = HashMap::new(); 376 | input.insert( 377 | uuid::Uuid::new_v4(), 378 | json!({"name": "test"}) 379 | ); 380 | 381 | let result = node.execute(&input); 382 | assert!(result.is_ok()); 383 | 384 | if let Ok(Value::Array(results)) = result { 385 | assert_eq!(results.len(), 1); 386 | assert_eq!(results[0]["json"]["name"], "test"); 387 | assert_eq!(results[0]["json"]["myNewField"], 42); 388 | } 389 | } 390 | 391 | #[test] 392 | fn test_unsupported_language() { 393 | let node = FunctionNode::new( 394 | uuid::Uuid::new_v4(), 395 | "Test Function".to_string(), 396 | "function".to_string(), 397 | None, 398 | Some(json!({ 399 | "function_code": "print('hello')", 400 | "language": "python" 401 | })), 402 | ); 403 | 404 | let input = HashMap::new(); 405 | let result = node.execute(&input); 406 | assert!(result.is_err()); 407 | } 408 | 409 | #[test] 410 | fn test_empty_function_code() { 411 | let node = FunctionNode::new( 412 | uuid::Uuid::new_v4(), 413 | "Test Function".to_string(), 414 | "function".to_string(), 415 | None, 416 | Some(json!({ 417 | "function_code": "", 418 | "language": "javascript" 419 | })), 420 | ); 421 | 422 | let input = HashMap::new(); 423 | let result = node.execute(&input); 424 | assert!(result.is_err()); 425 | } 426 | 427 | #[test] 428 | fn test_parse_simple_values() { 429 | let node = FunctionNode::new( 430 | uuid::Uuid::new_v4(), 431 | "Test Function".to_string(), 432 | "function".to_string(), 433 | None, 434 | None, 435 | ); 436 | 437 | assert_eq!(node.parse_simple_value("42").unwrap(), json!(42)); 438 | assert_eq!(node.parse_simple_value("3.14").unwrap(), json!(3.14)); 439 | assert_eq!(node.parse_simple_value("\"hello\"").unwrap(), json!("hello")); 440 | assert_eq!(node.parse_simple_value("'world'").unwrap(), json!("world")); 441 | assert_eq!(node.parse_simple_value("true").unwrap(), json!(true)); 442 | assert_eq!(node.parse_simple_value("false").unwrap(), json!(false)); 443 | assert_eq!(node.parse_simple_value("null").unwrap(), Value::Null); 444 | } 445 | 446 | #[test] 447 | fn test_parse_assignment() { 448 | let node = FunctionNode::new( 449 | uuid::Uuid::new_v4(), 450 | "Test Function".to_string(), 451 | "function".to_string(), 452 | None, 453 | None, 454 | ); 455 | 456 | if let Some((field, value)) = node.parse_assignment("item.json.myField = 123;") { 457 | assert_eq!(field, "item.json.myField"); 458 | assert_eq!(value, "123"); 459 | } else { 460 | panic!("Failed to parse assignment"); 461 | } 462 | } 463 | } -------------------------------------------------------------------------------- /src/node/remove_duplicates.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use serde_json::{json, Value}; 3 | use std::collections::HashSet; 4 | 5 | use crate::execution::NodeOutput; 6 | use crate::node::{INode, NodeId, Error}; 7 | 8 | #[derive(Debug, Clone, Serialize, Deserialize)] 9 | pub struct RemoveDuplicatesNode { 10 | pub id: NodeId, 11 | pub name: String, 12 | pub description: Option, 13 | pub parameters: Option, 14 | } 15 | 16 | #[derive(Debug, Clone, Serialize, Deserialize)] 17 | pub struct CompareField { 18 | pub field_name: String, 19 | } 20 | 21 | #[derive(Debug, Clone, Serialize, Deserialize)] 22 | pub struct RemoveDuplicatesParams { 23 | #[serde(default)] 24 | pub compare: String, // "allFields", "selectedFields", "allFieldsExcept" 25 | #[serde(default)] 26 | pub fields_to_compare: Vec, 27 | #[serde(default)] 28 | pub fields_to_exclude: Vec, 29 | #[serde(default = "default_false")] 30 | pub disable_dot_notation: bool, 31 | } 32 | 33 | fn default_false() -> bool { false } 34 | 35 | impl Default for RemoveDuplicatesParams { 36 | fn default() -> Self { 37 | Self { 38 | compare: "allFields".to_string(), 39 | fields_to_compare: vec![], 40 | fields_to_exclude: vec![], 41 | disable_dot_notation: false, 42 | } 43 | } 44 | } 45 | 46 | impl INode for RemoveDuplicatesNode { 47 | fn new( 48 | id: NodeId, 49 | name: String, 50 | _node_type: String, 51 | description: Option, 52 | parameters: Option, 53 | ) -> Self { 54 | Self { 55 | id, 56 | name, 57 | description, 58 | parameters, 59 | } 60 | } 61 | 62 | fn id(&self) -> NodeId { 63 | self.id 64 | } 65 | 66 | fn name(&self) -> String { 67 | self.name.clone() 68 | } 69 | 70 | fn description(&self) -> Option { 71 | self.description.clone() 72 | } 73 | 74 | fn parameter(&self) -> Option { 75 | self.parameters.clone() 76 | } 77 | 78 | fn execute(&self, input: &NodeOutput) -> Result { 79 | let params = if let Some(params_value) = &self.parameters { 80 | serde_json::from_value::(params_value.clone()) 81 | .unwrap_or_default() 82 | } else { 83 | RemoveDuplicatesParams::default() 84 | }; 85 | 86 | // Process input data 87 | let input_data: Vec = input.values().cloned().collect(); 88 | 89 | if input_data.is_empty() { 90 | return Ok(json!([])); 91 | } 92 | 93 | // Flatten all items into a single array 94 | let mut all_items = Vec::new(); 95 | for item in input_data { 96 | if let Value::Array(items) = item { 97 | all_items.extend(items); 98 | } else { 99 | all_items.push(item); 100 | } 101 | } 102 | 103 | // Remove duplicates 104 | let unique_items = self.remove_duplicates(¶ms, all_items)?; 105 | 106 | Ok(Value::Array(unique_items)) 107 | } 108 | 109 | fn validate(&self) -> bool { 110 | if let Some(params_value) = &self.parameters { 111 | if let Ok(params) = serde_json::from_value::(params_value.clone()) { 112 | match params.compare.as_str() { 113 | "allFields" | "allFieldsExcept" => true, 114 | "selectedFields" => !params.fields_to_compare.is_empty(), 115 | _ => false 116 | } 117 | } else { 118 | false 119 | } 120 | } else { 121 | true // Default is valid 122 | } 123 | } 124 | 125 | fn dependencies(&self) -> Vec { 126 | vec![] 127 | } 128 | } 129 | 130 | impl RemoveDuplicatesNode { 131 | fn remove_duplicates(&self, params: &RemoveDuplicatesParams, items: Vec) -> Result, Error> { 132 | let mut seen_keys = HashSet::new(); 133 | let mut unique_items = Vec::new(); 134 | 135 | for item in items { 136 | let comparison_key = self.generate_comparison_key(params, &item)?; 137 | 138 | if !seen_keys.contains(&comparison_key) { 139 | seen_keys.insert(comparison_key); 140 | unique_items.push(item); 141 | } 142 | } 143 | 144 | Ok(unique_items) 145 | } 146 | 147 | fn generate_comparison_key(&self, params: &RemoveDuplicatesParams, item: &Value) -> Result { 148 | match params.compare.as_str() { 149 | "allFields" => { 150 | // Use the entire item as comparison key 151 | Ok(item.to_string()) 152 | }, 153 | "selectedFields" => { 154 | // Only compare specified fields - create a sorted vector to ensure deterministic comparison 155 | let mut comparison_fields = Vec::new(); 156 | for field in ¶ms.fields_to_compare { 157 | let field_value = self.get_field_value(&field.field_name, item, params.disable_dot_notation); 158 | comparison_fields.push((field.field_name.clone(), field_value)); 159 | } 160 | // Sort by field name to ensure deterministic ordering 161 | comparison_fields.sort_by_key(|(name, _)| name.clone()); 162 | Ok(serde_json::to_string(&comparison_fields) 163 | .map_err(|e| Error::Validation(format!("Failed to serialize comparison data: {}", e)))?) 164 | }, 165 | "allFieldsExcept" => { 166 | // Compare all fields except specified ones 167 | let mut item_copy = item.clone(); 168 | for field in ¶ms.fields_to_exclude { 169 | self.remove_field_from_value(&mut item_copy, &field.field_name, params.disable_dot_notation)?; 170 | } 171 | Ok(item_copy.to_string()) 172 | }, 173 | _ => { 174 | Err(Error::Validation(format!("Unsupported compare mode: {}", params.compare))) 175 | } 176 | } 177 | } 178 | 179 | fn get_field_value(&self, field_path: &str, data: &Value, disable_dot_notation: bool) -> Value { 180 | if disable_dot_notation { 181 | // Direct field access only 182 | data.get(field_path).cloned().unwrap_or(Value::Null) 183 | } else { 184 | // Support dot notation like "user.name" 185 | let parts: Vec<&str> = field_path.split('.').collect(); 186 | let mut current = data; 187 | 188 | for part in parts { 189 | match current.get(part) { 190 | Some(next) => current = next, 191 | None => return Value::Null, 192 | } 193 | } 194 | 195 | current.clone() 196 | } 197 | } 198 | 199 | fn remove_field_from_value(&self, value: &mut Value, field_path: &str, disable_dot_notation: bool) -> Result<(), Error> { 200 | if disable_dot_notation { 201 | // Direct field removal only 202 | if let Value::Object(obj) = value { 203 | obj.remove(field_path); 204 | } 205 | return Ok(()); 206 | } 207 | 208 | // Support dot notation removal 209 | let parts: Vec<&str> = field_path.split('.').collect(); 210 | if parts.is_empty() { 211 | return Ok(()); 212 | } 213 | 214 | let mut current = value; 215 | 216 | // Navigate to the parent of the target field 217 | for part in &parts[..parts.len()-1] { 218 | match current { 219 | Value::Object(obj) => { 220 | current = obj.get_mut(*part).ok_or_else(|| 221 | Error::Validation(format!("Field path not found: {}", field_path)))?; 222 | }, 223 | _ => { 224 | return Err(Error::Validation("Cannot navigate to non-object field".to_string())); 225 | } 226 | } 227 | } 228 | 229 | // Remove the final field 230 | let final_field = parts[parts.len()-1]; 231 | if let Value::Object(obj) = current { 232 | obj.remove(final_field); 233 | } 234 | 235 | Ok(()) 236 | } 237 | } 238 | 239 | #[cfg(test)] 240 | mod tests { 241 | use super::*; 242 | use std::collections::HashMap; 243 | 244 | #[test] 245 | fn test_remove_duplicates_node_creation() { 246 | let node = RemoveDuplicatesNode::new( 247 | uuid::Uuid::new_v4(), 248 | "Test Remove Duplicates".to_string(), 249 | "removeDuplicates".to_string(), 250 | Some("Test description".to_string()), 251 | Some(json!({ 252 | "compare": "allFields" 253 | })), 254 | ); 255 | 256 | assert_eq!(node.name(), "Test Remove Duplicates"); 257 | assert!(node.validate()); 258 | } 259 | 260 | #[test] 261 | fn test_remove_duplicates_validation() { 262 | let node = RemoveDuplicatesNode::new( 263 | uuid::Uuid::new_v4(), 264 | "Test Remove Duplicates".to_string(), 265 | "removeDuplicates".to_string(), 266 | None, 267 | Some(json!({ 268 | "compare": "selectedFields", 269 | "fields_to_compare": [] 270 | })), 271 | ); 272 | 273 | assert!(!node.validate()); 274 | } 275 | 276 | #[test] 277 | fn test_remove_duplicates_all_fields() { 278 | let node = RemoveDuplicatesNode::new( 279 | uuid::Uuid::new_v4(), 280 | "Test Remove Duplicates".to_string(), 281 | "removeDuplicates".to_string(), 282 | None, 283 | Some(json!({ 284 | "compare": "allFields" 285 | })), 286 | ); 287 | 288 | let mut input = HashMap::new(); 289 | input.insert( 290 | uuid::Uuid::new_v4(), 291 | json!([ 292 | {"name": "John", "age": 30}, 293 | {"name": "Jane", "age": 25}, 294 | {"name": "John", "age": 30}, // Duplicate 295 | {"name": "Bob", "age": 35} 296 | ]) 297 | ); 298 | 299 | let result = node.execute(&input); 300 | assert!(result.is_ok()); 301 | 302 | if let Ok(Value::Array(results)) = result { 303 | assert_eq!(results.len(), 3); // Should remove one duplicate 304 | 305 | // Verify that we have unique items 306 | let mut names = HashSet::new(); 307 | for item in &results { 308 | let key = format!("{}:{}", item["name"], item["age"]); 309 | names.insert(key); 310 | } 311 | assert_eq!(names.len(), 3); 312 | } 313 | } 314 | 315 | #[test] 316 | fn test_remove_duplicates_selected_fields() { 317 | let node = RemoveDuplicatesNode::new( 318 | uuid::Uuid::new_v4(), 319 | "Test Remove Duplicates".to_string(), 320 | "removeDuplicates".to_string(), 321 | None, 322 | Some(json!({ 323 | "compare": "selectedFields", 324 | "fields_to_compare": [ 325 | {"field_name": "email"} 326 | ] 327 | })), 328 | ); 329 | 330 | let mut input = HashMap::new(); 331 | input.insert( 332 | uuid::Uuid::new_v4(), 333 | json!([ 334 | {"name": "John", "email": "john@example.com", "age": 30}, 335 | {"name": "Johnny", "email": "john@example.com", "age": 31}, // Same email, different other fields 336 | {"name": "Jane", "email": "jane@example.com", "age": 25} 337 | ]) 338 | ); 339 | 340 | let result = node.execute(&input); 341 | assert!(result.is_ok()); 342 | 343 | if let Ok(Value::Array(results)) = result { 344 | assert_eq!(results.len(), 2); // Should remove duplicate based on email only 345 | 346 | // Verify unique emails 347 | let mut emails = HashSet::new(); 348 | for item in &results { 349 | emails.insert(item["email"].as_str().unwrap()); 350 | } 351 | assert_eq!(emails.len(), 2); 352 | assert!(emails.contains("john@example.com")); 353 | assert!(emails.contains("jane@example.com")); 354 | } 355 | } 356 | 357 | #[test] 358 | fn test_remove_duplicates_except_fields() { 359 | let node = RemoveDuplicatesNode::new( 360 | uuid::Uuid::new_v4(), 361 | "Test Remove Duplicates".to_string(), 362 | "removeDuplicates".to_string(), 363 | None, 364 | Some(json!({ 365 | "compare": "allFieldsExcept", 366 | "fields_to_exclude": [ 367 | {"field_name": "timestamp"} 368 | ] 369 | })), 370 | ); 371 | 372 | let mut input = HashMap::new(); 373 | input.insert( 374 | uuid::Uuid::new_v4(), 375 | json!([ 376 | {"name": "John", "email": "john@example.com", "timestamp": "2023-01-01"}, 377 | {"name": "John", "email": "john@example.com", "timestamp": "2023-01-02"}, // Same except timestamp 378 | {"name": "Jane", "email": "jane@example.com", "timestamp": "2023-01-01"} 379 | ]) 380 | ); 381 | 382 | let result = node.execute(&input); 383 | assert!(result.is_ok()); 384 | 385 | if let Ok(Value::Array(results)) = result { 386 | assert_eq!(results.len(), 2); // Should remove duplicate when ignoring timestamp 387 | } 388 | } 389 | 390 | #[test] 391 | fn test_remove_duplicates_nested_fields() { 392 | let node = RemoveDuplicatesNode::new( 393 | uuid::Uuid::new_v4(), 394 | "Test Remove Duplicates".to_string(), 395 | "removeDuplicates".to_string(), 396 | None, 397 | Some(json!({ 398 | "compare": "selectedFields", 399 | "fields_to_compare": [ 400 | {"field_name": "user.id"} 401 | ] 402 | })), 403 | ); 404 | 405 | let mut input = HashMap::new(); 406 | input.insert( 407 | uuid::Uuid::new_v4(), 408 | json!([ 409 | {"user": {"id": 1, "name": "John"}, "status": "active"}, 410 | {"user": {"id": 1, "name": "Johnny"}, "status": "inactive"}, // Same user.id 411 | {"user": {"id": 2, "name": "Jane"}, "status": "active"} 412 | ]) 413 | ); 414 | 415 | let result = node.execute(&input); 416 | assert!(result.is_ok()); 417 | 418 | if let Ok(Value::Array(results)) = result { 419 | assert_eq!(results.len(), 2); // Should remove duplicate based on user.id 420 | } 421 | } 422 | 423 | #[test] 424 | fn test_remove_duplicates_no_duplicates() { 425 | let node = RemoveDuplicatesNode::new( 426 | uuid::Uuid::new_v4(), 427 | "Test Remove Duplicates".to_string(), 428 | "removeDuplicates".to_string(), 429 | None, 430 | Some(json!({ 431 | "compare": "allFields" 432 | })), 433 | ); 434 | 435 | let mut input = HashMap::new(); 436 | input.insert( 437 | uuid::Uuid::new_v4(), 438 | json!([ 439 | {"name": "John", "age": 30}, 440 | {"name": "Jane", "age": 25}, 441 | {"name": "Bob", "age": 35} 442 | ]) 443 | ); 444 | 445 | let result = node.execute(&input); 446 | assert!(result.is_ok()); 447 | 448 | if let Ok(Value::Array(results)) = result { 449 | assert_eq!(results.len(), 3); // Should keep all items as no duplicates 450 | } 451 | } 452 | 453 | #[test] 454 | fn test_remove_duplicates_empty_input() { 455 | let node = RemoveDuplicatesNode::new( 456 | uuid::Uuid::new_v4(), 457 | "Test Remove Duplicates".to_string(), 458 | "removeDuplicates".to_string(), 459 | None, 460 | Some(json!({ 461 | "compare": "allFields" 462 | })), 463 | ); 464 | 465 | let input = HashMap::new(); 466 | let result = node.execute(&input); 467 | assert!(result.is_ok()); 468 | 469 | if let Ok(Value::Array(results)) = result { 470 | assert_eq!(results.len(), 0); 471 | } 472 | } 473 | 474 | #[test] 475 | fn test_remove_duplicates_multiple_fields() { 476 | let node = RemoveDuplicatesNode::new( 477 | uuid::Uuid::new_v4(), 478 | "Test Remove Duplicates".to_string(), 479 | "removeDuplicates".to_string(), 480 | None, 481 | Some(json!({ 482 | "compare": "selectedFields", 483 | "fields_to_compare": [ 484 | {"field_name": "name"}, 485 | {"field_name": "department"} 486 | ] 487 | })), 488 | ); 489 | 490 | let mut input = HashMap::new(); 491 | input.insert( 492 | uuid::Uuid::new_v4(), 493 | json!([ 494 | {"name": "John", "department": "Engineering", "salary": 80000}, 495 | {"name": "John", "department": "Engineering", "salary": 85000}, // Same name+dept, different salary 496 | {"name": "John", "department": "Marketing", "salary": 75000}, // Same name, different dept 497 | {"name": "Jane", "department": "Engineering", "salary": 82000} 498 | ]) 499 | ); 500 | 501 | let result = node.execute(&input); 502 | assert!(result.is_ok()); 503 | 504 | if let Ok(Value::Array(results)) = result { 505 | assert_eq!(results.len(), 3); // Should keep 3 unique name+department combinations 506 | } 507 | } 508 | } -------------------------------------------------------------------------------- /src/node/transform.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use serde_json::{json, Value}; 3 | use std::collections::HashMap; 4 | 5 | use crate::execution::NodeOutput; 6 | use crate::node::{INode, NodeId, Error}; 7 | 8 | #[derive(Debug, Clone, Serialize, Deserialize)] 9 | pub struct TransformNode { 10 | pub id: NodeId, 11 | pub name: String, 12 | pub description: Option, 13 | pub parameters: Option, 14 | } 15 | 16 | #[derive(Debug, Clone, Serialize, Deserialize)] 17 | pub struct TransformParams { 18 | pub transformations: Vec, 19 | pub mode: String, // "each" or "batch" 20 | } 21 | 22 | #[derive(Debug, Clone, Serialize, Deserialize)] 23 | pub struct Transformation { 24 | pub operation: String, // "map", "filter", "sort", "group", "rename", "add_field", "remove_field" 25 | pub field: Option, 26 | pub new_field: Option, 27 | pub value: Option, 28 | pub condition: Option, 29 | pub sort_key: Option, 30 | pub sort_order: Option, // "asc" or "desc" 31 | } 32 | 33 | #[derive(Debug, Clone, Serialize, Deserialize)] 34 | pub struct TransformCondition { 35 | pub field: String, 36 | pub operator: String, 37 | pub value: Value, 38 | } 39 | 40 | impl Default for TransformParams { 41 | fn default() -> Self { 42 | Self { 43 | transformations: vec![], 44 | mode: "each".to_string(), 45 | } 46 | } 47 | } 48 | 49 | impl INode for TransformNode { 50 | fn new( 51 | id: NodeId, 52 | name: String, 53 | _node_type: String, 54 | description: Option, 55 | parameters: Option, 56 | ) -> Self { 57 | Self { 58 | id, 59 | name, 60 | description, 61 | parameters, 62 | } 63 | } 64 | 65 | fn id(&self) -> NodeId { 66 | self.id 67 | } 68 | 69 | fn name(&self) -> String { 70 | self.name.clone() 71 | } 72 | 73 | fn description(&self) -> Option { 74 | self.description.clone() 75 | } 76 | 77 | fn parameter(&self) -> Option { 78 | self.parameters.clone() 79 | } 80 | 81 | fn execute(&self, input: &NodeOutput) -> Result { 82 | let params = if let Some(params_value) = &self.parameters { 83 | serde_json::from_value::(params_value.clone()) 84 | .unwrap_or_default() 85 | } else { 86 | TransformParams::default() 87 | }; 88 | 89 | // Combine all input data from previous nodes 90 | let input_data: Vec = input.values().cloned().collect(); 91 | 92 | // If no input data, return empty array 93 | if input_data.is_empty() { 94 | return Ok(json!([])); 95 | } 96 | 97 | // Flatten all input arrays into a single collection 98 | let mut all_items = Vec::new(); 99 | for item in input_data { 100 | if let Value::Array(items) = item { 101 | all_items.extend(items); 102 | } else { 103 | all_items.push(item); 104 | } 105 | } 106 | 107 | match params.mode.as_str() { 108 | "each" => self.transform_each(¶ms.transformations, all_items), 109 | "batch" => self.transform_batch(¶ms.transformations, all_items), 110 | _ => Err(Error::Validation(format!("Unsupported mode: {}", params.mode))) 111 | } 112 | } 113 | 114 | fn validate(&self) -> bool { 115 | if let Some(params_value) = &self.parameters { 116 | if let Ok(params) = serde_json::from_value::(params_value.clone()) { 117 | return !params.transformations.is_empty(); 118 | } 119 | } 120 | false 121 | } 122 | 123 | fn dependencies(&self) -> Vec { 124 | vec![] 125 | } 126 | } 127 | 128 | impl TransformNode { 129 | fn transform_each(&self, transformations: &[Transformation], items: Vec) -> Result { 130 | let mut result = Vec::new(); 131 | 132 | for item in items { 133 | let mut transformed_item = item; 134 | 135 | // Apply each transformation to the item 136 | for transformation in transformations { 137 | transformed_item = self.apply_transformation(transformation, transformed_item)?; 138 | } 139 | 140 | result.push(transformed_item); 141 | } 142 | 143 | Ok(Value::Array(result)) 144 | } 145 | 146 | fn transform_batch(&self, transformations: &[Transformation], items: Vec) -> Result { 147 | let mut result = items; 148 | 149 | // Apply each transformation to the entire batch 150 | for transformation in transformations { 151 | result = self.apply_batch_transformation(transformation, result)?; 152 | } 153 | 154 | Ok(Value::Array(result)) 155 | } 156 | 157 | fn apply_transformation(&self, transformation: &Transformation, item: Value) -> Result { 158 | match transformation.operation.as_str() { 159 | "add_field" => self.add_field(transformation, item), 160 | "remove_field" => self.remove_field(transformation, item), 161 | "rename_field" => self.rename_field(transformation, item), 162 | "map_value" => self.map_value(transformation, item), 163 | "filter" => { 164 | if self.passes_condition(transformation, &item)? { 165 | Ok(item) 166 | } else { 167 | Ok(Value::Null) // Will be filtered out later 168 | } 169 | } 170 | _ => Err(Error::Validation(format!("Unsupported transformation: {}", transformation.operation))) 171 | } 172 | } 173 | 174 | fn apply_batch_transformation(&self, transformation: &Transformation, items: Vec) -> Result, Error> { 175 | match transformation.operation.as_str() { 176 | "sort" => self.sort_items(transformation, items), 177 | "group" => self.group_items(transformation, items), 178 | "filter" => self.filter_items(transformation, items), 179 | _ => { 180 | // Apply single-item transformations to each item 181 | let mut result = Vec::new(); 182 | for item in items { 183 | let transformed = self.apply_transformation(transformation, item)?; 184 | if !transformed.is_null() { 185 | result.push(transformed); 186 | } 187 | } 188 | Ok(result) 189 | } 190 | } 191 | } 192 | 193 | fn add_field(&self, transformation: &Transformation, mut item: Value) -> Result { 194 | let new_field = transformation.new_field.as_ref() 195 | .ok_or_else(|| Error::Validation("new_field is required for add_field operation".to_string()))?; 196 | let value = transformation.value.as_ref() 197 | .ok_or_else(|| Error::Validation("value is required for add_field operation".to_string()))?; 198 | 199 | if let Value::Object(ref mut obj) = item { 200 | obj.insert(new_field.clone(), value.clone()); 201 | } 202 | 203 | Ok(item) 204 | } 205 | 206 | fn remove_field(&self, transformation: &Transformation, mut item: Value) -> Result { 207 | let field = transformation.field.as_ref() 208 | .ok_or_else(|| Error::Validation("field is required for remove_field operation".to_string()))?; 209 | 210 | if let Value::Object(ref mut obj) = item { 211 | obj.remove(field); 212 | } 213 | 214 | Ok(item) 215 | } 216 | 217 | fn rename_field(&self, transformation: &Transformation, mut item: Value) -> Result { 218 | let old_field = transformation.field.as_ref() 219 | .ok_or_else(|| Error::Validation("field is required for rename_field operation".to_string()))?; 220 | let new_field = transformation.new_field.as_ref() 221 | .ok_or_else(|| Error::Validation("new_field is required for rename_field operation".to_string()))?; 222 | 223 | if let Value::Object(ref mut obj) = item { 224 | if let Some(value) = obj.remove(old_field) { 225 | obj.insert(new_field.clone(), value); 226 | } 227 | } 228 | 229 | Ok(item) 230 | } 231 | 232 | fn map_value(&self, transformation: &Transformation, mut item: Value) -> Result { 233 | let field = transformation.field.as_ref() 234 | .ok_or_else(|| Error::Validation("field is required for map_value operation".to_string()))?; 235 | let new_value = transformation.value.as_ref() 236 | .ok_or_else(|| Error::Validation("value is required for map_value operation".to_string()))?; 237 | 238 | if let Value::Object(ref mut obj) = item { 239 | obj.insert(field.clone(), new_value.clone()); 240 | } 241 | 242 | Ok(item) 243 | } 244 | 245 | fn sort_items(&self, transformation: &Transformation, mut items: Vec) -> Result, Error> { 246 | let sort_key = transformation.sort_key.as_ref() 247 | .ok_or_else(|| Error::Validation("sort_key is required for sort operation".to_string()))?; 248 | let sort_order = transformation.sort_order.as_ref().map(|s| s.as_str()).unwrap_or("asc"); 249 | 250 | items.sort_by(|a, b| { 251 | let a_val = self.get_field_value(sort_key, a).unwrap_or(Value::Null); 252 | let b_val = self.get_field_value(sort_key, b).unwrap_or(Value::Null); 253 | 254 | let cmp = match (&a_val, &b_val) { 255 | (Value::Number(a_num), Value::Number(b_num)) => { 256 | a_num.as_f64().partial_cmp(&b_num.as_f64()).unwrap_or(std::cmp::Ordering::Equal) 257 | } 258 | (Value::String(a_str), Value::String(b_str)) => a_str.cmp(b_str), 259 | _ => std::cmp::Ordering::Equal, 260 | }; 261 | 262 | if sort_order == "desc" { 263 | cmp.reverse() 264 | } else { 265 | cmp 266 | } 267 | }); 268 | 269 | Ok(items) 270 | } 271 | 272 | fn group_items(&self, transformation: &Transformation, items: Vec) -> Result, Error> { 273 | let group_key = transformation.field.as_ref() 274 | .ok_or_else(|| Error::Validation("field is required for group operation".to_string()))?; 275 | 276 | let mut groups: HashMap> = HashMap::new(); 277 | 278 | for item in items { 279 | let key_value = self.get_field_value(group_key, &item).unwrap_or(Value::Null); 280 | let key = match key_value { 281 | Value::String(s) => s, 282 | Value::Number(n) => n.to_string(), 283 | Value::Bool(b) => b.to_string(), 284 | _ => "null".to_string(), 285 | }; 286 | 287 | groups.entry(key).or_insert_with(Vec::new).push(item); 288 | } 289 | 290 | // Convert groups to result format 291 | let mut result = Vec::new(); 292 | for (key, group_items) in groups { 293 | result.push(json!({ 294 | "key": key, 295 | "items": group_items, 296 | "count": group_items.len() 297 | })); 298 | } 299 | 300 | Ok(result) 301 | } 302 | 303 | fn filter_items(&self, transformation: &Transformation, items: Vec) -> Result, Error> { 304 | let mut result = Vec::new(); 305 | 306 | for item in items { 307 | if self.passes_condition(transformation, &item)? { 308 | result.push(item); 309 | } 310 | } 311 | 312 | Ok(result) 313 | } 314 | 315 | fn passes_condition(&self, transformation: &Transformation, item: &Value) -> Result { 316 | let condition = transformation.condition.as_ref() 317 | .ok_or_else(|| Error::Validation("condition is required for filter operation".to_string()))?; 318 | 319 | let field_value = self.get_field_value(&condition.field, item)?; 320 | 321 | match condition.operator.as_str() { 322 | "equals" => Ok(field_value == condition.value), 323 | "notEquals" => Ok(field_value != condition.value), 324 | "contains" => { 325 | if let (Value::String(haystack), Value::String(needle)) = (&field_value, &condition.value) { 326 | Ok(haystack.contains(needle)) 327 | } else { 328 | Ok(false) 329 | } 330 | } 331 | "greaterThan" => { 332 | match (&field_value, &condition.value) { 333 | (Value::Number(a), Value::Number(b)) => { 334 | if let (Some(a_f64), Some(b_f64)) = (a.as_f64(), b.as_f64()) { 335 | Ok(a_f64 > b_f64) 336 | } else { 337 | Ok(false) 338 | } 339 | } 340 | _ => Ok(false) 341 | } 342 | } 343 | "lessThan" => { 344 | match (&field_value, &condition.value) { 345 | (Value::Number(a), Value::Number(b)) => { 346 | if let (Some(a_f64), Some(b_f64)) = (a.as_f64(), b.as_f64()) { 347 | Ok(a_f64 < b_f64) 348 | } else { 349 | Ok(false) 350 | } 351 | } 352 | _ => Ok(false) 353 | } 354 | } 355 | _ => Err(Error::Validation(format!("Unsupported operator: {}", condition.operator))) 356 | } 357 | } 358 | 359 | fn get_field_value(&self, field_path: &str, data: &Value) -> Result { 360 | let parts: Vec<&str> = field_path.split('.').collect(); 361 | let mut current = data; 362 | 363 | for part in parts { 364 | if let Some(next) = current.get(part) { 365 | current = next; 366 | } else { 367 | return Ok(Value::Null); 368 | } 369 | } 370 | 371 | Ok(current.clone()) 372 | } 373 | } 374 | 375 | #[cfg(test)] 376 | mod tests { 377 | use super::*; 378 | use uuid::Uuid; 379 | 380 | #[test] 381 | fn test_transform_node_creation() { 382 | let node = TransformNode::new( 383 | Uuid::new_v4(), 384 | "Test Transform".to_string(), 385 | "transform".to_string(), 386 | Some("Test description".to_string()), 387 | Some(json!({ 388 | "transformations": [{ 389 | "operation": "add_field", 390 | "new_field": "processed", 391 | "value": true 392 | }], 393 | "mode": "each" 394 | })), 395 | ); 396 | 397 | assert_eq!(node.name(), "Test Transform"); 398 | assert!(node.validate()); 399 | } 400 | 401 | #[test] 402 | fn test_transform_validation() { 403 | let node = TransformNode::new( 404 | Uuid::new_v4(), 405 | "Test Transform".to_string(), 406 | "transform".to_string(), 407 | None, 408 | Some(json!({ 409 | "transformations": [], 410 | "mode": "each" 411 | })), 412 | ); 413 | 414 | assert!(!node.validate()); 415 | } 416 | 417 | #[test] 418 | fn test_add_field_transformation() { 419 | let node = TransformNode::new( 420 | Uuid::new_v4(), 421 | "Test Transform".to_string(), 422 | "transform".to_string(), 423 | None, 424 | Some(json!({ 425 | "transformations": [{ 426 | "operation": "add_field", 427 | "new_field": "processed", 428 | "value": true 429 | }], 430 | "mode": "each" 431 | })), 432 | ); 433 | 434 | let mut input = std::collections::HashMap::new(); 435 | input.insert(Uuid::new_v4(), json!([ 436 | {"name": "item1", "value": 1}, 437 | {"name": "item2", "value": 2} 438 | ])); 439 | 440 | let result = node.execute(&input); 441 | assert!(result.is_ok()); 442 | 443 | if let Ok(Value::Array(items)) = result { 444 | assert_eq!(items.len(), 2); 445 | for item in items { 446 | assert_eq!(item["processed"], true); 447 | assert!(item["name"].is_string()); 448 | assert!(item["value"].is_number()); 449 | } 450 | } 451 | } 452 | 453 | #[test] 454 | fn test_sort_transformation() { 455 | let node = TransformNode::new( 456 | Uuid::new_v4(), 457 | "Test Transform".to_string(), 458 | "transform".to_string(), 459 | None, 460 | Some(json!({ 461 | "transformations": [{ 462 | "operation": "sort", 463 | "sort_key": "value", 464 | "sort_order": "desc" 465 | }], 466 | "mode": "batch" 467 | })), 468 | ); 469 | 470 | let mut input = std::collections::HashMap::new(); 471 | input.insert(Uuid::new_v4(), json!([ 472 | {"name": "item1", "value": 1}, 473 | {"name": "item3", "value": 3}, 474 | {"name": "item2", "value": 2} 475 | ])); 476 | 477 | let result = node.execute(&input); 478 | assert!(result.is_ok()); 479 | 480 | if let Ok(Value::Array(items)) = result { 481 | assert_eq!(items.len(), 3); 482 | assert_eq!(items[0]["value"], 3); 483 | assert_eq!(items[1]["value"], 2); 484 | assert_eq!(items[2]["value"], 1); 485 | } 486 | } 487 | } --------------------------------------------------------------------------------