use crate::dto::{EdgeDef, NodeDef, NodeType}; use crate::engine::model::FlowGraph; use crate::error::{WorkflowError, WorkflowResult}; /// 解析节点和边列表为 FlowGraph 并验证合法性。 pub fn parse_and_validate(nodes: &[NodeDef], edges: &[EdgeDef]) -> WorkflowResult { // 基本检查:至少有一个节点 if nodes.is_empty() { return Err(WorkflowError::InvalidDiagram("流程图不能为空".to_string())); } // 检查恰好 1 个 StartEvent let start_count = nodes .iter() .filter(|n| n.node_type == NodeType::StartEvent) .count(); if start_count == 0 { return Err(WorkflowError::InvalidDiagram( "流程图必须包含一个开始事件".to_string(), )); } if start_count > 1 { return Err(WorkflowError::InvalidDiagram( "流程图只能包含一个开始事件".to_string(), )); } // 检查至少 1 个 EndEvent let end_count = nodes .iter() .filter(|n| n.node_type == NodeType::EndEvent) .count(); if end_count == 0 { return Err(WorkflowError::InvalidDiagram( "流程图必须包含至少一个结束事件".to_string(), )); } // 检查节点 ID 唯一性 let node_ids: std::collections::HashSet<&str> = nodes.iter().map(|n| n.id.as_str()).collect(); if node_ids.len() != nodes.len() { return Err(WorkflowError::InvalidDiagram( "节点 ID 不能重复".to_string(), )); } // 检查边引用的节点存在 for e in edges { if !node_ids.contains(e.source.as_str()) { return Err(WorkflowError::InvalidDiagram(format!( "连线 {} 的源节点 {} 不存在", e.id, e.source ))); } if !node_ids.contains(e.target.as_str()) { return Err(WorkflowError::InvalidDiagram(format!( "连线 {} 的目标节点 {} 不存在", e.id, e.target ))); } } // 构建图 let graph = FlowGraph::build(nodes, edges); // 检查 StartEvent 没有入边 if let Some(start_id) = &graph.start_node_id { if !graph.get_incoming_edges(start_id).is_empty() { return Err(WorkflowError::InvalidDiagram( "开始事件不能有入边".to_string(), )); } if graph.get_outgoing_edges(start_id).is_empty() { return Err(WorkflowError::InvalidDiagram( "开始事件必须有出边".to_string(), )); } } // 检查 EndEvent 没有出边 for end_id in &graph.end_node_ids { if !graph.get_outgoing_edges(end_id).is_empty() { return Err(WorkflowError::InvalidDiagram( "结束事件不能有出边".to_string(), )); } } // 检查网关至少有一个入边和一个出边(排除 start/end) for node in nodes { match &node.node_type { NodeType::ExclusiveGateway | NodeType::ParallelGateway => { let inc = graph.get_incoming_edges(&node.id); let out = graph.get_outgoing_edges(&node.id); if inc.is_empty() { return Err(WorkflowError::InvalidDiagram(format!( "网关 '{}' 必须有至少一条入边", node.name ))); } if out.is_empty() { return Err(WorkflowError::InvalidDiagram(format!( "网关 '{}' 必须有至少一条出边", node.name ))); } // 排他网关的出边应该有条件(第一条可以无条件作为默认分支) if node.node_type == NodeType::ExclusiveGateway && out.len() > 1 { let with_condition: Vec<_> = out.iter().filter(|e| e.condition.is_some()).collect(); if with_condition.is_empty() { return Err(WorkflowError::InvalidDiagram(format!( "排他网关 '{}' 有多条出边但没有条件表达式", node.name ))); } } } _ => {} } } Ok(graph) } #[cfg(test)] mod tests { use super::*; use crate::dto::NodePosition; fn make_start() -> NodeDef { NodeDef { id: "start".to_string(), node_type: NodeType::StartEvent, name: "开始".to_string(), assignee_id: None, candidate_groups: None, service_type: None, service_config: None, position: Some(NodePosition { x: 100.0, y: 100.0 }), } } fn make_end() -> NodeDef { NodeDef { id: "end".to_string(), node_type: NodeType::EndEvent, name: "结束".to_string(), assignee_id: None, candidate_groups: None, service_type: None, service_config: None, position: Some(NodePosition { x: 100.0, y: 300.0 }), } } fn make_user_task(id: &str, name: &str) -> NodeDef { NodeDef { id: id.to_string(), node_type: NodeType::UserTask, name: name.to_string(), assignee_id: None, candidate_groups: None, service_type: None, service_config: None, position: None, } } fn make_edge(id: &str, source: &str, target: &str) -> EdgeDef { EdgeDef { id: id.to_string(), source: source.to_string(), target: target.to_string(), condition: None, label: None, } } #[test] fn test_valid_linear_flow() { let nodes = vec![make_start(), make_user_task("task1", "审批"), make_end()]; let edges = vec![ make_edge("e1", "start", "task1"), make_edge("e2", "task1", "end"), ]; let result = parse_and_validate(&nodes, &edges); assert!(result.is_ok()); let graph = result.unwrap(); assert_eq!(graph.start_node_id, Some("start".to_string())); assert_eq!(graph.end_node_ids, vec!["end".to_string()]); } #[test] fn test_no_start_event() { let nodes = vec![make_user_task("task1", "审批"), make_end()]; let edges = vec![make_edge("e1", "task1", "end")]; let result = parse_and_validate(&nodes, &edges); assert!(result.is_err()); let msg = result.unwrap_err().to_string(); assert!(msg.contains("开始事件")); } #[test] fn test_no_end_event() { let nodes = vec![make_start(), make_user_task("task1", "审批")]; let edges = vec![make_edge("e1", "start", "task1")]; let result = parse_and_validate(&nodes, &edges); assert!(result.is_err()); let msg = result.unwrap_err().to_string(); assert!(msg.contains("结束事件")); } #[test] fn test_duplicate_node_id() { let nodes = vec![ make_start(), NodeDef { id: "start".to_string(), // 重复 ID node_type: NodeType::EndEvent, name: "结束".to_string(), assignee_id: None, candidate_groups: None, service_type: None, service_config: None, position: None, }, ]; let edges = vec![]; let result = parse_and_validate(&nodes, &edges); assert!(result.is_err()); } #[test] fn test_end_event_with_outgoing() { let nodes = vec![make_start(), make_end()]; let edges = vec![ make_edge("e1", "start", "end"), make_edge("e2", "end", "start"), // 结束事件有出边 ]; let result = parse_and_validate(&nodes, &edges); assert!(result.is_err()); } #[test] fn test_exclusive_gateway_without_conditions() { let nodes = vec![ make_start(), NodeDef { id: "gw1".to_string(), node_type: NodeType::ExclusiveGateway, name: "判断".to_string(), assignee_id: None, candidate_groups: None, service_type: None, service_config: None, position: None, }, make_end(), ]; let edges = vec![ make_edge("e1", "start", "gw1"), make_edge("e2", "gw1", "end"), make_edge("e3", "gw1", "end"), // 两条出边无条件 ]; let result = parse_and_validate(&nodes, &edges); assert!(result.is_err()); } }