vx_dependency/
graph.rs

1//! Dependency graph representation and algorithms
2
3use crate::{types::*, Result};
4use std::collections::{HashMap, HashSet};
5
6/// Dependency graph for tools
7#[derive(Debug, Clone)]
8pub struct DependencyGraph {
9    /// Nodes in the graph (tools)
10    nodes: HashMap<String, DependencyNode>,
11    /// Adjacency list (tool -> dependencies)
12    edges: HashMap<String, Vec<String>>,
13    /// Reverse adjacency list (tool -> dependents)
14    reverse_edges: HashMap<String, Vec<String>>,
15}
16
17/// Node in the dependency graph
18#[derive(Debug, Clone)]
19pub struct DependencyNode {
20    /// Tool specification
21    pub tool_spec: ToolSpec,
22    /// Whether this tool is currently available
23    pub available: bool,
24    /// Installed version (if available)
25    pub installed_version: Option<String>,
26    /// Installation state
27    pub state: NodeState,
28}
29
30/// State of a dependency node
31#[derive(Debug, Clone, PartialEq)]
32pub enum NodeState {
33    /// Not processed yet
34    Unvisited,
35    /// Currently being processed (for cycle detection)
36    Visiting,
37    /// Processing completed
38    Visited,
39    /// Installation pending
40    Pending,
41    /// Currently installing
42    Installing,
43    /// Installation completed
44    Installed,
45    /// Installation failed
46    Failed(String),
47}
48
49/// Result of dependency resolution
50#[derive(Debug, Clone)]
51pub struct ResolutionResult {
52    /// Installation order (dependencies first)
53    pub install_order: Vec<String>,
54    /// Tools that need to be installed
55    pub missing_tools: Vec<String>,
56    /// Tools that are already available
57    pub available_tools: Vec<String>,
58    /// Circular dependencies detected
59    pub circular_dependencies: Vec<Vec<String>>,
60    /// Version conflicts
61    pub version_conflicts: Vec<VersionConflict>,
62}
63
64/// Version conflict information
65#[derive(Debug, Clone)]
66pub struct VersionConflict {
67    /// Tool with version conflict
68    pub tool: String,
69    /// Required version constraint
70    pub required: String,
71    /// Currently installed version
72    pub installed: String,
73    /// Tools that require this version
74    pub required_by: Vec<String>,
75}
76
77impl DependencyGraph {
78    /// Create a new empty dependency graph
79    pub fn new() -> Self {
80        Self {
81            nodes: HashMap::new(),
82            edges: HashMap::new(),
83            reverse_edges: HashMap::new(),
84        }
85    }
86
87    /// Add a tool to the graph
88    pub fn add_tool(&mut self, tool_spec: ToolSpec) -> Result<()> {
89        let tool_name = tool_spec.name.clone();
90
91        // Add node
92        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        // Add edges for dependencies
103        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        // Update reverse edges
113        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    /// Get a tool node
124    pub fn get_tool(&self, tool_name: &str) -> Option<&DependencyNode> {
125        self.nodes.get(tool_name)
126    }
127
128    /// Get a mutable tool node
129    pub fn get_tool_mut(&mut self, tool_name: &str) -> Option<&mut DependencyNode> {
130        self.nodes.get_mut(tool_name)
131    }
132
133    /// Update tool availability
134    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    /// Resolve dependencies for a tool
150    pub fn resolve_dependencies(&mut self, tool_name: &str) -> Result<ResolutionResult> {
151        // Reset all node states
152        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        // Perform topological sort with cycle detection
161        self.topological_sort_dfs(tool_name, &mut install_order, &mut circular_dependencies)?;
162
163        // Check version conflicts
164        self.check_version_conflicts(&mut version_conflicts)?;
165
166        // Categorize tools
167        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    /// Perform topological sort using DFS with cycle detection
190    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                // Cycle detected
205                circular_dependencies.push(vec![tool_name.to_string()]);
206                return Ok(());
207            }
208            NodeState::Visited => {
209                // Already processed
210                return Ok(());
211            }
212            _ => {}
213        }
214
215        // Mark as visiting
216        if let Some(node) = self.nodes.get_mut(tool_name) {
217            node.state = NodeState::Visiting;
218        }
219
220        // Visit dependencies first
221        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        // Mark as visited and add to install order
228        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    /// Check for version conflicts
240    fn check_version_conflicts(&self, _conflicts: &mut Vec<VersionConflict>) -> Result<()> {
241        // TODO: Implement version conflict detection
242        // This would check if multiple tools require different versions of the same dependency
243        Ok(())
244    }
245
246    /// Get all tools that depend on a given tool
247    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    /// Get direct dependencies of a tool
255    pub fn get_dependencies(&self, tool_name: &str) -> Vec<String> {
256        self.edges.get(tool_name).cloned().unwrap_or_default()
257    }
258
259    /// Check if the graph has cycles
260    pub fn has_cycles(&mut self) -> bool {
261        // Reset states
262        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    /// DFS cycle detection helper
278    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; // Back edge found - cycle detected
282            }
283            if node.state == NodeState::Visited {
284                return false; // Already processed
285            }
286
287            node.state = NodeState::Visiting;
288        }
289
290        // Check dependencies
291        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        // Mark as visited
300        if let Some(node) = self.nodes.get_mut(tool_name) {
301            node.state = NodeState::Visited;
302        }
303
304        false
305    }
306
307    /// Get tools in installation order (dependencies first)
308    pub fn get_install_order(&mut self, tools: &[String]) -> Result<Vec<String>> {
309        let mut all_tools = HashSet::new();
310
311        // Collect all tools and their dependencies
312        for tool in tools {
313            let resolution = self.resolve_dependencies(tool)?;
314            all_tools.extend(resolution.install_order);
315        }
316
317        // Convert to sorted vector
318        let ordered_tools: Vec<String> = all_tools.into_iter().collect();
319
320        // Sort by dependency order using topological sort
321        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    /// Get statistics about the dependency graph
335    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/// Statistics about the dependency graph
351#[derive(Debug, Clone)]
352pub struct GraphStats {
353    /// Total number of tools in the graph
354    pub total_tools: usize,
355    /// Total number of dependency relationships
356    pub total_dependencies: usize,
357    /// Number of available tools
358    pub available_tools: usize,
359    /// Number of missing tools
360    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        // node (no dependencies)
389        graph.add_tool(create_test_tool("node", vec![])).unwrap();
390
391        // yarn depends on node
392        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        // Layer 1: base tools
405        graph.add_tool(create_test_tool("node", vec![])).unwrap();
406        graph.add_tool(create_test_tool("python", vec![])).unwrap();
407
408        // Layer 2: tools that depend on base tools
409        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        // Layer 3: tools that depend on layer 2
417        graph
418            .add_tool(create_test_tool("yarn", vec!["node", "npm"]))
419            .unwrap();
420
421        let resolution = graph.resolve_dependencies("yarn").unwrap();
422
423        // Should install in dependency order
424        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); // node before npm
430        assert!(npm_pos < yarn_pos); // npm before yarn
431        assert!(node_pos < yarn_pos); // node before yarn
432    }
433
434    #[test]
435    fn test_circular_dependency_detection() {
436        let mut graph = DependencyGraph::new();
437
438        // Create circular dependency: a -> b -> c -> a
439        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        // Diamond dependency: d -> b,c -> a
454        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        // 'a' should appear only once and before 'b' and 'c'
464        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        // Mark node as available
489        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); // yarn -> node
511    }
512}