240 lines
8.1 KiB
Rust
240 lines
8.1 KiB
Rust
//! DAG(有向无环图)定义与拓扑排序
|
||
|
||
use std::collections::{HashMap, HashSet, VecDeque};
|
||
|
||
use df_types::error::Result;
|
||
use df_types::types::NodeId;
|
||
|
||
use crate::node::Node;
|
||
|
||
/// 图的边:从 source 到 target
|
||
#[derive(Debug, Clone)]
|
||
pub struct Edge {
|
||
pub source: NodeId,
|
||
pub target: NodeId,
|
||
/// 边的条件表达式(可选)
|
||
pub condition: Option<String>,
|
||
}
|
||
|
||
/// DAG 结构
|
||
pub struct Dag {
|
||
/// 节点集合
|
||
pub nodes: HashMap<NodeId, Box<dyn Node>>,
|
||
/// 边集合
|
||
pub edges: Vec<Edge>,
|
||
/// 节点级配置(DagDef.nodes[id].config 提取自 build_dag,run 时与全局 config deep_merge)
|
||
///
|
||
/// 语义:节点级覆盖全局级。DagExecutor 构造 NodeContext 时执行
|
||
/// `deep_merge(initial_config, node_configs[id])`,节点定义里写的同名 key 优先于
|
||
/// run_workflow 传入的全局 config。
|
||
pub node_configs: HashMap<NodeId, serde_json::Value>,
|
||
}
|
||
|
||
impl Dag {
|
||
/// 创建空 DAG
|
||
pub fn new() -> Self {
|
||
Self {
|
||
nodes: HashMap::new(),
|
||
edges: Vec::new(),
|
||
node_configs: HashMap::new(),
|
||
}
|
||
}
|
||
|
||
/// 添加节点
|
||
pub fn add_node(&mut self, id: NodeId, node: Box<dyn Node>) {
|
||
self.nodes.insert(id, node);
|
||
}
|
||
|
||
/// 添加节点(附带节点级配置)
|
||
///
|
||
/// 节点级配置在 DagExecutor 构造 NodeContext 时覆盖全局 config(deep_merge)。
|
||
pub fn add_node_with_config(
|
||
&mut self,
|
||
id: NodeId,
|
||
node: Box<dyn Node>,
|
||
config: serde_json::Value,
|
||
) {
|
||
self.nodes.insert(id.clone(), node);
|
||
self.node_configs.insert(id, config);
|
||
}
|
||
|
||
/// 添加边
|
||
pub fn add_edge(&mut self, source: NodeId, target: NodeId) {
|
||
self.edges.push(Edge {
|
||
source,
|
||
target,
|
||
condition: None,
|
||
});
|
||
}
|
||
|
||
/// 添加带条件的边
|
||
pub fn add_edge_with_condition(
|
||
&mut self,
|
||
source: NodeId,
|
||
target: NodeId,
|
||
condition: String,
|
||
) {
|
||
self.edges.push(Edge {
|
||
source,
|
||
target,
|
||
condition: Some(condition),
|
||
});
|
||
}
|
||
|
||
/// 拓扑排序 — 返回按执行顺序排列的节点 ID 层级
|
||
///
|
||
/// 返回 Vec<Vec<NodeId>>,每层内的节点可以并行执行。
|
||
///
|
||
/// 复杂度:O(V+E)。一次性遍历边构建邻接索引(出边表)与入度表,
|
||
/// BFS 分层只走索引查询(每条边仅被访问一次),不再为每个节点全表扫描边集合。
|
||
pub fn topological_layers(&self) -> Result<Vec<Vec<NodeId>>> {
|
||
let node_ids: HashSet<NodeId> = self.nodes.keys().cloned().collect();
|
||
let mut in_degree: HashMap<NodeId, usize> = HashMap::new();
|
||
// 邻接出边表:source → 直接后继列表(仅含两端均在 nodes 内的有效边)
|
||
let mut adjacency_out: HashMap<NodeId, Vec<NodeId>> = HashMap::new();
|
||
|
||
// 初始化入度(确保每个节点都有表项,便于后续 O(1) 修改)
|
||
for id in &node_ids {
|
||
in_degree.insert(id.clone(), 0);
|
||
}
|
||
// 单次 O(E) 遍历:同时构建入度表与出边表
|
||
for edge in &self.edges {
|
||
// 仅收录两端均为已注册节点的边,跳过野节点
|
||
if node_ids.contains(&edge.source) && node_ids.contains(&edge.target) {
|
||
*in_degree.get_mut(&edge.target).unwrap() += 1;
|
||
adjacency_out
|
||
.entry(edge.source.clone())
|
||
.or_default()
|
||
.push(edge.target.clone());
|
||
}
|
||
}
|
||
|
||
// BFS 分层
|
||
let mut layers = Vec::new();
|
||
let mut queue: VecDeque<NodeId> = in_degree
|
||
.iter()
|
||
.filter(|(_, °)| deg == 0)
|
||
.map(|(id, _)| id.clone())
|
||
.collect();
|
||
|
||
let mut processed = 0usize;
|
||
while !queue.is_empty() {
|
||
let mut layer: Vec<NodeId> = Vec::new();
|
||
let layer_size = queue.len();
|
||
for _ in 0..layer_size {
|
||
if let Some(id) = queue.pop_front() {
|
||
layer.push(id.clone());
|
||
// 出边已索引,O(出度) 遍历而非 O(E) 全表扫描
|
||
if let Some(succs) = adjacency_out.get(&id) {
|
||
for succ in succs {
|
||
let deg = in_degree.get_mut(succ).unwrap();
|
||
*deg -= 1;
|
||
if *deg == 0 {
|
||
queue.push_back(succ.clone());
|
||
}
|
||
}
|
||
}
|
||
processed += 1;
|
||
}
|
||
}
|
||
layers.push(layer);
|
||
}
|
||
|
||
if processed != node_ids.len() {
|
||
return Err(df_types::error::Error::Workflow(
|
||
"DAG 中存在环,无法进行拓扑排序".to_string(),
|
||
));
|
||
}
|
||
|
||
Ok(layers)
|
||
}
|
||
}
|
||
|
||
impl Default for Dag {
|
||
fn default() -> Self {
|
||
Self::new()
|
||
}
|
||
}
|
||
|
||
/// 深度合并两个 JSON 值:节点级 `node` 覆盖全局级 `global`。
|
||
///
|
||
/// 规则:
|
||
/// - 两端均为 Object:递归合并(同 key 时节点级覆盖全局级,新增 key 各自保留)。
|
||
/// - 非两端 Object:节点级直接覆盖全局级(包括 `Null`,符合 JSON 显式空语义)。
|
||
/// - 节点级为 `Null` 的语义是「显式置空」,按非 Object 规则覆盖(直接返回 Null);
|
||
/// 若调用方希望「跳过节点配置」,应在构造 Dag 时不写入该 key(node_configs 缺失即用全局)。
|
||
///
|
||
/// 用途:DagExecutor 构造 NodeContext 时 `deep_merge(initial_config, node_config)`,
|
||
/// 让节点定义里写的 NodeDef.config 优先于 run_workflow 传入的全局 config。
|
||
pub fn deep_merge(global: &serde_json::Value, node: &serde_json::Value) -> serde_json::Value {
|
||
use serde_json::Value;
|
||
match (global, node) {
|
||
(Value::Object(g), Value::Object(n)) => {
|
||
let mut merged = g.clone();
|
||
for (k, nv) in n {
|
||
// 同 key 递归(支持嵌套对象局部覆盖);不同类型时递归退化为「节点级直接覆盖」
|
||
let gv = merged.get(k).cloned();
|
||
let merged_v = match gv {
|
||
Some(gv) => deep_merge(&gv, nv),
|
||
None => nv.clone(),
|
||
};
|
||
merged.insert(k.clone(), merged_v);
|
||
}
|
||
Value::Object(merged)
|
||
}
|
||
// 非两端 Object:节点级覆盖全局级(含 Null、标量、数组)
|
||
_ => node.clone(),
|
||
}
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod deep_merge_tests {
|
||
use super::deep_merge;
|
||
use serde_json::json;
|
||
|
||
#[test]
|
||
fn node_overrides_global_scalar() {
|
||
assert_eq!(deep_merge(&json!({"a": 1}), &json!({"a": 2})), json!({"a": 2}));
|
||
}
|
||
|
||
#[test]
|
||
fn node_adds_new_key() {
|
||
assert_eq!(
|
||
deep_merge(&json!({"a": 1}), &json!({"b": 2})),
|
||
json!({"a": 1, "b": 2})
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn nested_object_recursive_merge() {
|
||
assert_eq!(
|
||
deep_merge(&json!({"o": {"x": 1, "y": 2}}), &json!({"o": {"y": 9}})),
|
||
json!({"o": {"x": 1, "y": 9}})
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn empty_node_config_is_noop() {
|
||
// 核心保现有调用方行为:节点 config 为空对象 → 结果等同全局(零行为破坏)
|
||
let global = json!({"title": "确认", "options": ["同意"]});
|
||
assert_eq!(deep_merge(&global, &json!({})), global);
|
||
}
|
||
|
||
#[test]
|
||
fn missing_node_config_treated_as_noop() {
|
||
// DagExecutor 对缺失 key 的节点不调 deep_merge(直接用 global),此处覆盖语义对照:
|
||
// 若强行把缺失视作 Null,应覆盖(显式空语义),与「缺失」不同(缺失走 if let Some 分支)。
|
||
let global = json!({"a": 1});
|
||
assert_eq!(deep_merge(&global, &json!(null)), json!(null));
|
||
}
|
||
|
||
#[test]
|
||
fn array_replaces_not_concat() {
|
||
// 非两端 Object(数组)→ 节点级直接覆盖,不拼接
|
||
assert_eq!(
|
||
deep_merge(&json!({"arr": [1, 2]}), &json!({"arr": [3]})),
|
||
json!({"arr": [3]})
|
||
);
|
||
}
|
||
}
|