use dashmap::DashMap;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use uuid::Uuid;
pub type NodeId = Uuid;
pub type EdgeId = Uuid;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum Direction {
Outgoing,
Incoming,
Both,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Edge {
pub id: EdgeId,
pub from: NodeId,
pub to: NodeId,
pub label: String,
pub weight: f64,
#[serde(default)]
pub properties: serde_json::Value,
}
impl Edge {
#[must_use]
pub fn new(from: NodeId, to: NodeId, label: impl Into<String>) -> Self {
Self {
id: Uuid::new_v4(),
from,
to,
label: label.into(),
weight: 1.0,
properties: serde_json::Value::Null,
}
}
#[must_use]
pub fn with_weight(mut self, w: f64) -> Self {
self.weight = w;
self
}
#[must_use]
pub fn with_properties(mut self, props: serde_json::Value) -> Self {
self.properties = props;
self
}
}
#[derive(Debug, Default)]
pub struct GraphStore {
outgoing: DashMap<NodeId, Vec<Arc<Edge>>>,
incoming: DashMap<NodeId, Vec<Arc<Edge>>>,
by_id: DashMap<EdgeId, Arc<Edge>>,
}
impl GraphStore {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn add_edge(&self, edge: Edge) -> EdgeId {
let id = edge.id;
let arc = Arc::new(edge);
self.outgoing.entry(arc.from).or_default().push(arc.clone());
self.incoming.entry(arc.to).or_default().push(arc.clone());
self.by_id.insert(id, arc);
id
}
pub fn remove_edge(&self, id: EdgeId) -> bool {
let Some((_, edge)) = self.by_id.remove(&id) else {
return false;
};
if let Some(mut out) = self.outgoing.get_mut(&edge.from) {
out.retain(|e| e.id != id);
}
if let Some(mut inc) = self.incoming.get_mut(&edge.to) {
inc.retain(|e| e.id != id);
}
true
}
#[must_use]
pub fn get_edge(&self, id: EdgeId) -> Option<Arc<Edge>> {
self.by_id.get(&id).map(|e| e.value().clone())
}
#[must_use]
pub fn edge_count(&self) -> usize {
self.by_id.len()
}
#[must_use]
pub fn node_count(&self) -> usize {
let mut nodes: std::collections::HashSet<NodeId> = std::collections::HashSet::new();
for kv in self.outgoing.iter() {
nodes.insert(*kv.key());
}
for kv in self.incoming.iter() {
nodes.insert(*kv.key());
}
nodes.len()
}
#[must_use]
pub fn outgoing(&self, node: NodeId, label: Option<&str>) -> Vec<Arc<Edge>> {
self.outgoing
.get(&node)
.map(|v| filter_label(v.value(), label))
.unwrap_or_default()
}
#[must_use]
pub fn incoming(&self, node: NodeId, label: Option<&str>) -> Vec<Arc<Edge>> {
self.incoming
.get(&node)
.map(|v| filter_label(v.value(), label))
.unwrap_or_default()
}
#[must_use]
pub fn neighbors(&self, node: NodeId, dir: Direction, label: Option<&str>) -> Vec<Arc<Edge>> {
match dir {
Direction::Outgoing => self.outgoing(node, label),
Direction::Incoming => self.incoming(node, label),
Direction::Both => {
let mut out = self.outgoing(node, label);
let mut seen: std::collections::HashSet<EdgeId> = out.iter().map(|e| e.id).collect();
for e in self.incoming(node, label) {
if seen.insert(e.id) {
out.push(e);
}
}
out
}
}
}
pub fn clear(&self) {
self.outgoing.clear();
self.incoming.clear();
self.by_id.clear();
}
}
fn filter_label(edges: &[Arc<Edge>], label: Option<&str>) -> Vec<Arc<Edge>> {
match label {
Some(l) => edges.iter().filter(|e| e.label == l).cloned().collect(),
None => edges.to_vec(),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn n() -> NodeId {
Uuid::new_v4()
}
#[test]
fn add_and_lookup_edge() {
let g = GraphStore::new();
let a = n();
let b = n();
let id = g.add_edge(Edge::new(a, b, "follows"));
assert_eq!(g.edge_count(), 1);
let e = g.get_edge(id).expect("edge");
assert_eq!(e.from, a);
assert_eq!(e.to, b);
assert_eq!(e.label, "follows");
}
#[test]
fn outgoing_and_incoming_indexes_match() {
let g = GraphStore::new();
let a = n();
let b = n();
let c = n();
g.add_edge(Edge::new(a, b, "x"));
g.add_edge(Edge::new(a, c, "x"));
g.add_edge(Edge::new(b, a, "x"));
assert_eq!(g.outgoing(a, None).len(), 2);
assert_eq!(g.incoming(a, None).len(), 1);
assert_eq!(g.incoming(b, None).len(), 1);
}
#[test]
fn label_filter_applies() {
let g = GraphStore::new();
let a = n();
let b = n();
g.add_edge(Edge::new(a, b, "follows"));
g.add_edge(Edge::new(a, b, "blocks"));
assert_eq!(g.outgoing(a, Some("follows")).len(), 1);
assert_eq!(g.outgoing(a, Some("blocks")).len(), 1);
assert_eq!(g.outgoing(a, Some("missing")).len(), 0);
assert_eq!(g.outgoing(a, None).len(), 2);
}
#[test]
fn remove_edge_clears_both_indexes() {
let g = GraphStore::new();
let a = n();
let b = n();
let id = g.add_edge(Edge::new(a, b, "x"));
assert!(g.remove_edge(id));
assert_eq!(g.edge_count(), 0);
assert!(g.outgoing(a, None).is_empty());
assert!(g.incoming(b, None).is_empty());
assert!(!g.remove_edge(id), "second removal returns false");
}
#[test]
fn neighbors_both_dedups() {
let g = GraphStore::new();
let a = n();
let b = n();
g.add_edge(Edge::new(a, b, "x"));
g.add_edge(Edge::new(b, a, "x"));
let both = g.neighbors(a, Direction::Both, None);
assert_eq!(both.len(), 2);
}
#[test]
fn weight_and_properties_roundtrip() {
let g = GraphStore::new();
let a = n();
let b = n();
let id = g.add_edge(
Edge::new(a, b, "road")
.with_weight(2.5)
.with_properties(serde_json::json!({"surface": "gravel"})),
);
let e = g.get_edge(id).expect("edge");
assert!((e.weight - 2.5).abs() < f64::EPSILON);
assert_eq!(e.properties["surface"], "gravel");
}
}