use std::collections::HashMap;
use petgraph::graph::DiGraph;
use crate::import::ResolvedPackage;
use crate::model::file::AgmFile;
use super::{AgmGraph, RelationKind};
#[must_use]
pub fn build_graph(file: &AgmFile) -> AgmGraph {
let mut inner: DiGraph<String, RelationKind> = DiGraph::new();
let mut index = HashMap::new();
for node in &file.nodes {
let idx = inner.add_node(node.id.clone());
index.insert(node.id.clone(), idx);
}
add_edges_from_nodes(&file.nodes, &mut inner, &index);
AgmGraph { inner, index }
}
#[must_use]
pub fn build_graph_with_imports(file: &AgmFile, imports: &[ResolvedPackage]) -> AgmGraph {
let mut inner: DiGraph<String, RelationKind> = DiGraph::new();
let mut index = HashMap::new();
for node in &file.nodes {
if !index.contains_key(&node.id) {
let idx = inner.add_node(node.id.clone());
index.insert(node.id.clone(), idx);
}
}
for pkg in imports {
for node in &pkg.file.nodes {
let qualified_id = format!("{}.{}", pkg.package, node.id);
index
.entry(qualified_id)
.or_insert_with_key(|key| inner.add_node(key.clone()));
}
}
add_edges_from_nodes(&file.nodes, &mut inner, &index);
for pkg in imports {
for node in &pkg.file.nodes {
let qualified_source = format!("{}.{}", pkg.package, node.id);
let Some(&src_idx) = index.get(&qualified_source) else {
continue;
};
add_edges_for_targets(
src_idx,
node.depends.as_deref().unwrap_or(&[]),
RelationKind::Depends,
&pkg.package,
&mut inner,
&index,
);
add_edges_for_targets(
src_idx,
node.related_to.as_deref().unwrap_or(&[]),
RelationKind::RelatedTo,
&pkg.package,
&mut inner,
&index,
);
add_edges_for_targets(
src_idx,
node.replaces.as_deref().unwrap_or(&[]),
RelationKind::Replaces,
&pkg.package,
&mut inner,
&index,
);
add_edges_for_targets(
src_idx,
node.conflicts.as_deref().unwrap_or(&[]),
RelationKind::Conflicts,
&pkg.package,
&mut inner,
&index,
);
add_edges_for_targets(
src_idx,
node.see_also.as_deref().unwrap_or(&[]),
RelationKind::SeeAlso,
&pkg.package,
&mut inner,
&index,
);
}
}
AgmGraph { inner, index }
}
fn add_edges_from_nodes(
nodes: &[crate::model::node::Node],
inner: &mut DiGraph<String, RelationKind>,
index: &HashMap<String, petgraph::graph::NodeIndex>,
) {
for node in nodes {
let Some(&src_idx) = index.get(&node.id) else {
continue;
};
for (targets, kind) in [
(
node.depends.as_deref().unwrap_or(&[]),
RelationKind::Depends,
),
(
node.related_to.as_deref().unwrap_or(&[]),
RelationKind::RelatedTo,
),
(
node.replaces.as_deref().unwrap_or(&[]),
RelationKind::Replaces,
),
(
node.conflicts.as_deref().unwrap_or(&[]),
RelationKind::Conflicts,
),
(
node.see_also.as_deref().unwrap_or(&[]),
RelationKind::SeeAlso,
),
] {
for target in targets {
if let Some(&tgt_idx) = index.get(target) {
inner.add_edge(src_idx, tgt_idx, kind);
}
}
}
}
}
fn add_edges_for_targets(
src_idx: petgraph::graph::NodeIndex,
targets: &[String],
kind: RelationKind,
package: &str,
inner: &mut DiGraph<String, RelationKind>,
index: &HashMap<String, petgraph::graph::NodeIndex>,
) {
for target in targets {
let tgt_idx = if let Some(&idx) = index.get(target.as_str()) {
idx
} else {
let qualified = format!("{package}.{target}");
if let Some(&idx) = index.get(&qualified) {
idx
} else {
continue;
}
};
inner.add_edge(src_idx, tgt_idx, kind);
}
}
#[cfg(test)]
mod tests {
use std::path::PathBuf;
use super::*;
use crate::graph::test_helpers::*;
#[test]
fn test_build_graph_empty_file_returns_empty_graph() {
let file = make_file(vec![]);
let graph = build_graph(&file);
assert_eq!(graph.node_count(), 0);
assert_eq!(graph.edge_count(), 0);
}
#[test]
fn test_build_graph_single_node_no_deps_returns_one_node() {
let file = make_file(vec![make_node("a")]);
let graph = build_graph(&file);
assert_eq!(graph.node_count(), 1);
assert_eq!(graph.edge_count(), 0);
assert!(graph.contains_node("a"));
}
#[test]
fn test_build_graph_depends_edges_created() {
let mut a = make_node("a");
a.depends = Some(vec!["b".to_owned()]);
let b = make_node("b");
let file = make_file(vec![a, b]);
let graph = build_graph(&file);
assert_eq!(graph.node_count(), 2);
assert_eq!(graph.edge_count(), 1);
let edges = graph.edges_of_kind("a", RelationKind::Depends);
assert_eq!(edges, vec!["b"]);
}
#[test]
fn test_build_graph_all_relationship_types() {
let mut a = make_node("a");
let b = make_node("b");
let c = make_node("c");
let d = make_node("d");
let e = make_node("e");
let f = make_node("f");
a.depends = Some(vec!["b".to_owned()]);
a.related_to = Some(vec!["c".to_owned()]);
a.replaces = Some(vec!["d".to_owned()]);
a.conflicts = Some(vec!["e".to_owned()]);
a.see_also = Some(vec!["f".to_owned()]);
let file = make_file(vec![a, b, c, d, e, f]);
let graph = build_graph(&file);
assert_eq!(graph.node_count(), 6);
assert_eq!(graph.edge_count(), 5);
assert!(!graph.edges_of_kind("a", RelationKind::Depends).is_empty());
assert!(!graph.edges_of_kind("a", RelationKind::RelatedTo).is_empty());
assert!(!graph.edges_of_kind("a", RelationKind::Replaces).is_empty());
assert!(!graph.edges_of_kind("a", RelationKind::Conflicts).is_empty());
assert!(!graph.edges_of_kind("a", RelationKind::SeeAlso).is_empty());
}
#[test]
fn test_build_graph_unknown_target_skipped() {
let mut a = make_node("a");
a.depends = Some(vec!["nonexistent".to_owned()]);
let file = make_file(vec![a]);
let graph = build_graph(&file);
assert_eq!(graph.node_count(), 1);
assert_eq!(graph.edge_count(), 0);
}
#[test]
fn test_build_graph_with_imports_adds_qualified_nodes() {
let file = make_file(vec![]);
let imported_node = make_node("auth.rules");
let pkg = ResolvedPackage {
package: "shared.security".to_owned(),
version: semver::Version::parse("1.0.0").unwrap(),
path: PathBuf::from("shared.security.agm"),
file: make_file(vec![imported_node]),
};
let graph = build_graph_with_imports(&file, &[pkg]);
assert!(graph.contains_node("shared.security.auth.rules"));
}
#[test]
fn test_build_graph_with_imports_cross_package_edge() {
let mut local = make_node("local.node");
local.depends = Some(vec!["shared.security.auth.rules".to_owned()]);
let file = make_file(vec![local]);
let imported_node = make_node("auth.rules");
let pkg = ResolvedPackage {
package: "shared.security".to_owned(),
version: semver::Version::parse("1.0.0").unwrap(),
path: PathBuf::from("shared.security.agm"),
file: make_file(vec![imported_node]),
};
let graph = build_graph_with_imports(&file, &[pkg]);
assert_eq!(graph.node_count(), 2);
let edges = graph.edges_of_kind("local.node", RelationKind::Depends);
assert!(edges.contains(&"shared.security.auth.rules"));
}
}