use std::collections::HashMap;
use petgraph::graph::{DiGraph, NodeIndex};
use petgraph::visit::EdgeRef;
use serde::{Deserialize, Serialize};
pub mod build;
pub mod cycles;
pub mod query;
pub mod topo;
pub use build::{build_graph, build_graph_with_imports};
pub use cycles::{CycleError, detect_cycles};
pub use query::{find_conflicts, transitive_dependents, transitive_deps};
pub use topo::topological_sort;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum RelationKind {
Depends,
RelatedTo,
Replaces,
Conflicts,
SeeAlso,
}
#[derive(Debug, Clone)]
pub struct AgmGraph {
pub(crate) inner: DiGraph<String, RelationKind>,
pub(crate) index: HashMap<String, NodeIndex>,
}
impl AgmGraph {
#[must_use]
pub fn node_count(&self) -> usize {
self.inner.node_count()
}
#[must_use]
pub fn edge_count(&self) -> usize {
self.inner.edge_count()
}
#[must_use]
pub fn contains_node(&self, node_id: &str) -> bool {
self.index.contains_key(node_id)
}
#[must_use]
pub fn node_index(&self, node_id: &str) -> Option<NodeIndex> {
self.index.get(node_id).copied()
}
#[must_use]
pub fn node_ids(&self) -> Vec<&str> {
self.index.keys().map(String::as_str).collect()
}
#[must_use]
pub fn edges_of_kind(&self, node_id: &str, kind: RelationKind) -> Vec<&str> {
let Some(&idx) = self.index.get(node_id) else {
return vec![];
};
self.inner
.edges(idx)
.filter(|e| *e.weight() == kind)
.map(|e| self.inner[e.target()].as_str())
.collect()
}
}
pub(crate) fn depends_subgraph(graph: &AgmGraph) -> DiGraph<String, ()> {
let mut sub: DiGraph<String, ()> = DiGraph::new();
let mut old_to_new: HashMap<NodeIndex, NodeIndex> = HashMap::new();
for node_idx in graph.inner.node_indices() {
let id = graph.inner[node_idx].clone();
let new_idx = sub.add_node(id);
old_to_new.insert(node_idx, new_idx);
}
for edge in graph.inner.edge_references() {
if *edge.weight() == RelationKind::Depends {
let src = old_to_new[&edge.source()];
let tgt = old_to_new[&edge.target()];
sub.add_edge(src, tgt, ());
}
}
sub
}
#[cfg(test)]
pub(crate) mod test_helpers {
use crate::model::fields::{NodeType, Span};
use crate::model::file::{AgmFile, Header};
use crate::model::node::Node;
pub fn minimal_header() -> Header {
Header {
agm: "1.0".to_owned(),
package: "test.pkg".to_owned(),
version: "0.1.0".to_owned(),
title: None,
owner: None,
imports: None,
default_load: None,
description: None,
tags: None,
status: None,
load_profiles: None,
target_runtime: None,
}
}
pub fn make_node(id: &str) -> Node {
Node {
id: id.to_owned(),
node_type: NodeType::Facts,
summary: format!("test node {id}"),
span: Span::new(1, 1),
..Default::default()
}
}
pub fn make_file(nodes: Vec<Node>) -> AgmFile {
AgmFile {
header: minimal_header(),
nodes,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use test_helpers::*;
#[test]
fn test_relation_kind_debug_and_clone() {
let k = RelationKind::Depends;
let k2 = k;
assert_eq!(k, k2);
assert_eq!(format!("{k:?}"), "Depends");
let json = serde_json::to_string(&RelationKind::Conflicts).unwrap();
let back: RelationKind = serde_json::from_str(&json).unwrap();
assert_eq!(back, RelationKind::Conflicts);
}
#[test]
fn test_agm_graph_empty_returns_zero_counts() {
let graph = build_graph(&make_file(vec![]));
assert_eq!(graph.node_count(), 0);
assert_eq!(graph.edge_count(), 0);
}
}