//! YAML 模板加载器 — 从 YAML 字符串加载 DagDef + 校验 //! //! 模板格式: //! ```yaml //! name: 代码审查 //! description: AI 驱动的代码审查流程 //! nodes: //! review: //! type: ai //! config: //! prompt: "审查以下代码变更..." //! persona_id: reviewer //! notify: //! type: notify //! config: //! type: desktop //! title: "审查完成" //! edges: //! - from: review //! to: notify //! ``` use serde::{Deserialize, Serialize}; use std::collections::HashMap; use crate::dag_def::{DagDef, EdgeDef, NodeDef}; /// YAML 模板顶层结构 #[derive(Debug, Clone, Serialize, Deserialize)] pub struct WorkflowTemplate { pub name: String, #[serde(default)] pub description: String, pub nodes: HashMap, #[serde(default)] pub edges: Vec, } /// 模板节点(YAML 友好命名) #[derive(Debug, Clone, Serialize, Deserialize)] pub struct TemplateNode { /// 节点类型(ai/script/human/git/http/notify/subflow) #[serde(rename = "type")] pub node_type: String, #[serde(default)] pub config: serde_json::Value, #[serde(default)] pub label: Option, } /// 模板边(YAML 友好命名) #[derive(Debug, Clone, Serialize, Deserialize)] pub struct TemplateEdge { pub from: String, pub to: String, #[serde(default)] pub condition: Option, } /// 模板加载错误 #[derive(Debug)] pub enum TemplateError { /// YAML 解析失败 YamlParse(String), /// 校验失败(空节点/未知类型/依赖环) Validation(String), } impl std::fmt::Display for TemplateError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::YamlParse(msg) => write!(f, "YAML 解析失败: {}", msg), Self::Validation(msg) => write!(f, "模板校验失败: {}", msg), } } } impl std::error::Error for TemplateError {} /// 已知的节点类型白名单 const KNOWN_NODE_TYPES: &[&str] = &[ "ai", "script", "human", "git", "http", "notify", "subflow", ]; /// 从 YAML 字符串加载模板并转换为 DagDef /// /// 步骤: /// 1. YAML → WorkflowTemplate(serde_yaml) /// 2. 校验(节点非空/类型合法/边引用存在/无环) /// 3. 转换为 DagDef pub fn load_template(yaml: &str) -> Result { // 1. YAML 解析 let template: WorkflowTemplate = serde_yaml::from_str(yaml) .map_err(|e| TemplateError::YamlParse(e.to_string()))?; // 2. 校验 validate_template(&template)?; // 3. 转换为 DagDef let mut dag = DagDef::new(); for (id, node) in &template.nodes { dag.nodes.insert( id.clone(), NodeDef { id: id.clone(), node_type: node.node_type.clone(), config: node.config.clone(), label: node.label.clone(), }, ); } for edge in &template.edges { dag.edges.push(EdgeDef { source: edge.from.clone(), target: edge.to.clone(), condition: edge.condition.clone(), }); } Ok(dag) } /// 校验模板结构 fn validate_template(t: &WorkflowTemplate) -> Result<(), TemplateError> { // 节点非空 if t.nodes.is_empty() { return Err(TemplateError::Validation("模板节点不能为空".into())); } // 节点类型合法 for (id, node) in &t.nodes { if !KNOWN_NODE_TYPES.contains(&node.node_type.as_str()) { return Err(TemplateError::Validation(format!( "节点 '{}' 使用了未知类型 '{}',已知类型: {:?}", id, node.node_type, KNOWN_NODE_TYPES ))); } } // 边引用的节点存在 let node_ids: std::collections::HashSet<&String> = t.nodes.keys().collect(); for edge in &t.edges { if !node_ids.contains(&edge.from) { return Err(TemplateError::Validation(format!( "边的起点 '{}' 不存在", edge.from ))); } if !node_ids.contains(&edge.to) { return Err(TemplateError::Validation(format!( "边的终点 '{}' 不存在", edge.to ))); } } // 依赖无环(DFS 检测) if has_cycle(&t.edges) { return Err(TemplateError::Validation("依赖存在环".into())); } Ok(()) } /// DFS 检测边列表是否有环 fn has_cycle(edges: &[TemplateEdge]) -> bool { use std::collections::{HashMap, HashSet}; // 建邻接表 let mut adj: HashMap<&str, Vec<&str>> = HashMap::new(); for e in edges { adj.entry(e.from.as_str()).or_default().push(e.to.as_str()); } let mut visited: HashSet<&str> = HashSet::new(); let mut in_stack: HashSet<&str> = HashSet::new(); fn dfs<'a>( node: &'a str, adj: &HashMap<&'a str, Vec<&'a str>>, visited: &mut HashSet<&'a str>, in_stack: &mut HashSet<&'a str>, ) -> bool { if in_stack.contains(node) { return true; // 环 } if visited.contains(node) { return false; // 已访问过,无环 } visited.insert(node); in_stack.insert(node); if let Some(neighbors) = adj.get(node) { for next in neighbors { if dfs(next, adj, visited, in_stack) { return true; } } } in_stack.remove(node); false } for e in edges { let start = e.from.as_str(); if dfs(start, &adj, &mut visited, &mut in_stack) { return true; } } false } #[cfg(test)] mod tests { use super::*; #[test] fn tpl_01_valid_template_loads() { let yaml = r#" name: 代码审查 description: AI 驱动的代码审查 nodes: review: type: ai config: prompt: "审查代码" notify: type: notify config: type: desktop title: "完成" edges: - from: review to: notify "#; let dag = load_template(yaml).unwrap(); assert_eq!(dag.nodes.len(), 2); assert_eq!(dag.edges.len(), 1); assert!(dag.nodes.contains_key("review")); assert_eq!(dag.nodes.get("review").unwrap().node_type, "ai"); } #[test] fn tpl_02_empty_nodes_rejected() { let yaml = "name: 空\nnodes: {}\n"; let result = load_template(yaml); assert!(matches!(result, Err(TemplateError::Validation(_)))); } #[test] fn tpl_03_unknown_node_type_rejected() { let yaml = r#" name: test nodes: bad: type: unknown_type config: {} "#; let result = load_template(yaml); assert!(matches!(result, Err(TemplateError::Validation(_)))); } #[test] fn tpl_04_cycle_detected() { let yaml = r#" name: 环 nodes: a: type: ai config: {} b: type: ai config: {} edges: - from: a to: b - from: b to: a "#; let result = load_template(yaml); assert!(matches!(result, Err(TemplateError::Validation(ref msg)) if msg.contains("环"))); } #[test] fn tpl_05_edge_to_nonexistent_rejected() { let yaml = r#" name: test nodes: a: type: ai config: {} edges: - from: a to: ghost "#; let result = load_template(yaml); assert!(matches!(result, Err(TemplateError::Validation(_)))); } #[test] fn tpl_06_no_edges_ok() { let yaml = r#" name: 单节点 nodes: solo: type: script config: command: "echo hello" "#; let dag = load_template(yaml).unwrap(); assert_eq!(dag.nodes.len(), 1); assert!(dag.edges.is_empty()); } #[test] fn tpl_07_invalid_yaml_rejected() { let yaml = "not: valid: yaml: {{{"; let result = load_template(yaml); assert!(matches!(result, Err(TemplateError::YamlParse(_)))); } #[test] fn tpl_08_all_known_node_types() { for nt in KNOWN_NODE_TYPES { let yaml = format!( "name: test\nnodes:\n n:\n type: {}\n config: {{}}", nt ); let result = load_template(&yaml); assert!(result.is_ok(), "节点类型 {} 应合法", nt); } } }