1use crate::Result;
2use crate::pitchfork_toml::PitchforkTomlDaemon;
3use indexmap::IndexMap;
4use miette::bail;
5use std::collections::{HashMap, HashSet, VecDeque};
6
7#[derive(Debug)]
9pub struct DependencyOrder {
10 pub levels: Vec<Vec<String>>,
13}
14
15pub fn resolve_dependencies(
24 requested: &[String],
25 all_daemons: &IndexMap<String, PitchforkTomlDaemon>,
26) -> Result<DependencyOrder> {
27 let mut to_start: HashSet<String> = HashSet::new();
29 let mut queue: VecDeque<String> = requested.iter().cloned().collect();
30
31 while let Some(id) = queue.pop_front() {
32 if to_start.contains(&id) {
33 continue;
34 }
35
36 let daemon = all_daemons
37 .get(&id)
38 .ok_or_else(|| miette::miette!("Daemon '{}' not found in configuration", id))?;
39
40 to_start.insert(id.clone());
41
42 for dep in &daemon.depends {
44 if !all_daemons.contains_key(dep) {
45 bail!(
46 "Daemon '{}' depends on '{}' which is not defined in configuration",
47 id,
48 dep
49 );
50 }
51 if !to_start.contains(dep) {
52 queue.push_back(dep.clone());
53 }
54 }
55 }
56
57 let mut in_degree: HashMap<String, usize> = HashMap::new();
59 let mut dependents: HashMap<String, Vec<String>> = HashMap::new();
60
61 for id in &to_start {
62 in_degree.entry(id.clone()).or_insert(0);
63 dependents.entry(id.clone()).or_default();
64 }
65
66 for id in &to_start {
67 let daemon = all_daemons.get(id).ok_or_else(|| {
68 miette::miette!("Internal error: daemon '{}' missing from configuration", id)
69 })?;
70 for dep in &daemon.depends {
71 if to_start.contains(dep) {
72 *in_degree.get_mut(id).ok_or_else(|| {
73 miette::miette!("Internal error: in_degree missing for daemon '{}'", id)
74 })? += 1;
75 dependents
76 .get_mut(dep)
77 .ok_or_else(|| {
78 miette::miette!("Internal error: dependents missing for daemon '{}'", dep)
79 })?
80 .push(id.clone());
81 }
82 }
83 }
84
85 let mut processed: HashSet<String> = HashSet::new();
87 let mut levels: Vec<Vec<String>> = Vec::new();
88 let mut current_level: Vec<String> = in_degree
89 .iter()
90 .filter(|(_, deg)| **deg == 0)
91 .map(|(id, _)| id.clone())
92 .collect();
93
94 current_level.sort();
96
97 while !current_level.is_empty() {
98 let mut next_level = Vec::new();
99
100 for id in ¤t_level {
101 processed.insert(id.clone());
102
103 let deps = dependents.get(id).ok_or_else(|| {
104 miette::miette!("Internal error: dependents missing for daemon '{}'", id)
105 })?;
106 for dependent in deps {
107 let deg = in_degree.get_mut(dependent).ok_or_else(|| {
108 miette::miette!(
109 "Internal error: in_degree missing for daemon '{}'",
110 dependent
111 )
112 })?;
113 *deg -= 1;
114 if *deg == 0 {
115 next_level.push(dependent.clone());
116 }
117 }
118 }
119
120 levels.push(current_level);
121 next_level.sort(); current_level = next_level;
123 }
124
125 if processed.len() != to_start.len() {
127 let remaining: Vec<_> = to_start.difference(&processed).cloned().collect::<Vec<_>>();
128 bail!("Circular dependency detected involving: {:?}", remaining);
129 }
130
131 Ok(DependencyOrder { levels })
132}
133
134#[cfg(test)]
135mod tests {
136 use super::*;
137 use crate::pitchfork_toml::PitchforkTomlDaemon;
138 use indexmap::IndexMap;
139
140 fn make_daemon(depends: Vec<&str>) -> PitchforkTomlDaemon {
141 PitchforkTomlDaemon {
142 run: "echo test".to_string(),
143 auto: vec![],
144 cron: None,
145 retry: 0,
146 ready_delay: None,
147 ready_output: None,
148 ready_http: None,
149 ready_port: None,
150 boot_start: None,
151 depends: depends.into_iter().map(String::from).collect(),
152 path: None,
153 }
154 }
155
156 #[test]
157 fn test_no_dependencies() {
158 let mut daemons = IndexMap::new();
159 daemons.insert("api".to_string(), make_daemon(vec![]));
160
161 let result = resolve_dependencies(&["api".to_string()], &daemons).unwrap();
162
163 assert_eq!(result.levels.len(), 1);
164 assert_eq!(result.levels[0], vec!["api"]);
165 }
166
167 #[test]
168 fn test_simple_dependency() {
169 let mut daemons = IndexMap::new();
170 daemons.insert("postgres".to_string(), make_daemon(vec![]));
171 daemons.insert("api".to_string(), make_daemon(vec!["postgres"]));
172
173 let result = resolve_dependencies(&["api".to_string()], &daemons).unwrap();
174
175 assert_eq!(result.levels.len(), 2);
176 assert_eq!(result.levels[0], vec!["postgres"]);
177 assert_eq!(result.levels[1], vec!["api"]);
178 }
179
180 #[test]
181 fn test_multiple_dependencies() {
182 let mut daemons = IndexMap::new();
183 daemons.insert("postgres".to_string(), make_daemon(vec![]));
184 daemons.insert("redis".to_string(), make_daemon(vec![]));
185 daemons.insert("api".to_string(), make_daemon(vec!["postgres", "redis"]));
186
187 let result = resolve_dependencies(&["api".to_string()], &daemons).unwrap();
188
189 assert_eq!(result.levels.len(), 2);
190 assert!(result.levels[0].contains(&"postgres".to_string()));
192 assert!(result.levels[0].contains(&"redis".to_string()));
193 assert_eq!(result.levels[1], vec!["api"]);
194 }
195
196 #[test]
197 fn test_transitive_dependencies() {
198 let mut daemons = IndexMap::new();
199 daemons.insert("database".to_string(), make_daemon(vec![]));
200 daemons.insert("backend".to_string(), make_daemon(vec!["database"]));
201 daemons.insert("api".to_string(), make_daemon(vec!["backend"]));
202
203 let result = resolve_dependencies(&["api".to_string()], &daemons).unwrap();
204
205 assert_eq!(result.levels.len(), 3);
206 assert_eq!(result.levels[0], vec!["database"]);
207 assert_eq!(result.levels[1], vec!["backend"]);
208 assert_eq!(result.levels[2], vec!["api"]);
209 }
210
211 #[test]
212 fn test_diamond_dependency() {
213 let mut daemons = IndexMap::new();
214 daemons.insert("db".to_string(), make_daemon(vec![]));
215 daemons.insert("auth".to_string(), make_daemon(vec!["db"]));
216 daemons.insert("data".to_string(), make_daemon(vec!["db"]));
217 daemons.insert("api".to_string(), make_daemon(vec!["auth", "data"]));
218
219 let result = resolve_dependencies(&["api".to_string()], &daemons).unwrap();
220
221 assert_eq!(result.levels.len(), 3);
222 assert_eq!(result.levels[0], vec!["db"]);
223 assert!(result.levels[1].contains(&"auth".to_string()));
225 assert!(result.levels[1].contains(&"data".to_string()));
226 assert_eq!(result.levels[2], vec!["api"]);
227 }
228
229 #[test]
230 fn test_circular_dependency_detected() {
231 let mut daemons = IndexMap::new();
232 daemons.insert("a".to_string(), make_daemon(vec!["c"]));
233 daemons.insert("b".to_string(), make_daemon(vec!["a"]));
234 daemons.insert("c".to_string(), make_daemon(vec!["b"]));
235
236 let result = resolve_dependencies(&["a".to_string()], &daemons);
237
238 assert!(result.is_err());
239 let err = result.unwrap_err().to_string();
240 assert!(err.contains("Circular dependency"));
241 }
242
243 #[test]
244 fn test_missing_dependency_error() {
245 let mut daemons = IndexMap::new();
246 daemons.insert("api".to_string(), make_daemon(vec!["nonexistent"]));
247
248 let result = resolve_dependencies(&["api".to_string()], &daemons);
249
250 assert!(result.is_err());
251 let err = result.unwrap_err().to_string();
252 assert!(err.contains("nonexistent"));
253 assert!(err.contains("not defined"));
254 }
255
256 #[test]
257 fn test_missing_requested_daemon_error() {
258 let daemons = IndexMap::new();
259
260 let result = resolve_dependencies(&["nonexistent".to_string()], &daemons);
261
262 assert!(result.is_err());
263 let err = result.unwrap_err().to_string();
264 assert!(err.contains("nonexistent"));
265 assert!(err.contains("not found"));
266 }
267
268 #[test]
269 fn test_multiple_requested_daemons() {
270 let mut daemons = IndexMap::new();
271 daemons.insert("db".to_string(), make_daemon(vec![]));
272 daemons.insert("api".to_string(), make_daemon(vec!["db"]));
273 daemons.insert("worker".to_string(), make_daemon(vec!["db"]));
274
275 let result =
276 resolve_dependencies(&["api".to_string(), "worker".to_string()], &daemons).unwrap();
277
278 assert_eq!(result.levels.len(), 2);
279 assert_eq!(result.levels[0], vec!["db"]);
280 assert!(result.levels[1].contains(&"api".to_string()));
282 assert!(result.levels[1].contains(&"worker".to_string()));
283 }
284
285 #[test]
286 fn test_start_all_with_dependencies() {
287 let mut daemons = IndexMap::new();
288 daemons.insert("db".to_string(), make_daemon(vec![]));
289 daemons.insert("cache".to_string(), make_daemon(vec![]));
290 daemons.insert("api".to_string(), make_daemon(vec!["db", "cache"]));
291 daemons.insert("worker".to_string(), make_daemon(vec!["db"]));
292
293 let all_ids: Vec<String> = daemons.keys().cloned().collect();
294 let result = resolve_dependencies(&all_ids, &daemons).unwrap();
295
296 assert_eq!(result.levels.len(), 2);
297 assert!(result.levels[0].contains(&"db".to_string()));
299 assert!(result.levels[0].contains(&"cache".to_string()));
300 assert!(result.levels[1].contains(&"api".to_string()));
302 assert!(result.levels[1].contains(&"worker".to_string()));
303 }
304}