use std::collections::{HashMap, HashSet};
use crate::graph;
use super::impact::ImpactModel;
use super::why::Why;
#[derive(Debug, Clone, PartialEq)]
pub struct ImportedRow {
pub file: String,
pub repo: String,
pub hop: u32,
pub imported_kind: String,
pub why: Why,
}
#[derive(Debug, Clone, PartialEq, Default)]
pub struct ImportedSection {
pub rows: Vec<ImportedRow>,
pub dropped: usize,
pub affected: usize,
}
fn local_hops(model: &ImpactModel) -> HashMap<&str, u32> {
let mut hops = HashMap::new();
for f in &model.seed_files {
hops.entry(f.as_str()).or_insert(0);
}
for r in &model.rows {
hops.entry(r.file.as_str()).or_insert(r.hop);
}
hops
}
fn is_mapped(end: &graph::EdgeEnd, mapped_repo: &str) -> bool {
end.repo.as_deref() == Some(mapped_repo)
}
fn direct_rows(
edges: &graph::ImportedEdges,
hops: &HashMap<&str, u32>,
seen: &mut HashSet<(String, String)>,
out: &mut Vec<ImportedRow>,
) {
for rec in &edges.edges {
if is_mapped(&rec.from, &edges.mapped_repo) || !is_mapped(&rec.to, &edges.mapped_repo) {
continue;
}
let Some(to_file) = rec.to.file.as_deref() else {
continue;
};
let Some(&hop) = hops.get(to_file) else {
continue;
};
let (Some(from_repo), Some(from_file)) =
(rec.from.repo.as_deref(), rec.from.file.as_deref())
else {
continue;
};
if seen.insert((from_repo.to_string(), from_file.to_string())) {
out.push(ImportedRow {
file: from_file.to_string(),
repo: from_repo.to_string(),
hop: hop + 1,
imported_kind: rec.kind.clone(),
why: Why::ImportedEdge,
});
}
}
}
fn composed_rows(
edges: &graph::ImportedEdges,
hops: &HashMap<&str, u32>,
seen: &mut HashSet<(String, String)>,
out: &mut Vec<ImportedRow>,
) {
for pub_rec in &edges.edges {
if pub_rec.kind != "publishes" || !is_mapped(&pub_rec.from, &edges.mapped_repo) {
continue;
}
let Some(pub_file) = pub_rec.from.file.as_deref() else {
continue;
};
let Some(&hop) = hops.get(pub_file) else {
continue;
};
let Some(msg_key) = pub_rec.key.as_deref() else {
continue;
};
for con_rec in &edges.edges {
if con_rec.kind != "consumes" && con_rec.kind != "enqueues" {
continue;
}
if con_rec.key.as_deref() != Some(msg_key) {
continue;
}
if is_mapped(&con_rec.to, &edges.mapped_repo) {
continue;
}
if con_rec.to.file.as_deref() == Some(pub_file) && con_rec.to.line == pub_rec.from.line
{
continue;
}
let (Some(con_repo), Some(con_file)) =
(con_rec.to.repo.as_deref(), con_rec.to.file.as_deref())
else {
continue;
};
if seen.insert((con_repo.to_string(), con_file.to_string())) {
out.push(ImportedRow {
file: con_file.to_string(),
repo: con_repo.to_string(),
hop: hop + 1,
imported_kind: con_rec.kind.clone(),
why: Why::ImportedEdge,
});
}
}
}
}
pub fn build_imported_section(
model: &ImpactModel,
edges: &graph::ImportedEdges,
cap: usize,
) -> ImportedSection {
let hops = local_hops(model);
let mut seen: HashSet<(String, String)> = HashSet::new();
let mut rows = Vec::new();
direct_rows(edges, &hops, &mut seen, &mut rows);
composed_rows(edges, &hops, &mut seen, &mut rows);
rows.sort_by(|a, b| {
a.hop
.cmp(&b.hop)
.then_with(|| a.repo.cmp(&b.repo))
.then_with(|| a.file.cmp(&b.file))
});
let affected = rows.len();
let dropped = affected.saturating_sub(cap);
rows.truncate(cap);
ImportedSection {
rows,
dropped,
affected,
}
}