Files
nj/crates/erp-workflow/src/engine/parser.rs
iven c539e6fd83 feat: initialize Nuanji (Warm Notes) project
- Base platform from base.git (ERP base: auth, core, config, message, workflow, plugin)
- Created erp-diary module skeleton (lib.rs, dto.rs, error.rs, event.rs, state.rs)
- Integrated erp-diary into workspace and erp-server
- Added DiaryModule registration in main.rs
- Added DiaryState FromRef in state.rs
- Diary routes mounted (empty routes, ready for implementation)
- Product design spec v1.2 preserved in docs/
- Implementation plan preserved in plans/

Cargo check: OK
Cargo test: OK (78+ base tests passing)
2026-05-31 20:52:19 +08:00

492 lines
16 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<FlowGraph> {
// 基本检查:至少有一个节点
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());
}
// ---- 边界测试扩展 ----
#[test]
fn test_empty_nodes_rejected() {
let result = parse_and_validate(&[], &[]);
assert!(result.is_err());
let msg = result.unwrap_err().to_string();
assert!(msg.contains("不能为空"));
}
#[test]
fn test_multiple_start_events_rejected() {
let nodes = vec![
make_start(),
NodeDef {
id: "start2".to_string(),
node_type: NodeType::StartEvent,
name: "开始2".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", "end"),
make_edge("e2", "start2", "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_edge_references_nonexistent_source() {
let nodes = vec![make_start(), make_end()];
let edges = vec![make_edge("e1", "ghost", "end")];
let result = parse_and_validate(&nodes, &edges);
assert!(result.is_err());
let msg = result.unwrap_err().to_string();
assert!(msg.contains("源节点") && msg.contains("不存在"));
}
#[test]
fn test_edge_references_nonexistent_target() {
let nodes = vec![make_start(), make_end()];
let edges = vec![make_edge("e1", "start", "ghost")];
let result = parse_and_validate(&nodes, &edges);
assert!(result.is_err());
let msg = result.unwrap_err().to_string();
assert!(msg.contains("目标节点") && msg.contains("不存在"));
}
#[test]
fn test_start_event_with_incoming_edge_rejected() {
let nodes = vec![make_start(), make_end()];
let edges = vec![
make_edge("e1", "start", "end"),
make_edge("e2", "end", "start"), // start 有入边
];
let result = parse_and_validate(&nodes, &edges);
assert!(result.is_err());
let msg = result.unwrap_err().to_string();
assert!(msg.contains("入边"));
}
#[test]
fn test_start_event_without_outgoing_edge_rejected() {
let nodes = vec![make_start(), make_end()];
let edges = vec![]; // start 没有出边
let result = parse_and_validate(&nodes, &edges);
assert!(result.is_err());
let msg = result.unwrap_err().to_string();
assert!(msg.contains("出边"));
}
#[test]
fn test_exclusive_gateway_single_outgoing_ok() {
// 单条出边的排他网关不需要条件
let nodes = vec![
make_start(),
NodeDef {
id: "gw".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", "gw"), make_edge("e2", "gw", "end")];
assert!(parse_and_validate(&nodes, &edges).is_ok());
}
#[test]
fn test_exclusive_gateway_with_conditions_ok() {
let nodes = vec![
make_start(),
NodeDef {
id: "gw".to_string(),
node_type: NodeType::ExclusiveGateway,
name: "金额判断".to_string(),
assignee_id: None,
candidate_groups: None,
service_type: None,
service_config: None,
position: None,
},
make_user_task("task_a", "小额审批"),
make_user_task("task_b", "大额审批"),
make_end(),
];
let edges = vec![
make_edge("e1", "start", "gw"),
EdgeDef {
id: "e2".to_string(),
source: "gw".to_string(),
target: "task_a".to_string(),
condition: Some("amount <= 1000".to_string()),
label: None,
},
EdgeDef {
id: "e3".to_string(),
source: "gw".to_string(),
target: "task_b".to_string(),
condition: Some("amount > 1000".to_string()),
label: None,
},
make_edge("e4", "task_a", "end"),
make_edge("e5", "task_b", "end"),
];
assert!(parse_and_validate(&nodes, &edges).is_ok());
}
#[test]
fn test_gateway_without_incoming_rejected() {
let nodes = vec![
make_start(),
NodeDef {
id: "gw".to_string(),
node_type: NodeType::ParallelGateway,
name: "并行".to_string(),
assignee_id: None,
candidate_groups: None,
service_type: None,
service_config: None,
position: None,
},
make_end(),
];
// gw 没有入边
let edges = vec![
make_edge("e1", "start", "end"),
make_edge("e2", "gw", "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_gateway_without_outgoing_rejected() {
let nodes = vec![
make_start(),
NodeDef {
id: "gw".to_string(),
node_type: NodeType::ParallelGateway,
name: "并行".to_string(),
assignee_id: None,
candidate_groups: None,
service_type: None,
service_config: None,
position: None,
},
make_end(),
];
// gw 没有出边
let edges = vec![
make_edge("e1", "start", "gw"),
make_edge("e2", "start", "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_parallel_gateway_valid() {
let nodes = vec![
make_start(),
NodeDef {
id: "fork".to_string(),
node_type: NodeType::ParallelGateway,
name: "拆分".to_string(),
assignee_id: None,
candidate_groups: None,
service_type: None,
service_config: None,
position: None,
},
make_user_task("a", "任务A"),
make_user_task("b", "任务B"),
make_end(),
];
let edges = vec![
make_edge("e1", "start", "fork"),
make_edge("e2", "fork", "a"),
make_edge("e3", "fork", "b"),
make_edge("e4", "a", "end"),
make_edge("e5", "b", "end"),
];
assert!(parse_and_validate(&nodes, &edges).is_ok());
}
}