1use serde::{Deserialize, Deserializer, Serialize};
2
3use super::core::EdgeStore;
4use super::edge::Edge;
5use super::error::GraphError;
6use super::traits::{Cascadable, Directed, EdgeSet, Graph};
7
8#[derive(Debug, Clone, PartialEq, Serialize)]
27pub struct DagGraph<E> {
28 #[serde(flatten)]
29 store: EdgeStore<E>,
30}
31
32impl<E> Default for DagGraph<E> {
33 fn default() -> Self {
34 Self {
35 store: EdgeStore::new(),
36 }
37 }
38}
39
40impl<'de, E> Deserialize<'de> for DagGraph<E>
41where
42 E: Edge + Clone + Deserialize<'de>,
43{
44 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
45 let store: EdgeStore<E> = EdgeStore::deserialize(deserializer)?;
46 let mut graph: DagGraph<E> = DagGraph::default();
47 for edge in store.edges() {
48 graph
49 .add_edge_with_metadata(edge.clone())
50 .map_err(serde::de::Error::custom)?;
51 }
52 Ok(graph)
53 }
54}
55
56impl<E: Edge> DagGraph<E> {
57 pub fn new() -> Self {
58 Self::default()
59 }
60
61 pub fn edges(&self) -> &[E] {
65 self.store.edges()
66 }
67
68 pub fn add_edge_with_metadata(&mut self, edge: E) -> Result<(), GraphError> {
83 if edge.source() == edge.target() {
84 return Err(GraphError::SelfReference);
85 }
86 if edge.is_active() {
87 if self
88 .store
89 .outgoing_active(edge.source())
90 .any(|e| e.target() == edge.target())
91 {
92 return Err(GraphError::Duplicate);
93 }
94 let adj = self.store.adjacency_list();
95 if super::algorithms::would_create_cycle(&adj, edge.source(), edge.target()) {
96 return Err(GraphError::Cycle);
97 }
98 }
99 self.store.add_edge(edge);
100 Ok(())
101 }
102
103 pub fn descendants(&self, node: E::NodeId) -> Vec<E::NodeId> {
105 let mut out = Vec::new();
106 let mut stack = vec![node];
107 let mut seen = std::collections::HashSet::new();
108 seen.insert(node);
109 while let Some(n) = stack.pop() {
110 for next in self.outgoing(n) {
111 if seen.insert(next) {
112 out.push(next);
113 stack.push(next);
114 }
115 }
116 }
117 out
118 }
119
120 pub fn ancestors(&self, node: E::NodeId) -> Vec<E::NodeId> {
122 let mut out = Vec::new();
123 let mut stack = vec![node];
124 let mut seen = std::collections::HashSet::new();
125 seen.insert(node);
126 while let Some(n) = stack.pop() {
127 for prev in self.incoming(n) {
128 if seen.insert(prev) {
129 out.push(prev);
130 stack.push(prev);
131 }
132 }
133 }
134 out
135 }
136}
137
138impl<E: Edge> Cascadable for DagGraph<E> {
139 fn archive_node(&mut self, node: E::NodeId) {
140 self.store.archive_node(node);
141 }
142 fn unarchive_node(&mut self, node: E::NodeId) {
143 self.store.unarchive_node(node);
144 }
145 fn remove_node(&mut self, node: E::NodeId) {
146 self.store.remove_node(node);
147 }
148}
149
150impl<E: Edge> EdgeSet for DagGraph<E> {
151 fn len(&self) -> usize {
152 self.store.edge_count()
153 }
154 fn active_len(&self) -> usize {
155 self.store.active_edge_count()
156 }
157 fn contains(&self, a: E::NodeId, b: E::NodeId) -> bool {
160 self.store.outgoing_active(a).any(|e| e.target() == b)
161 }
162 fn contains_archived(&self, a: E::NodeId, b: E::NodeId) -> bool {
164 self.store
165 .edges()
166 .iter()
167 .any(|e| e.source() == a && e.target() == b)
168 }
169}
170
171impl<E: Edge> Graph for DagGraph<E> {
172 type NodeId = E::NodeId;
173
174 fn add_edge(&mut self, from: E::NodeId, to: E::NodeId) -> Result<(), GraphError> {
175 self.add_edge_with_metadata(E::from_endpoints(from, to))
181 }
182
183 fn remove_edge(&mut self, from: E::NodeId, to: E::NodeId) -> Result<(), GraphError> {
184 if self.store.remove_directed_edge(from, to) {
185 Ok(())
186 } else {
187 Err(GraphError::EdgeNotFound)
188 }
189 }
190
191 fn contains_edge(&self, from: E::NodeId, to: E::NodeId) -> bool {
192 self.store.outgoing_active(from).any(|e| e.target() == to)
193 }
194}
195
196impl<E: Edge> Directed for DagGraph<E> {
197 fn outgoing(&self, node: E::NodeId) -> Vec<E::NodeId> {
198 self.store
199 .outgoing_active(node)
200 .map(|e| e.target())
201 .collect()
202 }
203
204 fn incoming(&self, node: E::NodeId) -> Vec<E::NodeId> {
205 self.store
206 .incoming_active(node)
207 .map(|e| e.source())
208 .collect()
209 }
210}
211
212#[cfg(test)]
213mod tests {
214 use super::*;
215 use crate::graph::edge::EdgeBase;
216 use uuid::Uuid;
217
218 fn ids() -> (Uuid, Uuid, Uuid) {
219 (Uuid::new_v4(), Uuid::new_v4(), Uuid::new_v4())
220 }
221
222 fn add(g: &mut DagGraph<EdgeBase>, a: Uuid, b: Uuid) -> Result<(), GraphError> {
223 g.add_edge_with_metadata(EdgeBase::new(a, b))
224 }
225
226 #[test]
227 fn test_new_dag_is_empty() {
228 let g: DagGraph<EdgeBase> = DagGraph::new();
229 assert_eq!(g.len(), 0);
230 assert_eq!(g.active_len(), 0);
231 }
232
233 #[test]
234 fn test_add_edge_with_metadata_creates_outgoing_and_incoming() {
235 let (a, b, _) = ids();
236 let mut g: DagGraph<EdgeBase> = DagGraph::new();
237 add(&mut g, a, b).unwrap();
238 assert_eq!(g.outgoing(a), vec![b]);
239 assert_eq!(g.incoming(b), vec![a]);
240 }
241
242 #[test]
243 fn test_add_edge_with_metadata_self_reference_returns_error() {
244 let (a, _, _) = ids();
245 let mut g: DagGraph<EdgeBase> = DagGraph::new();
246 assert_eq!(add(&mut g, a, a), Err(GraphError::SelfReference));
247 }
248
249 #[test]
250 fn test_add_edge_with_metadata_creating_cycle_returns_error() {
251 let (a, b, c) = ids();
252 let mut g: DagGraph<EdgeBase> = DagGraph::new();
253 add(&mut g, a, b).unwrap();
254 add(&mut g, b, c).unwrap();
255 assert_eq!(add(&mut g, c, a), Err(GraphError::Cycle));
256 }
257
258 #[test]
259 fn test_remove_edge_existing_succeeds() {
260 let (a, b, _) = ids();
261 let mut g: DagGraph<EdgeBase> = DagGraph::new();
262 add(&mut g, a, b).unwrap();
263 assert_eq!(g.remove_edge(a, b), Ok(()));
264 assert!(g.outgoing(a).is_empty());
265 }
266
267 #[test]
268 fn test_remove_edge_missing_returns_edge_not_found() {
269 let (a, b, _) = ids();
270 let mut g: DagGraph<EdgeBase> = DagGraph::new();
271 assert_eq!(g.remove_edge(a, b), Err(GraphError::EdgeNotFound));
272 }
273
274 #[test]
275 fn test_contains_edge_distinguishes_direction() {
276 let (a, b, _) = ids();
277 let mut g: DagGraph<EdgeBase> = DagGraph::new();
278 add(&mut g, a, b).unwrap();
279 assert!(g.contains_edge(a, b));
280 assert!(!g.contains_edge(b, a));
281 }
282
283 #[test]
284 fn test_archive_node_removes_from_active_view() {
285 let (a, b, _) = ids();
286 let mut g: DagGraph<EdgeBase> = DagGraph::new();
287 add(&mut g, a, b).unwrap();
288 g.archive_node(b);
289 assert!(g.outgoing(a).is_empty());
290 assert_eq!(g.len(), 1);
291 assert_eq!(g.active_len(), 0);
292 }
293
294 #[test]
295 fn test_unarchive_node_restores_active_view() {
296 let (a, b, _) = ids();
297 let mut g: DagGraph<EdgeBase> = DagGraph::new();
298 add(&mut g, a, b).unwrap();
299 g.archive_node(b);
300 g.unarchive_node(b);
301 assert_eq!(g.outgoing(a), vec![b]);
302 }
303
304 #[test]
305 fn test_remove_node_deletes_all_involved_edges() {
306 let (a, b, c) = ids();
307 let mut g: DagGraph<EdgeBase> = DagGraph::new();
308 add(&mut g, a, b).unwrap();
309 add(&mut g, b, c).unwrap();
310 g.remove_node(b);
311 assert_eq!(g.len(), 0);
312 }
313
314 #[test]
315 fn test_descendants_returns_transitive_successors() {
316 let (a, b, c) = ids();
317 let d = Uuid::new_v4();
318 let mut g: DagGraph<EdgeBase> = DagGraph::new();
319 add(&mut g, a, b).unwrap();
320 add(&mut g, b, c).unwrap();
321 add(&mut g, c, d).unwrap();
322 let mut got = g.descendants(a);
323 got.sort();
324 let mut expected = vec![b, c, d];
325 expected.sort();
326 assert_eq!(got, expected);
327 }
328
329 #[test]
330 fn test_ancestors_returns_transitive_predecessors() {
331 let (a, b, c) = ids();
332 let mut g: DagGraph<EdgeBase> = DagGraph::new();
333 add(&mut g, a, b).unwrap();
334 add(&mut g, b, c).unwrap();
335 let mut got = g.ancestors(c);
336 got.sort();
337 let mut expected = vec![a, b];
338 expected.sort();
339 assert_eq!(got, expected);
340 }
341
342 #[test]
343 fn test_add_active_duplicate_returns_duplicate_error() {
344 let (a, b, _) = ids();
345 let mut g: DagGraph<EdgeBase> = DagGraph::new();
346 add(&mut g, a, b).unwrap();
347 assert_eq!(
348 add(&mut g, a, b),
349 Err(GraphError::Duplicate),
350 "second active a->b must be rejected as duplicate"
351 );
352 assert_eq!(
353 g.outgoing(a),
354 vec![b],
355 "rejected duplicate must not appear in adjacency"
356 );
357 }
358
359 #[test]
360 fn test_add_reverse_orientation_is_not_a_duplicate() {
361 let (a, b, _) = ids();
362 let mut g: DagGraph<EdgeBase> = DagGraph::new();
363 add(&mut g, a, b).unwrap();
364 assert_eq!(
369 add(&mut g, b, a),
370 Err(GraphError::Cycle),
371 "reverse orientation is a cycle in a DAG, not a duplicate"
372 );
373 }
374
375 #[test]
376 fn test_re_add_after_archive_succeeds_and_keeps_both_records() {
377 let (a, b, _) = ids();
378 let mut g: DagGraph<EdgeBase> = DagGraph::new();
379 add(&mut g, a, b).unwrap();
380 g.archive_node(a);
381 add(&mut g, a, b).unwrap();
383 assert_eq!(g.active_len(), 1, "one fresh active edge");
384 assert_eq!(g.len(), 2, "archive record is preserved alongside");
385 }
386
387 #[test]
388 fn test_archived_edge_is_ignored_for_cycle_check() {
389 let (a, b, _) = ids();
390 let mut g: DagGraph<EdgeBase> = DagGraph::new();
391 add(&mut g, a, b).unwrap();
392 g.archive_node(a);
393 assert_eq!(add(&mut g, b, a), Ok(()));
394 }
395
396 #[test]
397 fn test_deserialize_rejects_cycle_in_active_edges() {
398 let (a, b, c) = ids();
399 let now = chrono::Utc::now();
400 let json = serde_json::json!({
401 "edges": [
402 {"source": a, "target": b, "created_at": now, "archived_at": null},
403 {"source": b, "target": c, "created_at": now, "archived_at": null},
404 {"source": c, "target": a, "created_at": now, "archived_at": null},
405 ]
406 });
407 let result: Result<DagGraph<EdgeBase>, _> = serde_json::from_value(json);
408 let err = result.expect_err("a 3-cycle must fail to deserialize");
409 assert!(
410 err.to_string().to_lowercase().contains("cycle"),
411 "deserialize error should name the cycle invariant: {err}"
412 );
413 }
414
415 #[test]
416 fn test_deserialize_rejects_self_reference() {
417 let a = Uuid::new_v4();
418 let now = chrono::Utc::now();
419 let json = serde_json::json!({
420 "edges": [
421 {"source": a, "target": a, "created_at": now, "archived_at": null},
422 ]
423 });
424 let result: Result<DagGraph<EdgeBase>, _> = serde_json::from_value(json);
425 let err = result.expect_err("self-reference must fail to deserialize");
426 assert!(
427 err.to_string().to_lowercase().contains("self"),
428 "deserialize error should name the self-reference invariant: {err}"
429 );
430 }
431
432 #[test]
440 fn test_dag_graph_works_with_non_uuid_node_id() {
441 use crate::graph::edge::EdgeBase;
442 let mut g: DagGraph<EdgeBase<u32>> = DagGraph::new();
443 g.add_edge_with_metadata(EdgeBase::new(1u32, 2u32)).unwrap();
444 g.add_edge_with_metadata(EdgeBase::new(2u32, 3u32)).unwrap();
445 assert_eq!(
446 g.add_edge_with_metadata(EdgeBase::new(3u32, 1u32)),
447 Err(GraphError::Cycle),
448 "cycle detection works over u32 node ids"
449 );
450 assert_eq!(g.outgoing(1), vec![2u32]);
451 assert_eq!(g.incoming(3), vec![2u32]);
452 let mut desc = g.descendants(1);
453 desc.sort();
454 assert_eq!(desc, vec![2u32, 3u32]);
455 }
456
457 #[test]
458 fn test_deserialize_accepts_archived_edge_completing_cycle() {
459 let (a, b, c) = ids();
460 let now = chrono::Utc::now();
461 let json = serde_json::json!({
462 "edges": [
463 {"source": a, "target": b, "created_at": now, "archived_at": null},
464 {"source": b, "target": c, "created_at": now, "archived_at": null},
465 {"source": c, "target": a, "created_at": now, "archived_at": now},
466 ]
467 });
468 let graph: DagGraph<EdgeBase> =
469 serde_json::from_value(json).expect("cycle through archived edge must be loadable");
470 assert_eq!(graph.len(), 3);
471 assert_eq!(graph.active_len(), 2);
472 }
473}