1use crate::{Edge, GraphError, Node, NodeId, Result, SourceSpan};
2use serde::{Deserialize, Deserializer, Serialize, de::Error as _};
3use std::collections::{BTreeMap, BTreeSet};
4
5#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
6pub struct Graph {
7 nodes: Vec<Node>,
8 edges: Vec<Edge>,
9}
10
11impl Graph {
12 pub fn try_from_parts(
19 nodes: impl IntoIterator<Item = Node>,
20 edges: impl IntoIterator<Item = Edge>,
21 ) -> Result<Self> {
22 let mut builder = GraphBuilder::new();
23 for node in nodes {
24 builder.add_node(node)?;
25 }
26 for edge in edges {
27 builder.add_edge(edge)?;
28 }
29 builder.build()
30 }
31
32 #[must_use]
33 pub fn nodes(&self) -> &[Node] {
34 &self.nodes
35 }
36
37 #[must_use]
38 pub fn edges(&self) -> &[Edge] {
39 &self.edges
40 }
41
42 #[must_use]
43 pub fn node(&self, id: &str) -> Option<&Node> {
44 self.nodes
45 .binary_search_by(|node| node.id.as_str().cmp(id))
46 .ok()
47 .map(|index| &self.nodes[index])
48 }
49
50 pub fn outgoing<'graph>(
51 &'graph self,
52 id: &'graph NodeId,
53 ) -> impl Iterator<Item = &'graph Edge> {
54 self.edges.iter().filter(move |edge| &edge.source == id)
55 }
56
57 pub fn incoming<'graph>(
58 &'graph self,
59 id: &'graph NodeId,
60 ) -> impl Iterator<Item = &'graph Edge> {
61 self.edges.iter().filter(move |edge| &edge.target == id)
62 }
63
64 #[must_use]
65 pub const fn node_count(&self) -> usize {
66 self.nodes.len()
67 }
68
69 #[must_use]
70 pub const fn edge_count(&self) -> usize {
71 self.edges.len()
72 }
73
74 #[must_use]
75 pub const fn is_empty(&self) -> bool {
76 self.nodes.is_empty() && self.edges.is_empty()
77 }
78
79 #[must_use]
80 pub fn into_parts(self) -> (Vec<Node>, Vec<Edge>) {
81 (self.nodes, self.edges)
82 }
83}
84
85#[derive(Deserialize)]
86struct GraphWire {
87 nodes: Vec<Node>,
88 edges: Vec<Edge>,
89}
90
91impl<'de> Deserialize<'de> for Graph {
92 fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
93 where
94 D: Deserializer<'de>,
95 {
96 let wire = GraphWire::deserialize(deserializer)?;
97 Self::try_from_parts(wire.nodes, wire.edges).map_err(D::Error::custom)
98 }
99}
100
101#[derive(Debug, Default)]
102pub struct GraphBuilder {
103 nodes: BTreeMap<NodeId, Node>,
104 edges: BTreeSet<Edge>,
105}
106
107impl GraphBuilder {
108 #[must_use]
109 pub const fn new() -> Self {
110 Self {
111 nodes: BTreeMap::new(),
112 edges: BTreeSet::new(),
113 }
114 }
115
116 pub fn add_node(&mut self, node: Node) -> Result<&mut Self> {
123 if let Some(span) = &node.span {
124 validate_span(span)?;
125 }
126 if let Some(language) = &node.language {
127 validate_language(language)?;
128 }
129 if let Some(existing) = self.nodes.get(&node.id) {
130 if existing == &node {
131 return Ok(self);
132 }
133 return Err(GraphError::ConflictingNode {
134 id: node.id.to_string(),
135 });
136 }
137 self.nodes.insert(node.id.clone(), node);
138 Ok(self)
139 }
140
141 pub fn add_edge(&mut self, edge: Edge) -> Result<&mut Self> {
148 if edge.provenance.extractor.is_empty() {
149 return Err(GraphError::EmptyExtractor);
150 }
151 if let Some(span) = &edge.provenance.span {
152 validate_span(span)?;
153 }
154 self.edges.insert(edge);
155 Ok(self)
156 }
157
158 pub fn build(self) -> Result<Graph> {
164 for edge in &self.edges {
165 if !self.nodes.contains_key(&edge.source) {
166 return Err(GraphError::MissingEdgeSource {
167 id: edge.source.to_string(),
168 });
169 }
170 if !self.nodes.contains_key(&edge.target) {
171 return Err(GraphError::MissingEdgeTarget {
172 id: edge.target.to_string(),
173 });
174 }
175 }
176 Ok(Graph {
177 nodes: self.nodes.into_values().collect(),
178 edges: self.edges.into_iter().collect(),
179 })
180 }
181}
182
183fn validate_language(language: &str) -> Result<()> {
184 if language.is_empty() || language.trim() != language {
185 return Err(GraphError::InvalidKind {
186 category: "language",
187 value: language.to_owned(),
188 });
189 }
190 Ok(())
191}
192
193fn validate_span(span: &SourceSpan) -> Result<()> {
194 if span.file.is_empty() {
195 return Err(GraphError::InvalidSpan {
196 file: span.file.clone(),
197 reason: "file must not be empty",
198 });
199 }
200 if span.start.line == 0 || span.start.column == 0 || span.end.line == 0 || span.end.column == 0
201 {
202 return Err(GraphError::InvalidSpan {
203 file: span.file.clone(),
204 reason: "positions are one-based",
205 });
206 }
207 if span.end < span.start {
208 return Err(GraphError::InvalidSpan {
209 file: span.file.clone(),
210 reason: "end precedes start",
211 });
212 }
213 Ok(())
214}