use std::collections::BTreeMap;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MaintainerReach {
pub identity: String,
pub packages: Vec<String>,
}
impl MaintainerReach {
pub fn count(&self) -> usize {
self.packages.len()
}
}
pub fn capture_risk<'a>(
entries: impl IntoIterator<Item = (&'a str, &'a [String])>,
) -> Vec<MaintainerReach> {
let mut map: BTreeMap<String, Vec<String>> = BTreeMap::new();
for (pkg, maintainers) in entries {
for m in maintainers {
let e = map.entry(m.clone()).or_default();
if !e.iter().any(|p| p == pkg) {
e.push(pkg.to_string());
}
}
}
let mut out: Vec<MaintainerReach> = map
.into_iter()
.map(|(identity, mut packages)| {
packages.sort();
MaintainerReach { identity, packages }
})
.collect();
out.sort_by(|a, b| b.count().cmp(&a.count()).then(a.identity.cmp(&b.identity)));
out
}
pub fn top_share(reaches: &[MaintainerReach], total_packages: usize) -> f64 {
if total_packages == 0 {
return 0.0;
}
reaches
.first()
.map(|r| r.count() as f64 / total_packages as f64)
.unwrap_or(0.0)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn aggregates_and_ranks_by_reach() {
let a = vec!["alice@x".to_string()];
let ab = vec!["alice@x".to_string(), "bob@y".to_string()];
let entries: Vec<(&str, &[String])> = vec![
("pkg1", a.as_slice()),
("pkg2", ab.as_slice()),
("pkg3", a.as_slice()),
];
let reaches = capture_risk(entries);
assert_eq!(reaches[0].identity, "alice@x");
assert_eq!(reaches[0].count(), 3);
assert!((top_share(&reaches, 3) - 1.0).abs() < 1e-9);
}
}