matrix_sdk_ui/spaces/
graph.rs1use std::collections::{BTreeMap, BTreeSet};
16
17use ruma::{OwnedRoomId, RoomId};
18
19#[derive(Debug)]
20struct SpaceGraphNode {
21 id: OwnedRoomId,
22 parents: BTreeSet<OwnedRoomId>,
23 children: BTreeSet<OwnedRoomId>,
24}
25
26impl SpaceGraphNode {
27 fn new(id: OwnedRoomId) -> Self {
28 Self { id, parents: BTreeSet::new(), children: BTreeSet::new() }
29 }
30}
31
32#[derive(Debug)]
36pub(super) struct SpaceGraph {
37 nodes: BTreeMap<OwnedRoomId, SpaceGraphNode>,
38}
39
40impl Default for SpaceGraph {
41 fn default() -> Self {
42 Self::new()
43 }
44}
45
46impl SpaceGraph {
47 pub(super) fn new() -> Self {
49 Self { nodes: BTreeMap::new() }
50 }
51
52 pub(super) fn root_nodes(&self) -> Vec<&RoomId> {
55 self.nodes
56 .values()
57 .filter(|node| node.parents.is_empty())
58 .map(|node| node.id.as_ref())
59 .collect()
60 }
61
62 pub(super) fn children_of(&self, node_id: &RoomId) -> Vec<&RoomId> {
65 self.nodes
66 .get(node_id)
67 .map_or(vec![], |node| node.children.iter().map(|id| id.as_ref()).collect())
68 }
69
70 pub(super) fn add_node(&mut self, node_id: OwnedRoomId) {
72 self.nodes.entry(node_id.clone()).or_insert(SpaceGraphNode::new(node_id));
73 }
74
75 pub(super) fn has_node(&self, node_id: &RoomId) -> bool {
77 self.nodes.contains_key(node_id)
78 }
79
80 pub(super) fn add_edge(&mut self, parent_id: OwnedRoomId, child_id: OwnedRoomId) {
83 let parent_entry =
84 self.nodes.entry(parent_id.clone()).or_insert(SpaceGraphNode::new(parent_id.clone()));
85 parent_entry.children.insert(child_id.clone());
86
87 let child_entry =
88 self.nodes.entry(child_id.clone()).or_insert(SpaceGraphNode::new(child_id));
89 child_entry.parents.insert(parent_id);
90 }
91
92 pub(super) fn flattened_bottom_up_subtree(&self, node_id: &RoomId) -> Vec<OwnedRoomId> {
97 if !self.has_node(node_id) {
98 return Vec::new();
99 }
100
101 let mut stack = vec![node_id.to_owned()];
102 let mut result = Vec::new();
103
104 while let Some(node) = stack.pop() {
105 result.insert(0, node.clone());
106
107 if let Some(node) = self.nodes.get(&node) {
108 for child in &node.children {
109 stack.push(child.to_owned());
110 }
111 }
112 }
113
114 result
115 }
116
117 pub(super) fn remove_cycles(&mut self) {
121 let mut visited = BTreeSet::new();
122 let mut stack = BTreeSet::new();
123
124 let mut edges_to_remove = Vec::new();
125
126 for node_id in self.nodes.keys() {
127 self.dfs_remove_cycles(node_id, &mut visited, &mut stack, &mut edges_to_remove);
128 }
129
130 for (parent, child) in edges_to_remove {
131 if let Some(node) = self.nodes.get_mut(&parent) {
132 node.children.remove(&child);
133 }
134 if let Some(node) = self.nodes.get_mut(&child) {
135 node.parents.remove(&parent);
136 }
137 }
138 }
139
140 fn dfs_remove_cycles(
141 &self,
142 node_id: &OwnedRoomId,
143 visited: &mut BTreeSet<OwnedRoomId>,
144 stack: &mut BTreeSet<OwnedRoomId>,
145 edges_to_remove: &mut Vec<(OwnedRoomId, OwnedRoomId)>,
146 ) {
147 if !visited.insert(node_id.clone()) {
148 return;
149 }
150
151 stack.insert(node_id.clone());
152
153 if let Some(node) = self.nodes.get(node_id) {
154 for child in &node.children {
155 if stack.contains(child) {
156 edges_to_remove.push((node_id.clone(), child.clone()));
158 } else {
159 self.dfs_remove_cycles(child, visited, stack, edges_to_remove);
160 }
161 }
162 }
163
164 stack.remove(node_id);
165 }
166}
167
168#[cfg(test)]
169mod tests {
170 use ruma::{owned_room_id, room_id};
171
172 use super::*;
173
174 #[test]
175 fn test_add_edge_and_root_nodes() {
176 let mut graph = SpaceGraph::new();
177
178 let a = room_id!("!a:example.org").to_owned();
179 let b = room_id!("!b:example.org").to_owned();
180 let c = room_id!("!c:example.org").to_owned();
181
182 graph.add_edge(a.clone(), b.clone());
183 graph.add_edge(a.clone(), c.clone());
184
185 assert_eq!(graph.root_nodes(), vec![&a]);
186
187 assert!(graph.nodes[&b].parents.contains(&a));
188 assert!(graph.nodes[&c].parents.contains(&a));
189
190 assert_eq!(graph.children_of(&a), vec![&b, &c]);
191 }
192
193 #[test]
194 fn test_remove_cycles() {
195 let mut graph = SpaceGraph::new();
196
197 let a = room_id!("!a:example.org").to_owned();
198 let b = room_id!("!b:example.org").to_owned();
199 let c = room_id!("!c:example.org").to_owned();
200
201 graph.add_edge(a.clone(), b.clone());
202 graph.add_edge(b, c.clone());
203 graph.add_edge(c.clone(), a.clone()); assert!(graph.nodes[&c].children.contains(&a));
206
207 graph.remove_cycles();
208
209 assert!(!graph.nodes[&c].children.contains(&a));
210 assert!(!graph.nodes[&a].parents.contains(&c));
211 }
212
213 #[test]
214 fn test_disconnected_graph_roots() {
215 let mut graph = SpaceGraph::new();
216
217 let a = room_id!("!a:example.org").to_owned();
218 let b = room_id!("!b:example.org").to_owned();
219 graph.add_edge(a.clone(), b);
220
221 let x = room_id!("!x:example.org").to_owned();
222 let y = room_id!("!y:example.org").to_owned();
223 graph.add_edge(x.clone(), y);
224
225 let mut roots = graph.root_nodes();
226 roots.sort_by_key(|key| key.to_string());
227
228 let expected: Vec<&OwnedRoomId> = vec![&a, &x];
229 assert_eq!(roots, expected);
230 }
231
232 #[test]
233 fn test_multiple_parents() {
234 let mut graph = SpaceGraph::new();
235
236 let a = room_id!("!a:example.org").to_owned();
237 let b = room_id!("!b:example.org").to_owned();
238 let c = room_id!("!c:example.org").to_owned();
239 let d = room_id!("!d:example.org").to_owned();
240
241 graph.add_edge(a.clone(), c.clone());
242 graph.add_edge(b.clone(), c.clone());
243 graph.add_edge(c.clone(), d);
244
245 let mut roots = graph.root_nodes();
246 roots.sort_by_key(|key| key.to_string());
247
248 let expected = vec![&a, &b];
249 assert_eq!(roots, expected);
250
251 let c_parents = &graph.nodes[&c].parents;
252 assert!(c_parents.contains(&a));
253 assert!(c_parents.contains(&b));
254 }
255
256 #[test]
257 fn test_flattened_bottom_up_subtree() {
258 let graph = vehicle_graph();
259
260 let children = graph.flattened_bottom_up_subtree(room_id!("!personal:x.y"));
261 let expected = vec![
262 owned_room_id!("!gravel:x.y"),
263 owned_room_id!("!mountain:x.y"),
264 owned_room_id!("!road:x.y"),
265 owned_room_id!("!bicycle:x.y"),
266 owned_room_id!("!car:x.y"),
267 owned_room_id!("!helicopter:x.y"),
268 owned_room_id!("!personal:x.y"),
269 ];
270
271 assert_eq!(children, expected);
272
273 let children = graph.flattened_bottom_up_subtree(room_id!("!shared:x.y"));
274 let expected = vec![
275 owned_room_id!("!bus:x.y"),
276 owned_room_id!("!train:x.y"),
277 owned_room_id!("!shared:x.y"),
278 ];
279
280 assert_eq!(children, expected);
281
282 let children = graph.flattened_bottom_up_subtree(room_id!("!plane:x.y"));
283 let expected = vec![owned_room_id!("!plane:x.y")];
284
285 assert_eq!(children, expected);
286
287 let children = graph.flattened_bottom_up_subtree(room_id!("!floo_powder:x.y"));
288 assert!(children.is_empty());
289 }
290
291 fn vehicle_graph() -> SpaceGraph {
292 let mut graph = SpaceGraph::new();
307
308 graph.add_edge(owned_room_id!("!vehicles:x.y"), owned_room_id!("!shared:x.y"));
309 graph.add_edge(owned_room_id!("!shared:x.y"), owned_room_id!("!bus:x.y"));
310 graph.add_edge(owned_room_id!("!shared:x.y"), owned_room_id!("!train:x.y"));
311
312 graph.add_edge(owned_room_id!("!vehicles:x.y"), owned_room_id!("!personal:x.y"));
313 graph.add_edge(owned_room_id!("!personal:x.y"), owned_room_id!("!car:x.y"));
314
315 graph.add_edge(owned_room_id!("!personal:x.y"), owned_room_id!("!bicycle:x.y"));
316 graph.add_edge(owned_room_id!("!bicycle:x.y"), owned_room_id!("!road:x.y"));
317 graph.add_edge(owned_room_id!("!bicycle:x.y"), owned_room_id!("!gravel:x.y"));
318 graph.add_edge(owned_room_id!("!bicycle:x.y"), owned_room_id!("!mountain:x.y"));
319
320 graph.add_edge(owned_room_id!("!personal:x.y"), owned_room_id!("!helicopter:x.y"));
321
322 graph.add_edge(owned_room_id!("!vehicles:x.y"), owned_room_id!("!cargo:x.y"));
323 graph.add_edge(owned_room_id!("!cargo:x.y"), owned_room_id!("!plane:x.y"));
324
325 graph
326 }
327}