use crate::registry::{NpmRegistry, RegistryError};
use std::collections::{HashMap, HashSet};
#[derive(Debug, Clone)]
pub struct ResolvedPackage {
pub name: String,
pub version: String,
pub tarball_url: String,
pub integrity: Option<String>,
pub dependencies: HashMap<String, String>,
}
pub struct Resolver {
registry: NpmRegistry,
resolved: HashMap<String, ResolvedPackage>,
in_progress: HashSet<String>,
}
impl Resolver {
pub fn new(registry: NpmRegistry) -> Self {
Self {
registry,
resolved: HashMap::new(),
in_progress: HashSet::new(),
}
}
pub async fn resolve(
&mut self,
dependencies: &HashMap<String, String>,
) -> Result<Vec<ResolvedPackage>, ResolverError> {
for (name, version_req) in dependencies {
self.resolve_package(name, version_req).await?;
}
Ok(self.resolved.values().cloned().collect())
}
pub async fn resolve_package(
&mut self,
name: &str,
version_req: &str,
) -> Result<(), ResolverError> {
if self.resolved.contains_key(name) {
return Ok(());
}
if self.in_progress.contains(name) {
return Err(ResolverError::CircularDependency(name.to_string()));
}
self.in_progress.insert(name.to_string());
let version = self
.registry
.resolve_version(name, version_req)
.await
.map_err(ResolverError::Registry)?;
let metadata = self
.registry
.get_package(name)
.await
.map_err(ResolverError::Registry)?;
let version_info =
metadata
.versions
.get(&version)
.ok_or_else(|| ResolverError::VersionNotFound {
name: name.to_string(),
version: version.clone(),
})?;
let deps = version_info.dependencies.clone().unwrap_or_default();
let tarball_url = version_info.dist.tarball.clone();
let integrity = version_info.dist.integrity.clone();
for (dep_name, dep_version) in &deps {
Box::pin(self.resolve_package(dep_name, dep_version)).await?;
}
self.resolved.insert(
name.to_string(),
ResolvedPackage {
name: name.to_string(),
version,
tarball_url,
integrity,
dependencies: deps,
},
);
self.in_progress.remove(name);
Ok(())
}
pub fn get_resolved(&self) -> &HashMap<String, ResolvedPackage> {
&self.resolved
}
pub fn get_package(&self, name: &str) -> Option<&ResolvedPackage> {
self.resolved.get(name)
}
pub fn clear(&mut self) {
self.resolved.clear();
self.in_progress.clear();
}
pub fn into_registry(self) -> NpmRegistry {
self.registry
}
}
#[derive(Debug, thiserror::Error)]
pub enum ResolverError {
#[error("Registry error: {0}")]
Registry(#[from] RegistryError),
#[error("Circular dependency: {0}")]
CircularDependency(String),
#[error("Version not found: {name}@{version}")]
VersionNotFound { name: String, version: String },
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_resolver_new() {
let registry = NpmRegistry::new();
let resolver = Resolver::new(registry);
assert!(resolver.resolved.is_empty());
assert!(resolver.in_progress.is_empty());
}
#[tokio::test]
#[ignore] async fn test_resolve_simple() {
let registry = NpmRegistry::new();
let mut resolver = Resolver::new(registry);
let mut deps = HashMap::new();
deps.insert("is-odd".to_string(), "^3.0.0".to_string());
let result = resolver.resolve(&deps).await;
if let Ok(packages) = result {
assert!(!packages.is_empty());
let names: Vec<_> = packages.iter().map(|p| p.name.as_str()).collect();
assert!(names.contains(&"is-odd"));
}
}
#[tokio::test]
#[ignore] async fn test_resolve_with_transitive() {
let registry = NpmRegistry::new();
let mut resolver = Resolver::new(registry);
let mut deps = HashMap::new();
deps.insert("chalk".to_string(), "^4.0.0".to_string());
let result = resolver.resolve(&deps).await;
if let Ok(packages) = result {
assert!(packages.len() > 1);
let names: Vec<_> = packages.iter().map(|p| p.name.as_str()).collect();
assert!(names.contains(&"chalk"));
}
}
}