use std::{
collections::BTreeMap,
path::{Path, PathBuf},
};
use async_trait::async_trait;
use super::types::{ExternalDependency, WorkspaceDependency};
type BoxError = Box<dyn std::error::Error + Send + Sync>;
pub trait Package: Send + Sync {
fn name(&self) -> &str;
fn version(&self) -> Option<&str>;
fn path(&self) -> &Path;
fn workspace_dependencies(&self) -> &[WorkspaceDependency];
fn external_dependencies(&self) -> &[ExternalDependency];
}
pub trait LockfileEntry: Send + Sync {
fn name(&self) -> &str;
fn version(&self) -> &str;
fn dependencies(&self) -> &[String];
}
pub trait Lockfile: Send + Sync {
fn entries(&self) -> Vec<Box<dyn LockfileEntry>>;
fn find(&self, name: &str) -> Option<Box<dyn LockfileEntry>> {
self.entries().into_iter().find(|e| e.name() == name)
}
fn reverse_dependency_map(&self) -> BTreeMap<String, Vec<String>> {
let mut map: BTreeMap<String, Vec<String>> = BTreeMap::new();
for entry in self.entries() {
for dep in entry.dependencies() {
let dep_name = parse_dependency_name(dep);
map.entry(dep_name)
.or_default()
.push(entry.name().to_string());
}
}
map
}
}
pub trait LockfileDiffParser: Send + Sync {
fn parse_changes(&self, changes: &[(char, String)]) -> Vec<String>;
}
#[async_trait]
pub trait Workspace: Send + Sync {
fn root(&self) -> &Path;
fn lockfile_path(&self) -> &'static str;
fn member_patterns(&self) -> &[String];
async fn is_member_by_path(&self, path: &Path) -> bool;
async fn is_member_by_name(&self, name: &str) -> bool;
async fn find_member(&self, name: &str) -> Option<PathBuf>;
async fn packages(&self) -> Result<Vec<Box<dyn Package>>, BoxError>;
async fn read_lockfile(&self) -> Result<Box<dyn Lockfile>, BoxError>;
fn diff_parser(&self) -> Box<dyn LockfileDiffParser>;
async fn package_name_to_path(&self) -> Result<BTreeMap<String, String>, BoxError> {
let mut result = BTreeMap::new();
let root = self.root();
for pkg in self.packages().await? {
let rel_path = pkg
.path()
.strip_prefix(root)
.unwrap_or_else(|_| pkg.path())
.to_string_lossy()
.to_string();
result.insert(pkg.name().to_string(), rel_path);
}
Ok(result)
}
async fn package_names(&self) -> Result<Vec<String>, BoxError> {
Ok(self
.packages()
.await?
.iter()
.map(|p| p.name().to_string())
.collect())
}
fn parse_lockfile_diff(&self, changes: &[(char, String)]) -> Vec<String> {
self.diff_parser().parse_changes(changes)
}
async fn read_lockfile_entries(&self) -> Result<Vec<(String, String, Vec<String>)>, BoxError> {
let lockfile = self.read_lockfile().await?;
Ok(lockfile
.entries()
.iter()
.map(|e| {
(
e.name().to_string(),
e.version().to_string(),
e.dependencies().to_vec(),
)
})
.collect())
}
}
#[must_use]
pub fn parse_dependency_name(dep_spec: &str) -> String {
let spec = dep_spec.trim();
if let Some(stripped) = spec.strip_prefix('/') {
if let Some(at_pos) = stripped.rfind('@') {
if stripped.starts_with('@') {
if let Some(first_slash) = stripped.find('/') {
let after_scope = &stripped[first_slash + 1..];
if let Some(version_at) = after_scope.find('@') {
return stripped[..first_slash + 1 + version_at].to_string();
}
}
}
return stripped[..at_pos].to_string();
}
return stripped.to_string();
}
spec.split_whitespace().next().unwrap_or(spec).to_string()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_dependency_name_simple() {
assert_eq!(parse_dependency_name("serde"), "serde");
}
#[test]
fn test_parse_dependency_name_with_version() {
assert_eq!(parse_dependency_name("serde 1.0.0"), "serde");
}
#[test]
fn test_parse_dependency_name_cargo_lock_format() {
assert_eq!(
parse_dependency_name(
"serde 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)"
),
"serde"
);
}
#[test]
fn test_parse_dependency_name_pnpm_format() {
assert_eq!(parse_dependency_name("/lodash@4.17.21"), "lodash");
}
#[test]
fn test_parse_dependency_name_pnpm_scoped() {
assert_eq!(parse_dependency_name("/@babel/core@7.0.0"), "@babel/core");
}
}