Skip to main content

sz_rust_workflow/definition/
node.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2024-2026 SZ-Rust Team
3//
4use serde::{Deserialize, Serialize};
5
6use super::strategy::{ApprovalStrategyType, CandidateStrategy, FaultStrategy};
7
8/// 节点类型枚举,对齐 spec 6.3.2。
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
10#[serde(rename_all = "snake_case")]
11pub enum NodeType {
12    Start,
13    Approval,
14    Condition,
15    Parallel,
16    Plugin,
17    End,
18}
19
20/// 节点定义。
21#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
22pub struct Node {
23    /// 节点 ID,流程定义内唯一
24    pub node_id: String,
25    /// 节点类型
26    pub node_type: NodeType,
27    /// 节点配置(按类型不同)
28    #[serde(flatten)]
29    pub config: NodeConfig,
30}
31
32/// 节点配置枚举,按 [`NodeType`] 区分。
33#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
34#[serde(tag = "kind", rename_all = "snake_case")]
35pub enum NodeConfig {
36    Start {
37        next: String,
38    },
39    Approval {
40        approval_strategy: ApprovalStrategyType,
41        candidate_strategy: CandidateStrategy,
42        next: String,
43    },
44    Condition {
45        branches: Vec<ConditionBranch>,
46    },
47    Parallel {
48        branches: Vec<String>,
49        join_node: String,
50    },
51    Plugin {
52        /// 能力名,符合 `{plugin_name}.{capability_name}` 命名规范
53        capability_name: String,
54        /// 能力版本范围(semver)
55        #[serde(default = "default_version_range")]
56        capability_version_range: String,
57        /// 参数映射(键为能力参数名,值为上下文路径或常量)
58        #[serde(default)]
59        args_mapping: serde_json::Value,
60        /// 容错策略
61        #[serde(default)]
62        fault_strategy: FaultStrategy,
63        /// 输出 Schema(JSON Schema,校验能力返回值)
64        #[serde(default, skip_serializing_if = "Option::is_none")]
65        output_schema: Option<serde_json::Value>,
66        /// 下一节点
67        next: String,
68    },
69    End,
70}
71
72fn default_version_range() -> String {
73    "*".to_string()
74}
75
76/// 条件分支,用于 [`NodeConfig::Condition`]。
77#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
78pub struct ConditionBranch {
79    /// 守卫条件表达式
80    pub condition: String,
81    /// 满足条件时跳转的节点 ID
82    pub next: String,
83}
84
85/// 节点边,用于 [`super::models::ApprovalFlowDefinition`]。
86#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
87pub struct NodeEdge {
88    pub from: String,
89    pub to: String,
90    #[serde(default, skip_serializing_if = "Option::is_none")]
91    pub condition: Option<String>,
92}
93
94impl Node {
95    /// 获取节点的所有后继节点 ID(用于可达性分析)。
96    pub fn successors(&self) -> Vec<&str> {
97        match &self.config {
98            NodeConfig::Start { next } => vec![next.as_str()],
99            NodeConfig::Approval { next, .. } => vec![next.as_str()],
100            NodeConfig::Condition { branches } => {
101                branches.iter().map(|b| b.next.as_str()).collect()
102            }
103            NodeConfig::Parallel {
104                branches,
105                join_node,
106            } => {
107                let mut v: Vec<&str> = branches.iter().map(|s| s.as_str()).collect();
108                v.push(join_node.as_str());
109                v
110            }
111            NodeConfig::Plugin { next, .. } => vec![next.as_str()],
112            NodeConfig::End => vec![],
113        }
114    }
115}
116
117#[cfg(test)]
118mod tests {
119    use super::*;
120
121    #[test]
122    fn start_node_serde() {
123        let n = Node {
124            node_id: "start".into(),
125            node_type: NodeType::Start,
126            config: NodeConfig::Start { next: "n1".into() },
127        };
128        let json = serde_json::to_string(&n).unwrap();
129        let back: Node = serde_json::from_str(&json).unwrap();
130        assert_eq!(n, back);
131    }
132
133    #[test]
134    fn approval_node_serde() {
135        let n = Node {
136            node_id: "approve1".into(),
137            node_type: NodeType::Approval,
138            config: NodeConfig::Approval {
139                approval_strategy: ApprovalStrategyType::OrSign,
140                candidate_strategy: CandidateStrategy::Static {
141                    users: vec!["u1".into(), "u2".into()],
142                    roles: vec![],
143                },
144                next: "end".into(),
145            },
146        };
147        let json = serde_json::to_string(&n).unwrap();
148        let back: Node = serde_json::from_str(&json).unwrap();
149        assert_eq!(n, back);
150    }
151
152    #[test]
153    fn plugin_node_serde() {
154        let n = Node {
155            node_id: "p1".into(),
156            node_type: NodeType::Plugin,
157            config: NodeConfig::Plugin {
158                capability_name: "crm.search_customer".into(),
159                capability_version_range: "^1.0".into(),
160                args_mapping: serde_json::json!({"keyword": "$.keyword"}),
161                fault_strategy: FaultStrategy::Retry,
162                output_schema: None,
163                next: "end".into(),
164            },
165        };
166        let json = serde_json::to_string(&n).unwrap();
167        let back: Node = serde_json::from_str(&json).unwrap();
168        assert_eq!(n, back);
169    }
170
171    #[test]
172    fn condition_node_successors() {
173        let n = Node {
174            node_id: "c1".into(),
175            node_type: NodeType::Condition,
176            config: NodeConfig::Condition {
177                branches: vec![
178                    ConditionBranch {
179                        condition: "$.x > 0".into(),
180                        next: "a".into(),
181                    },
182                    ConditionBranch {
183                        condition: "$.x <= 0".into(),
184                        next: "b".into(),
185                    },
186                ],
187            },
188        };
189        assert_eq!(n.successors(), vec!["a", "b"]);
190    }
191
192    #[test]
193    fn parallel_node_successors() {
194        let n = Node {
195            node_id: "par1".into(),
196            node_type: NodeType::Parallel,
197            config: NodeConfig::Parallel {
198                branches: vec!["b1".into(), "b2".into()],
199                join_node: "join".into(),
200            },
201        };
202        assert_eq!(n.successors(), vec!["b1", "b2", "join"]);
203    }
204
205    #[test]
206    fn end_node_no_successors() {
207        let n = Node {
208            node_id: "end".into(),
209            node_type: NodeType::End,
210            config: NodeConfig::End,
211        };
212        assert!(n.successors().is_empty());
213    }
214}