Skip to main content

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}