vx_dependency/
resolver.rs

1//! High-level dependency resolver with caching and optimization
2
3use crate::{
4    graph::{DependencyGraph, ResolutionResult},
5    types::*,
6    Error, Result,
7};
8use std::collections::HashMap;
9use std::sync::Arc;
10use std::time::{Duration, Instant};
11use tokio::sync::RwLock;
12
13/// High-level dependency resolver
14pub struct DependencyResolver {
15    /// Dependency graph
16    graph: Arc<RwLock<DependencyGraph>>,
17    /// Tool registry
18    tool_registry: Arc<RwLock<HashMap<String, ToolSpec>>>,
19    /// Resolution cache
20    resolution_cache: Arc<RwLock<HashMap<String, CachedResolution>>>,
21    /// Availability checker
22    availability_checker: Option<Arc<dyn AvailabilityChecker>>,
23    /// Configuration
24    options: ResolutionOptions,
25}
26
27/// Cached resolution result
28#[derive(Debug, Clone)]
29struct CachedResolution {
30    /// Resolution result
31    result: ResolutionResult,
32    /// Cache timestamp
33    cached_at: Instant,
34    /// Cache TTL
35    ttl: Duration,
36}
37
38/// Options for dependency resolution
39#[derive(Debug, Clone)]
40pub struct ResolutionOptions {
41    /// Whether to include optional dependencies
42    pub include_optional: bool,
43    /// Whether to include development dependencies
44    pub include_dev: bool,
45    /// Maximum resolution depth (to prevent infinite recursion)
46    pub max_depth: usize,
47    /// Cache TTL for resolutions
48    pub cache_ttl: Duration,
49    /// Whether to enable parallel resolution
50    pub enable_parallel: bool,
51    /// Platform filter (only include dependencies for this platform)
52    pub platform_filter: Option<String>,
53    /// Whether to allow prerelease versions
54    pub allow_prerelease: bool,
55}
56
57/// Trait for checking tool availability
58#[async_trait::async_trait]
59pub trait AvailabilityChecker: Send + Sync {
60    /// Check if a tool is available
61    async fn is_available(&self, tool_name: &str) -> Result<bool>;
62
63    /// Get installed version of a tool
64    async fn get_version(&self, tool_name: &str) -> Result<Option<String>>;
65
66    /// Get tool installation path
67    async fn get_path(&self, tool_name: &str) -> Result<Option<String>>;
68}
69
70impl DependencyResolver {
71    /// Create a new dependency resolver
72    pub fn new() -> Self {
73        Self {
74            graph: Arc::new(RwLock::new(DependencyGraph::new())),
75            tool_registry: Arc::new(RwLock::new(HashMap::new())),
76            resolution_cache: Arc::new(RwLock::new(HashMap::new())),
77            availability_checker: None,
78            options: ResolutionOptions::default(),
79        }
80    }
81
82    /// Create a resolver with custom options
83    pub fn with_options(options: ResolutionOptions) -> Self {
84        Self {
85            options,
86            ..Self::new()
87        }
88    }
89
90    /// Set availability checker
91    pub fn with_availability_checker(mut self, checker: Arc<dyn AvailabilityChecker>) -> Self {
92        self.availability_checker = Some(checker);
93        self
94    }
95
96    /// Register a tool specification
97    pub async fn register_tool(&self, tool_spec: ToolSpec) -> Result<()> {
98        let tool_name = tool_spec.name.clone();
99
100        // Add to registry
101        {
102            let mut registry = self.tool_registry.write().await;
103            registry.insert(tool_name.clone(), tool_spec.clone());
104        }
105
106        // Add to graph
107        {
108            let mut graph = self.graph.write().await;
109            graph.add_tool(tool_spec)?;
110        }
111
112        // Update availability if checker is available
113        if let Some(checker) = &self.availability_checker {
114            let available = checker.is_available(&tool_name).await.unwrap_or(false);
115            let version = if available {
116                checker.get_version(&tool_name).await.unwrap_or(None)
117            } else {
118                None
119            };
120
121            let mut graph = self.graph.write().await;
122            graph.set_tool_available(&tool_name, available, version);
123        }
124
125        Ok(())
126    }
127
128    /// Register multiple tools
129    pub async fn register_tools(&self, tools: Vec<ToolSpec>) -> Result<()> {
130        for tool in tools {
131            self.register_tool(tool).await?;
132        }
133        Ok(())
134    }
135
136    /// Resolve dependencies for a tool
137    pub async fn resolve(&self, tool_name: &str) -> Result<ResolutionResult> {
138        // Check cache first
139        if let Some(cached) = self.get_cached_resolution(tool_name).await {
140            if cached.cached_at.elapsed() < cached.ttl {
141                return Ok(cached.result);
142            }
143        }
144
145        // Perform resolution
146        let result = self.resolve_uncached(tool_name).await?;
147
148        // Cache result
149        self.cache_resolution(tool_name, result.clone()).await;
150
151        Ok(result)
152    }
153
154    /// Resolve dependencies without caching
155    async fn resolve_uncached(&self, tool_name: &str) -> Result<ResolutionResult> {
156        // Ensure tool is registered
157        if !self.is_tool_registered(tool_name).await {
158            return Err(Error::ToolNotFound {
159                tool: tool_name.to_string(),
160            });
161        }
162
163        // Update availability information
164        self.update_availability().await?;
165
166        // Perform resolution using the graph
167        let mut graph = self.graph.write().await;
168        let mut result = graph.resolve_dependencies(tool_name)?;
169
170        // Filter based on options
171        self.filter_resolution(&mut result).await;
172
173        Ok(result)
174    }
175
176    /// Resolve dependencies for multiple tools
177    pub async fn resolve_multiple(&self, tool_names: &[String]) -> Result<ResolutionResult> {
178        let mut combined_result = ResolutionResult {
179            install_order: Vec::new(),
180            missing_tools: Vec::new(),
181            available_tools: Vec::new(),
182            circular_dependencies: Vec::new(),
183            version_conflicts: Vec::new(),
184        };
185
186        // Resolve each tool and combine results
187        for tool_name in tool_names {
188            let result = self.resolve(tool_name).await?;
189
190            // Merge install orders (maintaining dependency order)
191            for tool in result.install_order {
192                if !combined_result.install_order.contains(&tool) {
193                    combined_result.install_order.push(tool);
194                }
195            }
196
197            // Merge other fields
198            for tool in result.missing_tools {
199                if !combined_result.missing_tools.contains(&tool) {
200                    combined_result.missing_tools.push(tool);
201                }
202            }
203
204            for tool in result.available_tools {
205                if !combined_result.available_tools.contains(&tool) {
206                    combined_result.available_tools.push(tool);
207                }
208            }
209
210            combined_result
211                .circular_dependencies
212                .extend(result.circular_dependencies);
213            combined_result
214                .version_conflicts
215                .extend(result.version_conflicts);
216        }
217
218        // Re-sort install order to ensure proper dependency ordering
219        let final_order = {
220            let mut graph = self.graph.write().await;
221            graph.get_install_order(&combined_result.install_order)?
222        };
223        combined_result.install_order = final_order;
224
225        Ok(combined_result)
226    }
227
228    /// Check if a tool is registered
229    pub async fn is_tool_registered(&self, tool_name: &str) -> bool {
230        let registry = self.tool_registry.read().await;
231        registry.contains_key(tool_name)
232    }
233
234    /// Get tool specification
235    pub async fn get_tool_spec(&self, tool_name: &str) -> Option<ToolSpec> {
236        let registry = self.tool_registry.read().await;
237        registry.get(tool_name).cloned()
238    }
239
240    /// Get all registered tools
241    pub async fn get_all_tools(&self) -> Vec<String> {
242        let registry = self.tool_registry.read().await;
243        registry.keys().cloned().collect()
244    }
245
246    /// Clear resolution cache
247    pub async fn clear_cache(&self) {
248        let mut cache = self.resolution_cache.write().await;
249        cache.clear();
250    }
251
252    /// Get dependency graph statistics
253    pub async fn get_stats(&self) -> crate::graph::GraphStats {
254        let graph = self.graph.read().await;
255        graph.get_stats()
256    }
257
258    // Private helper methods
259
260    async fn get_cached_resolution(&self, tool_name: &str) -> Option<CachedResolution> {
261        let cache = self.resolution_cache.read().await;
262        cache.get(tool_name).cloned()
263    }
264
265    async fn cache_resolution(&self, tool_name: &str, result: ResolutionResult) {
266        let mut cache = self.resolution_cache.write().await;
267        cache.insert(
268            tool_name.to_string(),
269            CachedResolution {
270                result,
271                cached_at: Instant::now(),
272                ttl: self.options.cache_ttl,
273            },
274        );
275    }
276
277    async fn update_availability(&self) -> Result<()> {
278        if let Some(checker) = &self.availability_checker {
279            let tools = self.get_all_tools().await;
280            let mut graph = self.graph.write().await;
281
282            for tool_name in tools {
283                let available = checker.is_available(&tool_name).await.unwrap_or(false);
284                let version = if available {
285                    checker.get_version(&tool_name).await.unwrap_or(None)
286                } else {
287                    None
288                };
289
290                graph.set_tool_available(&tool_name, available, version);
291            }
292        }
293        Ok(())
294    }
295
296    async fn filter_resolution(&self, result: &mut ResolutionResult) {
297        // Apply platform filter
298        if let Some(platform) = &self.options.platform_filter {
299            let registry = self.tool_registry.read().await;
300
301            result.install_order.retain(|tool_name| {
302                if let Some(tool_spec) = registry.get(tool_name) {
303                    tool_spec
304                        .dependencies
305                        .iter()
306                        .all(|dep| dep.applies_to_platform(platform))
307                } else {
308                    true
309                }
310            });
311        }
312
313        // Filter by dependency types
314        if !self.options.include_optional || !self.options.include_dev {
315            // TODO: Implement dependency type filtering
316        }
317    }
318}
319
320impl Default for ResolutionOptions {
321    fn default() -> Self {
322        Self {
323            include_optional: false,
324            include_dev: false,
325            max_depth: 10,
326            cache_ttl: Duration::from_secs(300), // 5 minutes
327            enable_parallel: true,
328            platform_filter: None,
329            allow_prerelease: false,
330        }
331    }
332}
333
334impl Default for DependencyResolver {
335    fn default() -> Self {
336        Self::new()
337    }
338}
339
340#[cfg(test)]
341mod tests {
342    use super::*;
343
344    struct MockAvailabilityChecker {
345        available_tools: HashMap<String, (bool, Option<String>)>,
346    }
347
348    impl MockAvailabilityChecker {
349        fn new() -> Self {
350            let mut available_tools = HashMap::new();
351            available_tools.insert("node".to_string(), (true, Some("18.0.0".to_string())));
352            available_tools.insert("python".to_string(), (true, Some("3.9.0".to_string())));
353
354            Self { available_tools }
355        }
356    }
357
358    #[async_trait::async_trait]
359    impl AvailabilityChecker for MockAvailabilityChecker {
360        async fn is_available(&self, tool_name: &str) -> Result<bool> {
361            Ok(self
362                .available_tools
363                .get(tool_name)
364                .map(|(available, _)| *available)
365                .unwrap_or(false))
366        }
367
368        async fn get_version(&self, tool_name: &str) -> Result<Option<String>> {
369            Ok(self
370                .available_tools
371                .get(tool_name)
372                .and_then(|(_, version)| version.clone()))
373        }
374
375        async fn get_path(&self, _tool_name: &str) -> Result<Option<String>> {
376            Ok(None)
377        }
378    }
379
380    fn create_test_tool(name: &str, deps: Vec<&str>) -> ToolSpec {
381        ToolSpec {
382            name: name.to_string(),
383            dependencies: deps
384                .into_iter()
385                .map(|dep| DependencySpec::required(dep, format!("{} requires {}", name, dep)))
386                .collect(),
387            ..Default::default()
388        }
389    }
390
391    #[tokio::test]
392    async fn test_resolver_basic_functionality() {
393        let resolver = DependencyResolver::new()
394            .with_availability_checker(Arc::new(MockAvailabilityChecker::new()));
395
396        // Register tools
397        resolver
398            .register_tool(create_test_tool("node", vec![]))
399            .await
400            .unwrap();
401        resolver
402            .register_tool(create_test_tool("yarn", vec!["node"]))
403            .await
404            .unwrap();
405
406        // Resolve dependencies
407        let result = resolver.resolve("yarn").await.unwrap();
408
409        assert_eq!(result.install_order, vec!["node", "yarn"]);
410        assert_eq!(result.available_tools, vec!["node"]);
411        assert_eq!(result.missing_tools, vec!["yarn"]);
412    }
413
414    #[tokio::test]
415    async fn test_resolver_multiple_tools() {
416        let resolver = DependencyResolver::new()
417            .with_availability_checker(Arc::new(MockAvailabilityChecker::new()));
418
419        // Register tools
420        resolver
421            .register_tool(create_test_tool("node", vec![]))
422            .await
423            .unwrap();
424        resolver
425            .register_tool(create_test_tool("python", vec![]))
426            .await
427            .unwrap();
428        resolver
429            .register_tool(create_test_tool("yarn", vec!["node"]))
430            .await
431            .unwrap();
432        resolver
433            .register_tool(create_test_tool("pip", vec!["python"]))
434            .await
435            .unwrap();
436
437        // Resolve multiple tools
438        let result = resolver
439            .resolve_multiple(&["yarn".to_string(), "pip".to_string()])
440            .await
441            .unwrap();
442
443        // Should include all dependencies
444        assert!(result.install_order.contains(&"node".to_string()));
445        assert!(result.install_order.contains(&"python".to_string()));
446        assert!(result.install_order.contains(&"yarn".to_string()));
447        assert!(result.install_order.contains(&"pip".to_string()));
448    }
449
450    #[tokio::test]
451    async fn test_resolver_caching() {
452        let resolver = DependencyResolver::new()
453            .with_availability_checker(Arc::new(MockAvailabilityChecker::new()));
454
455        resolver
456            .register_tool(create_test_tool("node", vec![]))
457            .await
458            .unwrap();
459        resolver
460            .register_tool(create_test_tool("yarn", vec!["node"]))
461            .await
462            .unwrap();
463
464        // First resolution
465        let start = Instant::now();
466        let result1 = resolver.resolve("yarn").await.unwrap();
467        let first_duration = start.elapsed();
468
469        // Second resolution (should be cached)
470        let start = Instant::now();
471        let result2 = resolver.resolve("yarn").await.unwrap();
472        let second_duration = start.elapsed();
473
474        // Results should be identical
475        assert_eq!(result1.install_order, result2.install_order);
476
477        // Second call should be faster (cached)
478        assert!(second_duration < first_duration);
479    }
480
481    #[tokio::test]
482    async fn test_resolver_unregistered_tool() {
483        let resolver = DependencyResolver::new();
484
485        let result = resolver.resolve("nonexistent").await;
486        assert!(result.is_err());
487
488        if let Err(Error::ToolNotFound { tool }) = result {
489            assert_eq!(tool, "nonexistent");
490        } else {
491            panic!("Expected ToolNotFound error");
492        }
493    }
494}