Skip to main content

kanban_core/graph/
edge.rs

1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3use uuid::Uuid;
4
5/// Direction of an edge in the graph
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
7pub enum EdgeDirection {
8    /// A -> B (source points to target, one-way relationship)
9    Directed,
10    /// A <-> B (bidirectional relationship)
11    Bidirectional,
12}
13
14/// A weighted, typed edge between two nodes
15///
16/// Generic over edge type `E` to support different relationship types
17/// (e.g., CardEdgeType::Blocks, CardEdgeType::RelatesTo, etc.)
18#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct Edge<E> {
20    /// Source node identifier
21    pub source: Uuid,
22    /// Target node identifier
23    pub target: Uuid,
24    /// Type of relationship (e.g., Blocks, RelatesTo)
25    pub edge_type: E,
26    /// Direction of the edge
27    pub direction: EdgeDirection,
28    /// Optional weight for weighted graph algorithms
29    pub weight: Option<f32>,
30    /// When this edge was created
31    pub created_at: DateTime<Utc>,
32    /// When this edge was archived (None = active)
33    pub archived_at: Option<DateTime<Utc>>,
34}
35
36impl<E> Edge<E> {
37    /// Create a new edge
38    pub fn new(source: Uuid, target: Uuid, edge_type: E, direction: EdgeDirection) -> Self {
39        Self {
40            source,
41            target,
42            edge_type,
43            direction,
44            weight: None,
45            created_at: Utc::now(),
46            archived_at: None,
47        }
48    }
49
50    /// Check if this edge is archived
51    pub fn is_archived(&self) -> bool {
52        self.archived_at.is_some()
53    }
54
55    /// Check if this edge is active (not archived)
56    pub fn is_active(&self) -> bool {
57        self.archived_at.is_none()
58    }
59
60    /// Archive this edge
61    pub fn archive(&mut self) {
62        if self.archived_at.is_none() {
63            self.archived_at = Some(Utc::now());
64        }
65    }
66
67    /// Unarchive this edge
68    pub fn unarchive(&mut self) {
69        self.archived_at = None;
70    }
71
72    /// Check if this edge involves a given node (source or target)
73    pub fn involves(&self, node_id: Uuid) -> bool {
74        self.source == node_id || self.target == node_id
75    }
76
77    /// Check if this edge connects two specific nodes (in either direction for bidirectional)
78    pub fn connects(&self, node_a: Uuid, node_b: Uuid) -> bool {
79        match self.direction {
80            EdgeDirection::Directed => self.source == node_a && self.target == node_b,
81            EdgeDirection::Bidirectional => {
82                (self.source == node_a && self.target == node_b)
83                    || (self.source == node_b && self.target == node_a)
84            }
85        }
86    }
87}
88
89#[cfg(test)]
90mod tests {
91    use super::*;
92
93    #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
94    enum TestEdgeType {
95        TypeA,
96        TypeB,
97    }
98
99    #[test]
100    fn test_edge_creation() {
101        let source = Uuid::new_v4();
102        let target = Uuid::new_v4();
103        let edge = Edge::new(source, target, TestEdgeType::TypeA, EdgeDirection::Directed);
104
105        assert_eq!(edge.source, source);
106        assert_eq!(edge.target, target);
107        assert!(edge.is_active());
108        assert!(!edge.is_archived());
109    }
110
111    #[test]
112    fn test_edge_archive() {
113        let source = Uuid::new_v4();
114        let target = Uuid::new_v4();
115        let mut edge = Edge::new(source, target, TestEdgeType::TypeA, EdgeDirection::Directed);
116
117        edge.archive();
118        assert!(edge.is_archived());
119        assert!(!edge.is_active());
120
121        edge.unarchive();
122        assert!(edge.is_active());
123        assert!(!edge.is_archived());
124    }
125
126    #[test]
127    fn test_edge_involves() {
128        let source = Uuid::new_v4();
129        let target = Uuid::new_v4();
130        let other = Uuid::new_v4();
131        let edge = Edge::new(source, target, TestEdgeType::TypeA, EdgeDirection::Directed);
132
133        assert!(edge.involves(source));
134        assert!(edge.involves(target));
135        assert!(!edge.involves(other));
136    }
137
138    #[test]
139    fn test_edge_connects_directed() {
140        let source = Uuid::new_v4();
141        let target = Uuid::new_v4();
142        let edge = Edge::new(source, target, TestEdgeType::TypeA, EdgeDirection::Directed);
143
144        assert!(edge.connects(source, target));
145        assert!(!edge.connects(target, source));
146    }
147
148    #[test]
149    fn test_edge_connects_bidirectional() {
150        let node_a = Uuid::new_v4();
151        let node_b = Uuid::new_v4();
152        let edge = Edge::new(
153            node_a,
154            node_b,
155            TestEdgeType::TypeA,
156            EdgeDirection::Bidirectional,
157        );
158
159        assert!(edge.connects(node_a, node_b));
160        assert!(edge.connects(node_b, node_a));
161    }
162}