kanban_core/graph/traits.rs
1use uuid::Uuid;
2
3use super::error::GraphError;
4
5// Note on the `Uuid` import: only the object-safety smoke tests use
6// it as a concrete binding for the `NodeId` associated type. The
7// traits themselves are now fully generic over `NodeId`.
8
9/// Trait for entities that can participate in a graph
10///
11/// Implemented by domain entities like Card, Sprint, Board, etc.
12/// Provides a unique identifier for graph node operations.
13pub trait GraphNode {
14 /// Get the unique identifier for this node
15 fn node_id(&self) -> Uuid;
16}
17
18/// Minimal contract every graph data structure satisfies.
19///
20/// Direction-agnostic: only methods that make sense for both directed
21/// and undirected graphs live here. Directional access (`outgoing` /
22/// `incoming`) lives on [`Directed`]; undirected neighbour access lives
23/// on [`Undirected`]. A type that needs both must pick the right
24/// subtype — there is no way to ask an [`Undirected`] graph for
25/// directed semantics and silently get a wrong answer.
26///
27/// Object-safe with an explicit `NodeId` binding, e.g.
28/// `&dyn Graph<NodeId = Uuid>`.
29pub trait Graph {
30 type NodeId: Copy + Eq + std::hash::Hash;
31
32 /// Add an edge `from -> to`. Returns [`GraphError::Cycle`] /
33 /// [`GraphError::SelfReference`] when the implementation rejects.
34 fn add_edge(&mut self, from: Self::NodeId, to: Self::NodeId) -> Result<(), GraphError>;
35
36 /// Remove the edge `from -> to`. Returns [`GraphError::EdgeNotFound`]
37 /// if no such edge exists.
38 fn remove_edge(&mut self, from: Self::NodeId, to: Self::NodeId) -> Result<(), GraphError>;
39
40 /// True iff an edge `from -> to` is present.
41 fn contains_edge(&self, from: Self::NodeId, to: Self::NodeId) -> bool;
42}
43
44/// Directed-graph vocabulary: distinct outgoing and incoming neighbour
45/// sets. Implementations preserve edge direction; calling
46/// `outgoing(node)` returns successors, `incoming(node)` returns
47/// predecessors.
48///
49/// Only types that genuinely encode direction implement this. An
50/// [`Undirected`] graph cannot accidentally satisfy this trait by
51/// returning the same set for both methods.
52pub trait Directed: Graph {
53 fn outgoing(&self, node: Self::NodeId) -> Vec<Self::NodeId>;
54 fn incoming(&self, node: Self::NodeId) -> Vec<Self::NodeId>;
55}
56
57/// Undirected-graph vocabulary: a single neighbour set per node.
58/// Calling `neighbors(node)` returns every node connected to `node` by
59/// any edge, ignoring orientation. Implementations that genuinely lack
60/// edge direction expose only this surface — directed callers must
61/// require [`Directed`] explicitly.
62pub trait Undirected: Graph {
63 fn neighbors(&self, node: Self::NodeId) -> Vec<Self::NodeId>;
64}
65
66/// Node-keyed cascade operations: archive, unarchive, and hard-remove
67/// every edge involving a given node.
68///
69/// Strictly mutating. Distinct from [`EdgeSet`] (which is read-only)
70/// because the two surfaces are independent — a caller that just
71/// counts edges doesn't need cascade authority, and a caller doing
72/// soft-deletes doesn't need to ask for counts. Splitting them keeps
73/// each trait's name accurate to its single purpose.
74///
75/// Generic over `Graph::NodeId` — no longer pinned to `Uuid`. The
76/// kanban domain still uses `Uuid` for every edge today; a future
77/// heterogeneous-entity graph can pick another identity (e.g. a
78/// discriminated `(EntityKind, Uuid)` newtype) without touching this
79/// trait or its callers.
80pub trait Cascadable: Graph {
81 /// Archive every edge involving `node` (soft delete).
82 fn archive_node(&mut self, node: Self::NodeId);
83 /// Unarchive every edge involving `node`.
84 fn unarchive_node(&mut self, node: Self::NodeId);
85 /// Remove every edge involving `node` (hard delete).
86 fn remove_node(&mut self, node: Self::NodeId);
87}
88
89/// Set-like read access over a graph's edges: size, active-only size,
90/// and membership.
91///
92/// Aligns with stdlib `HashSet` / `BTreeSet` vocabulary — `len`,
93/// `contains` — so a reader who has seen those once knows the shape
94/// of this trait immediately. Strictly read-only; lives separately
95/// from [`Cascadable`] so a generic consumer can require only the
96/// surface it actually uses, with no implicit mutation authority
97/// leaking into inspection code.
98///
99/// Generic over `Graph::NodeId`, same reasoning as `Cascadable`.
100///
101/// # Active vs archived
102///
103/// `is_empty` and `contains` both consult the **active** subset only,
104/// matching `Graph::contains_edge` and the principle of least
105/// surprise: a caller asking "is this here right now?" wants the
106/// current view, not the archive log. `contains_archived` is the
107/// explicit escape hatch for callers that need to consult both. The
108/// (active + archived) total is still available via `len`.
109pub trait EdgeSet: Graph {
110 /// Total edge count (active + archived).
111 fn len(&self) -> usize;
112 /// Active edge count only.
113 fn active_len(&self) -> usize;
114 /// True iff this set has no active edges. Archived edges, if any,
115 /// are not counted: a graph whose entire active set has been
116 /// archived reports `is_empty() == true`, matching what callers
117 /// asking "anything here right now?" expect.
118 fn is_empty(&self) -> bool {
119 self.active_len() == 0
120 }
121 /// True iff an **active** edge between `a` and `b` exists in this
122 /// set. Aligned with `Graph::contains_edge` semantics.
123 fn contains(&self, a: Self::NodeId, b: Self::NodeId) -> bool;
124 /// True iff any edge between `a` and `b` exists in this set,
125 /// **including archived ones**. Use when reasoning about edge
126 /// history; prefer `contains` for the current view.
127 fn contains_archived(&self, a: Self::NodeId, b: Self::NodeId) -> bool;
128}
129
130#[cfg(test)]
131mod tests {
132 use super::*;
133
134 #[test]
135 fn test_graph_trait_is_object_safe_for_uuid_nodes() {
136 fn _accepts_dyn(_: &dyn Graph<NodeId = Uuid>) {}
137 }
138
139 #[test]
140 fn test_directed_trait_is_object_safe() {
141 fn _accepts_dyn(_: &dyn Directed<NodeId = Uuid>) {}
142 }
143
144 #[test]
145 fn test_undirected_trait_is_object_safe() {
146 fn _accepts_dyn(_: &dyn Undirected<NodeId = Uuid>) {}
147 }
148
149 #[test]
150 fn test_cascadable_trait_is_object_safe() {
151 fn _accepts_dyn(_: &dyn Cascadable<NodeId = Uuid>) {}
152 fn _accepts_dyn_mut(_: &mut dyn Cascadable<NodeId = Uuid>) {}
153 }
154
155 #[test]
156 fn test_edge_set_trait_is_object_safe() {
157 fn _accepts_dyn(_: &dyn EdgeSet<NodeId = Uuid>) {}
158 }
159}