tatara_core/domain/
dag.rs1use serde::{Deserialize, Serialize};
4use std::collections::{HashMap, HashSet, VecDeque};
5
6#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct JobDependency {
9 pub job_id: String,
11
12 #[serde(default)]
14 pub condition: DependencyCondition,
15}
16
17#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
19#[serde(rename_all = "snake_case")]
20pub enum DependencyCondition {
21 #[default]
23 Healthy,
24 Complete,
26 OutputReady,
28}
29
30#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct JobOutput {
33 pub key: String,
34 pub value: String,
35}
36
37pub fn topological_sort(
40 job_ids: &[String],
41 dependencies: &HashMap<String, Vec<String>>,
42) -> Result<Vec<String>, Vec<String>> {
43 let mut in_degree: HashMap<&str, usize> = HashMap::new();
44 let mut adj: HashMap<&str, Vec<&str>> = HashMap::new();
45
46 for id in job_ids {
47 in_degree.entry(id.as_str()).or_insert(0);
48 adj.entry(id.as_str()).or_default();
49 }
50
51 let job_set: HashSet<&str> = job_ids.iter().map(|s| s.as_str()).collect();
52
53 for (job_id, deps) in dependencies {
54 for dep in deps {
55 if !job_set.contains(dep.as_str()) {
57 return Err(vec![format!(
58 "job '{}' depends on unknown job '{}'",
59 job_id, dep
60 )]);
61 }
62 adj.entry(dep.as_str()).or_default().push(job_id.as_str());
63 *in_degree.entry(job_id.as_str()).or_insert(0) += 1;
64 }
65 }
66
67 let mut queue: VecDeque<&str> = in_degree
68 .iter()
69 .filter(|(_, °)| deg == 0)
70 .map(|(&id, _)| id)
71 .collect();
72
73 let mut sorted = Vec::new();
74
75 while let Some(node) = queue.pop_front() {
76 sorted.push(node.to_string());
77 if let Some(neighbors) = adj.get(node) {
78 for &neighbor in neighbors {
79 if let Some(deg) = in_degree.get_mut(neighbor) {
80 *deg -= 1;
81 if *deg == 0 {
82 queue.push_back(neighbor);
83 }
84 }
85 }
86 }
87 }
88
89 if sorted.len() == job_ids.len() {
90 Ok(sorted)
91 } else {
92 let cycle: Vec<String> = in_degree
93 .iter()
94 .filter(|(_, °)| deg > 0)
95 .map(|(&id, _)| id.to_string())
96 .collect();
97 Err(cycle)
98 }
99}
100
101#[cfg(test)]
102mod tests {
103 use super::*;
104
105 #[test]
106 fn test_simple_dag() {
107 let ids = vec!["a".into(), "b".into(), "c".into()];
108 let mut deps = HashMap::new();
109 deps.insert("b".into(), vec!["a".into()]);
110 deps.insert("c".into(), vec!["b".into()]);
111
112 let sorted = topological_sort(&ids, &deps).unwrap();
113 assert_eq!(sorted, vec!["a", "b", "c"]);
114 }
115
116 #[test]
117 fn test_parallel_deps() {
118 let ids = vec!["a".into(), "b".into(), "c".into(), "d".into()];
119 let mut deps = HashMap::new();
120 deps.insert("c".into(), vec!["a".into(), "b".into()]);
121 deps.insert("d".into(), vec!["c".into()]);
122
123 let sorted = topological_sort(&ids, &deps).unwrap();
124 let pos_a = sorted.iter().position(|x| x == "a").unwrap();
126 let pos_b = sorted.iter().position(|x| x == "b").unwrap();
127 let pos_c = sorted.iter().position(|x| x == "c").unwrap();
128 let pos_d = sorted.iter().position(|x| x == "d").unwrap();
129 assert!(pos_a < pos_c);
130 assert!(pos_b < pos_c);
131 assert!(pos_c < pos_d);
132 }
133
134 #[test]
135 fn test_cycle_detection() {
136 let ids = vec!["a".into(), "b".into(), "c".into()];
137 let mut deps = HashMap::new();
138 deps.insert("a".into(), vec!["c".into()]);
139 deps.insert("b".into(), vec!["a".into()]);
140 deps.insert("c".into(), vec!["b".into()]);
141
142 let result = topological_sort(&ids, &deps);
143 assert!(result.is_err());
144 }
145
146 #[test]
147 fn test_no_deps() {
148 let ids = vec!["a".into(), "b".into(), "c".into()];
149 let deps = HashMap::new();
150
151 let sorted = topological_sort(&ids, &deps).unwrap();
152 assert_eq!(sorted.len(), 3);
153 }
154
155 #[test]
156 fn test_invalid_dependency() {
157 let ids = vec!["a".into(), "b".into()];
158 let mut deps = HashMap::new();
159 deps.insert("b".into(), vec!["nonexistent".into()]);
160
161 let result = topological_sort(&ids, &deps);
162 assert!(result.is_err());
163 let err = result.unwrap_err();
164 assert!(err[0].contains("unknown job"));
165 }
166
167 #[test]
168 fn test_self_loop() {
169 let ids = vec!["a".into()];
170 let mut deps = HashMap::new();
171 deps.insert("a".into(), vec!["a".into()]);
172
173 let result = topological_sort(&ids, &deps);
174 assert!(result.is_err());
175 }
176
177 #[test]
178 fn test_single_node() {
179 let ids = vec!["only".into()];
180 let deps = HashMap::new();
181 let sorted = topological_sort(&ids, &deps).unwrap();
182 assert_eq!(sorted, vec!["only"]);
183 }
184}