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