Skip to main content

sinter_core/
graph.rs

1use std::collections::{BTreeMap, BTreeSet};
2
3use crate::edge::Edge;
4use crate::error::GraphError;
5use crate::node::{Node, NodeId};
6
7/// Directed multigraph over typed nodes.
8///
9/// Invariants, enforced at construction:
10/// - node ids are unique and case-sensitive; a collision is an error
11/// - every edge endpoint refers to an existing node
12/// - every node has a non-empty id, name, and file, and a span with `end > start`
13///
14/// Parallel edges that differ in relation or confidence coexist; exact
15/// duplicate edges deduplicate silently.
16#[derive(Debug, Default, Clone, PartialEq, Eq)]
17pub struct Graph {
18    nodes: BTreeMap<NodeId, Node>,
19    edges: BTreeSet<Edge>,
20}
21
22impl Graph {
23    pub fn new() -> Self {
24        Self::default()
25    }
26
27    pub fn add_node(&mut self, node: Node) -> Result<(), GraphError> {
28        let empty = |field| GraphError::EmptyField {
29            id: node.id.clone(),
30            field,
31        };
32        if node.id.as_str().is_empty() {
33            return Err(empty("id"));
34        }
35        if node.name.is_empty() {
36            return Err(empty("name"));
37        }
38        if node.file.is_empty() {
39            return Err(empty("file"));
40        }
41        if node.span.end <= node.span.start {
42            return Err(GraphError::InvalidSpan {
43                id: node.id.clone(),
44                start: node.span.start,
45                end: node.span.end,
46            });
47        }
48        if self.nodes.contains_key(&node.id) {
49            return Err(GraphError::DuplicateNode(node.id));
50        }
51        self.nodes.insert(node.id.clone(), node);
52        Ok(())
53    }
54
55    pub fn add_edge(&mut self, edge: Edge) -> Result<(), GraphError> {
56        if !self.nodes.contains_key(&edge.src) {
57            return Err(GraphError::MissingEndpoint(edge.src));
58        }
59        if !self.nodes.contains_key(&edge.dst) {
60            return Err(GraphError::MissingEndpoint(edge.dst));
61        }
62        self.edges.insert(edge);
63        Ok(())
64    }
65
66    pub fn node(&self, id: &NodeId) -> Option<&Node> {
67        self.nodes.get(id)
68    }
69
70    pub fn nodes(&self) -> impl Iterator<Item = &Node> {
71        self.nodes.values()
72    }
73
74    pub fn edges(&self) -> impl Iterator<Item = &Edge> {
75        self.edges.iter()
76    }
77
78    // ponytail: linear scans; indexed adjacency lives in sinter-store, which
79    // owns all at-scale queries.
80    pub fn edges_from<'a>(&'a self, id: &'a NodeId) -> impl Iterator<Item = &'a Edge> {
81        self.edges.iter().filter(move |e| &e.src == id)
82    }
83
84    pub fn edges_to<'a>(&'a self, id: &'a NodeId) -> impl Iterator<Item = &'a Edge> {
85        self.edges.iter().filter(move |e| &e.dst == id)
86    }
87
88    pub fn node_count(&self) -> usize {
89        self.nodes.len()
90    }
91
92    pub fn edge_count(&self) -> usize {
93        self.edges.len()
94    }
95}