Files
DevFlow/crates/df-workflow/src/dag.rs

240 lines
8.1 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.
//! 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_dagrun 时与全局 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 时覆盖全局 configdeep_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)| 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 时不写入该 keynode_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]})
);
}
}