use aube_lockfile::LockfileGraph;
use std::collections::{BTreeMap, HashMap, VecDeque};
use std::sync::{Arc, Mutex, OnceLock};
#[derive(Debug, Default)]
pub struct ChainIndex {
chains: HashMap<(String, String), Vec<(String, String)>>,
}
impl ChainIndex {
pub fn lookup(&self, name: &str, version: &str) -> Option<&[(String, String)]> {
self.chains
.get(&(name.to_string(), version.to_string()))
.map(Vec::as_slice)
}
pub fn from_graph(graph: &LockfileGraph) -> Self {
let mut chains: HashMap<(String, String), Vec<(String, String)>> = HashMap::new();
let mut queue: VecDeque<(String, Vec<(String, String)>)> = VecDeque::new();
for deps in graph.importers.values() {
for direct in deps {
queue.push_back((direct.dep_path.clone(), Vec::new()));
}
}
while let Some((dep_path, ancestors)) = queue.pop_front() {
let Some(pkg) = graph.packages.get(&dep_path) else {
continue;
};
let alias_key = (pkg.name.clone(), pkg.version.clone());
if chains.contains_key(&alias_key) {
continue;
}
chains.insert(alias_key, ancestors.clone());
if let Some(real) = &pkg.alias_of {
let real_key = (real.clone(), pkg.version.clone());
chains.entry(real_key).or_insert_with(|| ancestors.clone());
}
let mut child_ancestors = ancestors;
child_ancestors.push((pkg.name.clone(), pkg.version.clone()));
push_children(&mut queue, &pkg.dependencies, &child_ancestors);
push_children(&mut queue, &pkg.optional_dependencies, &child_ancestors);
}
Self { chains }
}
}
fn push_children(
queue: &mut VecDeque<(String, Vec<(String, String)>)>,
children: &BTreeMap<String, String>,
ancestors: &[(String, String)],
) {
for (child_name, child_tail) in children {
let child_dep_path = format!("{child_name}@{child_tail}");
queue.push_back((child_dep_path, ancestors.to_vec()));
}
}
pub fn format_chain(ancestors: &[(String, String)], leaf_name: &str, leaf_version: &str) -> String {
if ancestors.is_empty() {
return String::new();
}
let mut s = String::from("chain: ");
for (i, (n, v)) in ancestors.iter().enumerate() {
if i > 0 {
s.push_str(" > ");
}
s.push_str(&format!("{n}@{v}"));
}
s.push_str(&format!(" > {leaf_name}@{leaf_version}"));
s
}
fn slot() -> &'static Mutex<Option<Arc<ChainIndex>>> {
static SLOT: OnceLock<Mutex<Option<Arc<ChainIndex>>>> = OnceLock::new();
SLOT.get_or_init(|| Mutex::new(None))
}
pub fn set_active(graph: &LockfileGraph) {
let idx = Arc::new(ChainIndex::from_graph(graph));
*slot().lock().expect("chain index slot poisoned") = Some(idx);
}
pub fn format_chain_for(name: &str, version: &str) -> String {
let guard = match slot().lock() {
Ok(g) => g,
Err(_) => return String::new(),
};
let Some(idx) = guard.as_ref() else {
return String::new();
};
match idx.lookup(name, version) {
Some(chain) if !chain.is_empty() => {
format!("\n{}", format_chain(chain, name, version))
}
_ => String::new(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use aube_lockfile::{DirectDep, LockedPackage};
fn pkg(name: &str, version: &str, deps: &[(&str, &str)]) -> (String, LockedPackage) {
let dep_path = format!("{name}@{version}");
let dependencies: BTreeMap<String, String> = deps
.iter()
.map(|(n, v)| (n.to_string(), v.to_string()))
.collect();
(
dep_path.clone(),
LockedPackage {
name: name.to_string(),
version: version.to_string(),
dep_path,
dependencies,
..Default::default()
},
)
}
fn direct(name: &str, version: &str) -> DirectDep {
DirectDep {
name: name.to_string(),
dep_path: format!("{name}@{version}"),
dep_type: aube_lockfile::DepType::Production,
specifier: None,
}
}
#[test]
fn shortest_chain_wins() {
let mut graph = LockfileGraph::default();
graph
.importers
.insert(".".to_string(), vec![direct("a", "1")]);
graph.packages.extend([
pkg("a", "1", &[("b", "1"), ("c", "1")]),
pkg("b", "1", &[("d", "1")]),
pkg("c", "1", &[]),
pkg("d", "1", &[]),
]);
let idx = ChainIndex::from_graph(&graph);
assert_eq!(idx.lookup("a", "1"), Some(&[][..]));
assert_eq!(
idx.lookup("b", "1"),
Some(&[("a".to_string(), "1".to_string())][..])
);
assert_eq!(
idx.lookup("d", "1"),
Some(
&[
("a".to_string(), "1".to_string()),
("b".to_string(), "1".to_string())
][..]
)
);
}
#[test]
fn format_chain_renders_arrow_path() {
let chain = vec![
("a".to_string(), "1".to_string()),
("b".to_string(), "2".to_string()),
];
assert_eq!(
format_chain(&chain, "leaf", "3"),
"chain: a@1 > b@2 > leaf@3"
);
}
#[test]
fn format_chain_empty_returns_empty() {
assert_eq!(format_chain(&[], "leaf", "3"), "");
}
#[test]
fn aliased_packages_resolve_under_both_alias_and_real_name() {
let mut graph = LockfileGraph::default();
graph.importers.insert(
".".to_string(),
vec![DirectDep {
name: "h3-v2".to_string(),
dep_path: "h3-v2@2.0.0".to_string(),
dep_type: aube_lockfile::DepType::Production,
specifier: None,
}],
);
graph.packages.insert(
"h3-v2@2.0.0".to_string(),
LockedPackage {
name: "h3-v2".to_string(),
version: "2.0.0".to_string(),
dep_path: "h3-v2@2.0.0".to_string(),
alias_of: Some("h3".to_string()),
..Default::default()
},
);
let idx = ChainIndex::from_graph(&graph);
assert_eq!(idx.lookup("h3-v2", "2.0.0"), Some(&[][..]));
assert_eq!(idx.lookup("h3", "2.0.0"), Some(&[][..]));
}
}