use std::collections::{HashMap, HashSet};
#[derive(Debug, Default, Clone)]
pub struct GraphOverlayDelta {
out_edges: HashMap<String, Vec<(String, String)>>,
in_edges: HashMap<String, Vec<(String, String)>>,
tombstones: HashSet<(String, String, String)>,
}
impl GraphOverlayDelta {
pub fn new() -> Self {
Self::default()
}
pub fn stage_edge(&mut self, src: &str, label: &str, dst: &str) {
self.out_edges
.entry(src.to_string())
.or_default()
.push((label.to_string(), dst.to_string()));
self.in_edges
.entry(dst.to_string())
.or_default()
.push((label.to_string(), src.to_string()));
}
pub fn stage_tombstone(&mut self, src: &str, label: &str, dst: &str) {
self.tombstones
.insert((src.to_string(), label.to_string(), dst.to_string()));
}
pub fn is_empty(&self) -> bool {
self.out_edges.is_empty() && self.in_edges.is_empty() && self.tombstones.is_empty()
}
pub fn out_neighbors<'a>(
&'a self,
src: &str,
label_filter: Option<&'a str>,
) -> impl Iterator<Item = (&'a str, &'a str)> {
self.out_edges.get(src).into_iter().flat_map(move |edges| {
edges
.iter()
.filter_map(move |(label, dst)| match label_filter {
Some(f) if f != label => None,
_ => Some((label.as_str(), dst.as_str())),
})
})
}
pub fn in_neighbors<'a>(
&'a self,
dst: &str,
label_filter: Option<&'a str>,
) -> impl Iterator<Item = (&'a str, &'a str)> {
self.in_edges.get(dst).into_iter().flat_map(move |edges| {
edges
.iter()
.filter_map(move |(label, src)| match label_filter {
Some(f) if f != label => None,
_ => Some((label.as_str(), src.as_str())),
})
})
}
pub fn staged_endpoint_names(&self) -> impl Iterator<Item = &str> {
self.out_edges
.keys()
.chain(self.in_edges.keys())
.map(String::as_str)
}
pub fn is_tombstoned(&self, src: &str, label: &str, dst: &str) -> bool {
self.tombstones
.contains(&(src.to_string(), label.to_string(), dst.to_string()))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_by_default() {
assert!(GraphOverlayDelta::new().is_empty());
}
#[test]
fn staged_edge_visible_both_directions() {
let mut d = GraphOverlayDelta::new();
d.stage_edge("a", "knows", "b");
assert!(!d.is_empty());
let out: Vec<_> = d.out_neighbors("a", None).collect();
assert_eq!(out, vec![("knows", "b")]);
let inn: Vec<_> = d.in_neighbors("b", None).collect();
assert_eq!(inn, vec![("knows", "a")]);
}
#[test]
fn label_filter_applied() {
let mut d = GraphOverlayDelta::new();
d.stage_edge("a", "knows", "b");
d.stage_edge("a", "likes", "c");
let out: Vec<_> = d.out_neighbors("a", Some("likes")).collect();
assert_eq!(out, vec![("likes", "c")]);
}
#[test]
fn tombstone_only_is_not_empty() {
let mut d = GraphOverlayDelta::new();
d.stage_tombstone("a", "knows", "b");
assert!(!d.is_empty());
assert!(d.is_tombstoned("a", "knows", "b"));
assert!(!d.is_tombstoned("a", "knows", "z"));
}
}