use crate::{
graph::{DependencyGraph, ResolutionResult},
types::*,
Error, Result,
};
use std::collections::HashMap;
use std::sync::Arc;
use std::time::{Duration, Instant};
use tokio::sync::RwLock;
pub struct DependencyResolver {
graph: Arc<RwLock<DependencyGraph>>,
tool_registry: Arc<RwLock<HashMap<String, ToolSpec>>>,
resolution_cache: Arc<RwLock<HashMap<String, CachedResolution>>>,
availability_checker: Option<Arc<dyn AvailabilityChecker>>,
options: ResolutionOptions,
}
#[derive(Debug, Clone)]
struct CachedResolution {
result: ResolutionResult,
cached_at: Instant,
ttl: Duration,
}
#[derive(Debug, Clone)]
pub struct ResolutionOptions {
pub include_optional: bool,
pub include_dev: bool,
pub max_depth: usize,
pub cache_ttl: Duration,
pub enable_parallel: bool,
pub platform_filter: Option<String>,
pub allow_prerelease: bool,
}
#[async_trait::async_trait]
pub trait AvailabilityChecker: Send + Sync {
async fn is_available(&self, tool_name: &str) -> Result<bool>;
async fn get_version(&self, tool_name: &str) -> Result<Option<String>>;
async fn get_path(&self, tool_name: &str) -> Result<Option<String>>;
}
impl DependencyResolver {
pub fn new() -> Self {
Self {
graph: Arc::new(RwLock::new(DependencyGraph::new())),
tool_registry: Arc::new(RwLock::new(HashMap::new())),
resolution_cache: Arc::new(RwLock::new(HashMap::new())),
availability_checker: None,
options: ResolutionOptions::default(),
}
}
pub fn with_options(options: ResolutionOptions) -> Self {
Self {
options,
..Self::new()
}
}
pub fn with_availability_checker(mut self, checker: Arc<dyn AvailabilityChecker>) -> Self {
self.availability_checker = Some(checker);
self
}
pub async fn register_tool(&self, tool_spec: ToolSpec) -> Result<()> {
let tool_name = tool_spec.name.clone();
{
let mut registry = self.tool_registry.write().await;
registry.insert(tool_name.clone(), tool_spec.clone());
}
{
let mut graph = self.graph.write().await;
graph.add_tool(tool_spec)?;
}
if let Some(checker) = &self.availability_checker {
let available = checker.is_available(&tool_name).await.unwrap_or(false);
let version = if available {
checker.get_version(&tool_name).await.unwrap_or(None)
} else {
None
};
let mut graph = self.graph.write().await;
graph.set_tool_available(&tool_name, available, version);
}
Ok(())
}
pub async fn register_tools(&self, tools: Vec<ToolSpec>) -> Result<()> {
for tool in tools {
self.register_tool(tool).await?;
}
Ok(())
}
pub async fn resolve(&self, tool_name: &str) -> Result<ResolutionResult> {
if let Some(cached) = self.get_cached_resolution(tool_name).await {
if cached.cached_at.elapsed() < cached.ttl {
return Ok(cached.result);
}
}
let result = self.resolve_uncached(tool_name).await?;
self.cache_resolution(tool_name, result.clone()).await;
Ok(result)
}
async fn resolve_uncached(&self, tool_name: &str) -> Result<ResolutionResult> {
if !self.is_tool_registered(tool_name).await {
return Err(Error::ToolNotFound {
tool: tool_name.to_string(),
});
}
self.update_availability().await?;
let mut graph = self.graph.write().await;
let mut result = graph.resolve_dependencies(tool_name)?;
self.filter_resolution(&mut result).await;
Ok(result)
}
pub async fn resolve_multiple(&self, tool_names: &[String]) -> Result<ResolutionResult> {
let mut combined_result = ResolutionResult {
install_order: Vec::new(),
missing_tools: Vec::new(),
available_tools: Vec::new(),
circular_dependencies: Vec::new(),
version_conflicts: Vec::new(),
};
for tool_name in tool_names {
let result = self.resolve(tool_name).await?;
for tool in result.install_order {
if !combined_result.install_order.contains(&tool) {
combined_result.install_order.push(tool);
}
}
for tool in result.missing_tools {
if !combined_result.missing_tools.contains(&tool) {
combined_result.missing_tools.push(tool);
}
}
for tool in result.available_tools {
if !combined_result.available_tools.contains(&tool) {
combined_result.available_tools.push(tool);
}
}
combined_result
.circular_dependencies
.extend(result.circular_dependencies);
combined_result
.version_conflicts
.extend(result.version_conflicts);
}
let final_order = {
let mut graph = self.graph.write().await;
graph.get_install_order(&combined_result.install_order)?
};
combined_result.install_order = final_order;
Ok(combined_result)
}
pub async fn is_tool_registered(&self, tool_name: &str) -> bool {
let registry = self.tool_registry.read().await;
registry.contains_key(tool_name)
}
pub async fn get_tool_spec(&self, tool_name: &str) -> Option<ToolSpec> {
let registry = self.tool_registry.read().await;
registry.get(tool_name).cloned()
}
pub async fn get_all_tools(&self) -> Vec<String> {
let registry = self.tool_registry.read().await;
registry.keys().cloned().collect()
}
pub async fn clear_cache(&self) {
let mut cache = self.resolution_cache.write().await;
cache.clear();
}
pub async fn get_stats(&self) -> crate::graph::GraphStats {
let graph = self.graph.read().await;
graph.get_stats()
}
async fn get_cached_resolution(&self, tool_name: &str) -> Option<CachedResolution> {
let cache = self.resolution_cache.read().await;
cache.get(tool_name).cloned()
}
async fn cache_resolution(&self, tool_name: &str, result: ResolutionResult) {
let mut cache = self.resolution_cache.write().await;
cache.insert(
tool_name.to_string(),
CachedResolution {
result,
cached_at: Instant::now(),
ttl: self.options.cache_ttl,
},
);
}
async fn update_availability(&self) -> Result<()> {
if let Some(checker) = &self.availability_checker {
let tools = self.get_all_tools().await;
let mut graph = self.graph.write().await;
for tool_name in tools {
let available = checker.is_available(&tool_name).await.unwrap_or(false);
let version = if available {
checker.get_version(&tool_name).await.unwrap_or(None)
} else {
None
};
graph.set_tool_available(&tool_name, available, version);
}
}
Ok(())
}
async fn filter_resolution(&self, result: &mut ResolutionResult) {
if let Some(platform) = &self.options.platform_filter {
let registry = self.tool_registry.read().await;
result.install_order.retain(|tool_name| {
if let Some(tool_spec) = registry.get(tool_name) {
tool_spec
.dependencies
.iter()
.all(|dep| dep.applies_to_platform(platform))
} else {
true
}
});
}
if !self.options.include_optional || !self.options.include_dev {
}
}
}
impl Default for ResolutionOptions {
fn default() -> Self {
Self {
include_optional: false,
include_dev: false,
max_depth: 10,
cache_ttl: Duration::from_secs(300), enable_parallel: true,
platform_filter: None,
allow_prerelease: false,
}
}
}
impl Default for DependencyResolver {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
struct MockAvailabilityChecker {
available_tools: HashMap<String, (bool, Option<String>)>,
}
impl MockAvailabilityChecker {
fn new() -> Self {
let mut available_tools = HashMap::new();
available_tools.insert("node".to_string(), (true, Some("18.0.0".to_string())));
available_tools.insert("python".to_string(), (true, Some("3.9.0".to_string())));
Self { available_tools }
}
}
#[async_trait::async_trait]
impl AvailabilityChecker for MockAvailabilityChecker {
async fn is_available(&self, tool_name: &str) -> Result<bool> {
Ok(self
.available_tools
.get(tool_name)
.map(|(available, _)| *available)
.unwrap_or(false))
}
async fn get_version(&self, tool_name: &str) -> Result<Option<String>> {
Ok(self
.available_tools
.get(tool_name)
.and_then(|(_, version)| version.clone()))
}
async fn get_path(&self, _tool_name: &str) -> Result<Option<String>> {
Ok(None)
}
}
fn create_test_tool(name: &str, deps: Vec<&str>) -> ToolSpec {
ToolSpec {
name: name.to_string(),
dependencies: deps
.into_iter()
.map(|dep| DependencySpec::required(dep, format!("{} requires {}", name, dep)))
.collect(),
..Default::default()
}
}
#[tokio::test]
async fn test_resolver_basic_functionality() {
let resolver = DependencyResolver::new()
.with_availability_checker(Arc::new(MockAvailabilityChecker::new()));
resolver
.register_tool(create_test_tool("node", vec![]))
.await
.unwrap();
resolver
.register_tool(create_test_tool("yarn", vec!["node"]))
.await
.unwrap();
let result = resolver.resolve("yarn").await.unwrap();
assert_eq!(result.install_order, vec!["node", "yarn"]);
assert_eq!(result.available_tools, vec!["node"]);
assert_eq!(result.missing_tools, vec!["yarn"]);
}
#[tokio::test]
async fn test_resolver_multiple_tools() {
let resolver = DependencyResolver::new()
.with_availability_checker(Arc::new(MockAvailabilityChecker::new()));
resolver
.register_tool(create_test_tool("node", vec![]))
.await
.unwrap();
resolver
.register_tool(create_test_tool("python", vec![]))
.await
.unwrap();
resolver
.register_tool(create_test_tool("yarn", vec!["node"]))
.await
.unwrap();
resolver
.register_tool(create_test_tool("pip", vec!["python"]))
.await
.unwrap();
let result = resolver
.resolve_multiple(&["yarn".to_string(), "pip".to_string()])
.await
.unwrap();
assert!(result.install_order.contains(&"node".to_string()));
assert!(result.install_order.contains(&"python".to_string()));
assert!(result.install_order.contains(&"yarn".to_string()));
assert!(result.install_order.contains(&"pip".to_string()));
}
#[tokio::test]
async fn test_resolver_caching() {
let resolver = DependencyResolver::new()
.with_availability_checker(Arc::new(MockAvailabilityChecker::new()));
resolver
.register_tool(create_test_tool("node", vec![]))
.await
.unwrap();
resolver
.register_tool(create_test_tool("yarn", vec!["node"]))
.await
.unwrap();
let start = Instant::now();
let result1 = resolver.resolve("yarn").await.unwrap();
let first_duration = start.elapsed();
let start = Instant::now();
let result2 = resolver.resolve("yarn").await.unwrap();
let second_duration = start.elapsed();
assert_eq!(result1.install_order, result2.install_order);
assert!(second_duration < first_duration);
}
#[tokio::test]
async fn test_resolver_unregistered_tool() {
let resolver = DependencyResolver::new();
let result = resolver.resolve("nonexistent").await;
assert!(result.is_err());
if let Err(Error::ToolNotFound { tool }) = result {
assert_eq!(tool, "nonexistent");
} else {
panic!("Expected ToolNotFound error");
}
}
}