use std::collections::{BTreeMap, BTreeSet};
use ruma::{OwnedRoomId, RoomId};
#[derive(Debug)]
struct SpaceGraphNode {
id: OwnedRoomId,
parents: BTreeSet<OwnedRoomId>,
children: BTreeSet<OwnedRoomId>,
}
impl SpaceGraphNode {
fn new(id: OwnedRoomId) -> Self {
Self { id, parents: BTreeSet::new(), children: BTreeSet::new() }
}
}
#[derive(Debug)]
pub(super) struct SpaceGraph {
nodes: BTreeMap<OwnedRoomId, SpaceGraphNode>,
}
impl Default for SpaceGraph {
fn default() -> Self {
Self::new()
}
}
impl SpaceGraph {
pub(super) fn new() -> Self {
Self { nodes: BTreeMap::new() }
}
pub(super) fn root_nodes(&self) -> Vec<&RoomId> {
self.nodes
.values()
.filter(|node| node.parents.is_empty())
.map(|node| node.id.as_ref())
.collect()
}
pub(super) fn children_of(&self, node_id: &RoomId) -> Vec<&RoomId> {
self.nodes
.get(node_id)
.map_or(vec![], |node| node.children.iter().map(|id| id.as_ref()).collect())
}
pub(super) fn parents_of(&self, node_id: &RoomId) -> Vec<&RoomId> {
self.nodes
.get(node_id)
.map_or(vec![], |node| node.parents.iter().map(|id| id.as_ref()).collect())
}
pub(super) fn add_node(&mut self, node_id: OwnedRoomId) {
self.nodes.entry(node_id.clone()).or_insert(SpaceGraphNode::new(node_id));
}
pub(super) fn has_node(&self, node_id: &RoomId) -> bool {
self.nodes.contains_key(node_id)
}
pub(super) fn add_edge(&mut self, parent_id: OwnedRoomId, child_id: OwnedRoomId) {
let parent_entry =
self.nodes.entry(parent_id.clone()).or_insert(SpaceGraphNode::new(parent_id.clone()));
parent_entry.children.insert(child_id.clone());
let child_entry =
self.nodes.entry(child_id.clone()).or_insert(SpaceGraphNode::new(child_id));
child_entry.parents.insert(parent_id);
}
pub(super) fn flattened_bottom_up_subtree(&self, node_id: &RoomId) -> Vec<OwnedRoomId> {
if !self.has_node(node_id) {
return Vec::new();
}
let mut stack = vec![node_id.to_owned()];
let mut result = Vec::new();
while let Some(node) = stack.pop() {
result.insert(0, node.clone());
if let Some(node) = self.nodes.get(&node) {
for child in &node.children {
stack.push(child.to_owned());
}
}
}
result
}
pub(super) fn remove_cycles(&mut self) {
let mut visited = BTreeSet::new();
let mut stack = BTreeSet::new();
let mut edges_to_remove = Vec::new();
for node_id in self.nodes.keys() {
self.dfs_remove_cycles(node_id, &mut visited, &mut stack, &mut edges_to_remove);
}
for (parent, child) in edges_to_remove {
if let Some(node) = self.nodes.get_mut(&parent) {
node.children.remove(&child);
}
if let Some(node) = self.nodes.get_mut(&child) {
node.parents.remove(&parent);
}
}
}
fn dfs_remove_cycles(
&self,
node_id: &OwnedRoomId,
visited: &mut BTreeSet<OwnedRoomId>,
stack: &mut BTreeSet<OwnedRoomId>,
edges_to_remove: &mut Vec<(OwnedRoomId, OwnedRoomId)>,
) {
if !visited.insert(node_id.clone()) {
return;
}
stack.insert(node_id.clone());
if let Some(node) = self.nodes.get(node_id) {
for child in &node.children {
if stack.contains(child) {
edges_to_remove.push((node_id.clone(), child.clone()));
} else {
self.dfs_remove_cycles(child, visited, stack, edges_to_remove);
}
}
}
stack.remove(node_id);
}
}
#[cfg(test)]
mod tests {
use ruma::{owned_room_id, room_id};
use super::*;
#[test]
fn test_add_edge_and_root_nodes() {
let mut graph = SpaceGraph::new();
let a = owned_room_id!("!a:example.org");
let b = owned_room_id!("!b:example.org");
let c = owned_room_id!("!c:example.org");
graph.add_edge(a.clone(), b.clone());
graph.add_edge(a.clone(), c.clone());
assert_eq!(graph.root_nodes(), vec![&a]);
assert_eq!(graph.parents_of(&b), vec![&a]);
assert_eq!(graph.parents_of(&c), vec![&a]);
assert_eq!(graph.children_of(&a), vec![&b, &c]);
}
#[test]
fn test_remove_cycles() {
let mut graph = SpaceGraph::new();
let a = owned_room_id!("!a:example.org");
let b = owned_room_id!("!b:example.org");
let c = owned_room_id!("!c:example.org");
graph.add_edge(a.clone(), b.clone());
graph.add_edge(b, c.clone());
graph.add_edge(c.clone(), a.clone());
assert!(graph.nodes[&c].children.contains(&a));
graph.remove_cycles();
assert!(!graph.nodes[&c].children.contains(&a));
assert!(!graph.nodes[&a].parents.contains(&c));
}
#[test]
fn test_disconnected_graph_roots() {
let mut graph = SpaceGraph::new();
let a = owned_room_id!("!a:example.org");
let b = owned_room_id!("!b:example.org");
graph.add_edge(a.clone(), b);
let x = owned_room_id!("!x:example.org");
let y = owned_room_id!("!y:example.org");
graph.add_edge(x.clone(), y);
let mut roots = graph.root_nodes();
roots.sort_by_key(|key| key.to_string());
let expected: Vec<&OwnedRoomId> = vec![&a, &x];
assert_eq!(roots, expected);
}
#[test]
fn test_multiple_parents() {
let mut graph = SpaceGraph::new();
let a = owned_room_id!("!a:example.org");
let b = owned_room_id!("!b:example.org");
let c = owned_room_id!("!c:example.org");
let d = owned_room_id!("!d:example.org");
graph.add_edge(a.clone(), c.clone());
graph.add_edge(b.clone(), c.clone());
graph.add_edge(c.clone(), d);
let mut roots = graph.root_nodes();
roots.sort_by_key(|key| key.to_string());
let expected = vec![&a, &b];
assert_eq!(roots, expected);
let c_parents = &graph.nodes[&c].parents;
assert!(c_parents.contains(&a));
assert!(c_parents.contains(&b));
}
#[test]
fn test_flattened_bottom_up_subtree() {
let graph = vehicle_graph();
let children = graph.flattened_bottom_up_subtree(room_id!("!personal:x.y"));
let expected = vec![
owned_room_id!("!gravel:x.y"),
owned_room_id!("!mountain:x.y"),
owned_room_id!("!road:x.y"),
owned_room_id!("!bicycle:x.y"),
owned_room_id!("!car:x.y"),
owned_room_id!("!helicopter:x.y"),
owned_room_id!("!personal:x.y"),
];
assert_eq!(children, expected);
let children = graph.flattened_bottom_up_subtree(room_id!("!shared:x.y"));
let expected = vec![
owned_room_id!("!bus:x.y"),
owned_room_id!("!train:x.y"),
owned_room_id!("!shared:x.y"),
];
assert_eq!(children, expected);
let children = graph.flattened_bottom_up_subtree(room_id!("!plane:x.y"));
let expected = vec![owned_room_id!("!plane:x.y")];
assert_eq!(children, expected);
let children = graph.flattened_bottom_up_subtree(room_id!("!floo_powder:x.y"));
assert!(children.is_empty());
}
fn vehicle_graph() -> SpaceGraph {
let mut graph = SpaceGraph::new();
graph.add_edge(owned_room_id!("!vehicles:x.y"), owned_room_id!("!shared:x.y"));
graph.add_edge(owned_room_id!("!shared:x.y"), owned_room_id!("!bus:x.y"));
graph.add_edge(owned_room_id!("!shared:x.y"), owned_room_id!("!train:x.y"));
graph.add_edge(owned_room_id!("!vehicles:x.y"), owned_room_id!("!personal:x.y"));
graph.add_edge(owned_room_id!("!personal:x.y"), owned_room_id!("!car:x.y"));
graph.add_edge(owned_room_id!("!personal:x.y"), owned_room_id!("!bicycle:x.y"));
graph.add_edge(owned_room_id!("!bicycle:x.y"), owned_room_id!("!road:x.y"));
graph.add_edge(owned_room_id!("!bicycle:x.y"), owned_room_id!("!gravel:x.y"));
graph.add_edge(owned_room_id!("!bicycle:x.y"), owned_room_id!("!mountain:x.y"));
graph.add_edge(owned_room_id!("!personal:x.y"), owned_room_id!("!helicopter:x.y"));
graph.add_edge(owned_room_id!("!vehicles:x.y"), owned_room_id!("!cargo:x.y"));
graph.add_edge(owned_room_id!("!cargo:x.y"), owned_room_id!("!plane:x.y"));
graph
}
}