1use crate::{types::*, Result};
4use std::collections::{HashMap, HashSet};
5
6#[derive(Debug, Clone)]
8pub struct DependencyGraph {
9 nodes: HashMap<String, DependencyNode>,
11 edges: HashMap<String, Vec<String>>,
13 reverse_edges: HashMap<String, Vec<String>>,
15}
16
17#[derive(Debug, Clone)]
19pub struct DependencyNode {
20 pub tool_spec: ToolSpec,
22 pub available: bool,
24 pub installed_version: Option<String>,
26 pub state: NodeState,
28}
29
30#[derive(Debug, Clone, PartialEq)]
32pub enum NodeState {
33 Unvisited,
35 Visiting,
37 Visited,
39 Pending,
41 Installing,
43 Installed,
45 Failed(String),
47}
48
49#[derive(Debug, Clone)]
51pub struct ResolutionResult {
52 pub install_order: Vec<String>,
54 pub missing_tools: Vec<String>,
56 pub available_tools: Vec<String>,
58 pub circular_dependencies: Vec<Vec<String>>,
60 pub version_conflicts: Vec<VersionConflict>,
62}
63
64#[derive(Debug, Clone)]
66pub struct VersionConflict {
67 pub tool: String,
69 pub required: String,
71 pub installed: String,
73 pub required_by: Vec<String>,
75}
76
77impl DependencyGraph {
78 pub fn new() -> Self {
80 Self {
81 nodes: HashMap::new(),
82 edges: HashMap::new(),
83 reverse_edges: HashMap::new(),
84 }
85 }
86
87 pub fn add_tool(&mut self, tool_spec: ToolSpec) -> Result<()> {
89 let tool_name = tool_spec.name.clone();
90
91 self.nodes.insert(
93 tool_name.clone(),
94 DependencyNode {
95 tool_spec: tool_spec.clone(),
96 available: false,
97 installed_version: None,
98 state: NodeState::Unvisited,
99 },
100 );
101
102 let mut dependencies = Vec::new();
104 for dep in &tool_spec.dependencies {
105 if !dep.optional || dep.dependency_type == DependencyType::Runtime {
106 dependencies.push(dep.tool_name.clone());
107 }
108 }
109
110 self.edges.insert(tool_name.clone(), dependencies.clone());
111
112 for dep in dependencies {
114 self.reverse_edges
115 .entry(dep)
116 .or_default()
117 .push(tool_name.clone());
118 }
119
120 Ok(())
121 }
122
123 pub fn get_tool(&self, tool_name: &str) -> Option<&DependencyNode> {
125 self.nodes.get(tool_name)
126 }
127
128 pub fn get_tool_mut(&mut self, tool_name: &str) -> Option<&mut DependencyNode> {
130 self.nodes.get_mut(tool_name)
131 }
132
133 pub fn set_tool_available(
135 &mut self,
136 tool_name: &str,
137 available: bool,
138 version: Option<String>,
139 ) {
140 if let Some(node) = self.nodes.get_mut(tool_name) {
141 node.available = available;
142 node.installed_version = version;
143 if available {
144 node.state = NodeState::Installed;
145 }
146 }
147 }
148
149 pub fn resolve_dependencies(&mut self, tool_name: &str) -> Result<ResolutionResult> {
151 for node in self.nodes.values_mut() {
153 node.state = NodeState::Unvisited;
154 }
155
156 let mut install_order = Vec::new();
157 let mut circular_dependencies = Vec::new();
158 let mut version_conflicts = Vec::new();
159
160 self.topological_sort_dfs(tool_name, &mut install_order, &mut circular_dependencies)?;
162
163 self.check_version_conflicts(&mut version_conflicts)?;
165
166 let mut missing_tools = Vec::new();
168 let mut available_tools = Vec::new();
169
170 for tool in &install_order {
171 if let Some(node) = self.nodes.get(tool) {
172 if node.available {
173 available_tools.push(tool.clone());
174 } else {
175 missing_tools.push(tool.clone());
176 }
177 }
178 }
179
180 Ok(ResolutionResult {
181 install_order,
182 missing_tools,
183 available_tools,
184 circular_dependencies,
185 version_conflicts,
186 })
187 }
188
189 fn topological_sort_dfs(
191 &mut self,
192 tool_name: &str,
193 install_order: &mut Vec<String>,
194 circular_dependencies: &mut Vec<Vec<String>>,
195 ) -> Result<()> {
196 let node_state = self
197 .nodes
198 .get(tool_name)
199 .map(|n| n.state.clone())
200 .unwrap_or(NodeState::Unvisited);
201
202 match node_state {
203 NodeState::Visiting => {
204 circular_dependencies.push(vec![tool_name.to_string()]);
206 return Ok(());
207 }
208 NodeState::Visited => {
209 return Ok(());
211 }
212 _ => {}
213 }
214
215 if let Some(node) = self.nodes.get_mut(tool_name) {
217 node.state = NodeState::Visiting;
218 }
219
220 if let Some(dependencies) = self.edges.get(tool_name).cloned() {
222 for dep in dependencies {
223 self.topological_sort_dfs(&dep, install_order, circular_dependencies)?;
224 }
225 }
226
227 if let Some(node) = self.nodes.get_mut(tool_name) {
229 node.state = NodeState::Visited;
230 }
231
232 if !install_order.contains(&tool_name.to_string()) {
233 install_order.push(tool_name.to_string());
234 }
235
236 Ok(())
237 }
238
239 fn check_version_conflicts(&self, _conflicts: &mut Vec<VersionConflict>) -> Result<()> {
241 Ok(())
244 }
245
246 pub fn get_dependents(&self, tool_name: &str) -> Vec<String> {
248 self.reverse_edges
249 .get(tool_name)
250 .cloned()
251 .unwrap_or_default()
252 }
253
254 pub fn get_dependencies(&self, tool_name: &str) -> Vec<String> {
256 self.edges.get(tool_name).cloned().unwrap_or_default()
257 }
258
259 pub fn has_cycles(&mut self) -> bool {
261 for node in self.nodes.values_mut() {
263 node.state = NodeState::Unvisited;
264 }
265
266 for tool_name in self.nodes.keys().cloned().collect::<Vec<_>>() {
267 if self.nodes.get(&tool_name).unwrap().state == NodeState::Unvisited
268 && self.has_cycle_dfs(&tool_name)
269 {
270 return true;
271 }
272 }
273
274 false
275 }
276
277 fn has_cycle_dfs(&mut self, tool_name: &str) -> bool {
279 if let Some(node) = self.nodes.get_mut(tool_name) {
280 if node.state == NodeState::Visiting {
281 return true; }
283 if node.state == NodeState::Visited {
284 return false; }
286
287 node.state = NodeState::Visiting;
288 }
289
290 if let Some(dependencies) = self.edges.get(tool_name).cloned() {
292 for dep in dependencies {
293 if self.has_cycle_dfs(&dep) {
294 return true;
295 }
296 }
297 }
298
299 if let Some(node) = self.nodes.get_mut(tool_name) {
301 node.state = NodeState::Visited;
302 }
303
304 false
305 }
306
307 pub fn get_install_order(&mut self, tools: &[String]) -> Result<Vec<String>> {
309 let mut all_tools = HashSet::new();
310
311 for tool in tools {
313 let resolution = self.resolve_dependencies(tool)?;
314 all_tools.extend(resolution.install_order);
315 }
316
317 let ordered_tools: Vec<String> = all_tools.into_iter().collect();
319
320 let mut final_order = Vec::new();
322 for tool in &ordered_tools {
323 let resolution = self.resolve_dependencies(tool)?;
324 for dep_tool in resolution.install_order {
325 if !final_order.contains(&dep_tool) {
326 final_order.push(dep_tool);
327 }
328 }
329 }
330
331 Ok(final_order)
332 }
333
334 pub fn get_stats(&self) -> GraphStats {
336 let total_tools = self.nodes.len();
337 let total_dependencies = self.edges.values().map(|deps| deps.len()).sum();
338 let available_tools = self.nodes.values().filter(|n| n.available).count();
339 let missing_tools = total_tools - available_tools;
340
341 GraphStats {
342 total_tools,
343 total_dependencies,
344 available_tools,
345 missing_tools,
346 }
347 }
348}
349
350#[derive(Debug, Clone)]
352pub struct GraphStats {
353 pub total_tools: usize,
355 pub total_dependencies: usize,
357 pub available_tools: usize,
359 pub missing_tools: usize,
361}
362
363impl Default for DependencyGraph {
364 fn default() -> Self {
365 Self::new()
366 }
367}
368
369#[cfg(test)]
370mod tests {
371 use super::*;
372
373 fn create_test_tool(name: &str, deps: Vec<&str>) -> ToolSpec {
374 ToolSpec {
375 name: name.to_string(),
376 dependencies: deps
377 .into_iter()
378 .map(|dep| DependencySpec::required(dep, format!("{} requires {}", name, dep)))
379 .collect(),
380 ..Default::default()
381 }
382 }
383
384 #[test]
385 fn test_simple_dependency_resolution() {
386 let mut graph = DependencyGraph::new();
387
388 graph.add_tool(create_test_tool("node", vec![])).unwrap();
390
391 graph
393 .add_tool(create_test_tool("yarn", vec!["node"]))
394 .unwrap();
395
396 let resolution = graph.resolve_dependencies("yarn").unwrap();
397 assert_eq!(resolution.install_order, vec!["node", "yarn"]);
398 }
399
400 #[test]
401 fn test_multi_layer_dependencies() {
402 let mut graph = DependencyGraph::new();
403
404 graph.add_tool(create_test_tool("node", vec![])).unwrap();
406 graph.add_tool(create_test_tool("python", vec![])).unwrap();
407
408 graph
410 .add_tool(create_test_tool("npm", vec!["node"]))
411 .unwrap();
412 graph
413 .add_tool(create_test_tool("pip", vec!["python"]))
414 .unwrap();
415
416 graph
418 .add_tool(create_test_tool("yarn", vec!["node", "npm"]))
419 .unwrap();
420
421 let resolution = graph.resolve_dependencies("yarn").unwrap();
422
423 let order = resolution.install_order;
425 let node_pos = order.iter().position(|x| x == "node").unwrap();
426 let npm_pos = order.iter().position(|x| x == "npm").unwrap();
427 let yarn_pos = order.iter().position(|x| x == "yarn").unwrap();
428
429 assert!(node_pos < npm_pos); assert!(npm_pos < yarn_pos); assert!(node_pos < yarn_pos); }
433
434 #[test]
435 fn test_circular_dependency_detection() {
436 let mut graph = DependencyGraph::new();
437
438 graph.add_tool(create_test_tool("a", vec!["b"])).unwrap();
440 graph.add_tool(create_test_tool("b", vec!["c"])).unwrap();
441 graph.add_tool(create_test_tool("c", vec!["a"])).unwrap();
442
443 assert!(graph.has_cycles());
444
445 let resolution = graph.resolve_dependencies("a").unwrap();
446 assert!(!resolution.circular_dependencies.is_empty());
447 }
448
449 #[test]
450 fn test_diamond_dependency() {
451 let mut graph = DependencyGraph::new();
452
453 graph.add_tool(create_test_tool("a", vec![])).unwrap();
455 graph.add_tool(create_test_tool("b", vec!["a"])).unwrap();
456 graph.add_tool(create_test_tool("c", vec!["a"])).unwrap();
457 graph
458 .add_tool(create_test_tool("d", vec!["b", "c"]))
459 .unwrap();
460
461 let resolution = graph.resolve_dependencies("d").unwrap();
462
463 let order = resolution.install_order;
465 let a_count = order.iter().filter(|&x| x == "a").count();
466 assert_eq!(a_count, 1);
467
468 let a_pos = order.iter().position(|x| x == "a").unwrap();
469 let b_pos = order.iter().position(|x| x == "b").unwrap();
470 let c_pos = order.iter().position(|x| x == "c").unwrap();
471 let d_pos = order.iter().position(|x| x == "d").unwrap();
472
473 assert!(a_pos < b_pos);
474 assert!(a_pos < c_pos);
475 assert!(b_pos < d_pos);
476 assert!(c_pos < d_pos);
477 }
478
479 #[test]
480 fn test_tool_availability() {
481 let mut graph = DependencyGraph::new();
482
483 graph.add_tool(create_test_tool("node", vec![])).unwrap();
484 graph
485 .add_tool(create_test_tool("yarn", vec!["node"]))
486 .unwrap();
487
488 graph.set_tool_available("node", true, Some("18.0.0".to_string()));
490
491 let resolution = graph.resolve_dependencies("yarn").unwrap();
492 assert_eq!(resolution.available_tools, vec!["node"]);
493 assert_eq!(resolution.missing_tools, vec!["yarn"]);
494 }
495
496 #[test]
497 fn test_graph_stats() {
498 let mut graph = DependencyGraph::new();
499
500 graph.add_tool(create_test_tool("node", vec![])).unwrap();
501 graph
502 .add_tool(create_test_tool("yarn", vec!["node"]))
503 .unwrap();
504 graph.set_tool_available("node", true, Some("18.0.0".to_string()));
505
506 let stats = graph.get_stats();
507 assert_eq!(stats.total_tools, 2);
508 assert_eq!(stats.available_tools, 1);
509 assert_eq!(stats.missing_tools, 1);
510 assert_eq!(stats.total_dependencies, 1); }
512}