synor/crates/synor-database/src/graph/edge.rs
2026-02-02 05:58:22 +05:30

321 lines
8.7 KiB
Rust

//! Graph edge (relationship) definition.
use super::node::NodeId;
use serde::{Deserialize, Serialize};
use serde_json::Value as JsonValue;
use std::sync::atomic::{AtomicU64, Ordering};
/// Unique edge identifier.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct EdgeId(pub [u8; 32]);
impl EdgeId {
/// Creates a new unique edge ID.
pub fn new() -> Self {
static COUNTER: AtomicU64 = AtomicU64::new(1);
let id = COUNTER.fetch_add(1, Ordering::SeqCst);
let mut bytes = [0u8; 32];
bytes[..8].copy_from_slice(&id.to_be_bytes());
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos() as u64;
bytes[8..16].copy_from_slice(&now.to_be_bytes());
EdgeId(*blake3::hash(&bytes).as_bytes())
}
/// Creates from raw bytes.
pub fn from_bytes(bytes: [u8; 32]) -> Self {
EdgeId(bytes)
}
/// Creates from hex string.
pub fn from_hex(hex: &str) -> Option<Self> {
let bytes = hex::decode(hex).ok()?;
if bytes.len() != 32 {
return None;
}
let mut arr = [0u8; 32];
arr.copy_from_slice(&bytes);
Some(EdgeId(arr))
}
/// Returns the bytes.
pub fn as_bytes(&self) -> &[u8; 32] {
&self.0
}
/// Converts to hex string.
pub fn to_hex(&self) -> String {
hex::encode(self.0)
}
}
impl Default for EdgeId {
fn default() -> Self {
Self::new()
}
}
impl std::fmt::Display for EdgeId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "edge_{}", hex::encode(&self.0[..8]))
}
}
/// An edge (relationship) in the graph.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Edge {
/// Unique edge ID.
pub id: EdgeId,
/// Source node ID.
pub source: NodeId,
/// Target node ID.
pub target: NodeId,
/// Edge type (relationship type), e.g., "FRIEND", "OWNS".
pub edge_type: String,
/// Properties stored as JSON.
pub properties: JsonValue,
/// Whether this is a directed edge.
pub directed: bool,
/// Weight for path-finding algorithms.
pub weight: f64,
/// Creation timestamp.
pub created_at: u64,
}
impl Edge {
/// Creates a new directed edge.
pub fn new(
source: NodeId,
target: NodeId,
edge_type: impl Into<String>,
properties: JsonValue,
) -> Self {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_millis() as u64;
Self {
id: EdgeId::new(),
source,
target,
edge_type: edge_type.into(),
properties,
directed: true,
weight: 1.0,
created_at: now,
}
}
/// Creates an undirected edge.
pub fn undirected(
source: NodeId,
target: NodeId,
edge_type: impl Into<String>,
properties: JsonValue,
) -> Self {
let mut edge = Self::new(source, target, edge_type, properties);
edge.directed = false;
edge
}
/// Sets the weight for this edge.
pub fn with_weight(mut self, weight: f64) -> Self {
self.weight = weight;
self
}
/// Returns the other end of this edge from the given node.
pub fn other_end(&self, from: &NodeId) -> Option<NodeId> {
if &self.source == from {
Some(self.target)
} else if &self.target == from && !self.directed {
Some(self.source)
} else if &self.target == from {
// For directed edges, can't traverse backward
None
} else {
None
}
}
/// Checks if this edge connects the given node (as source or target).
pub fn connects(&self, node: &NodeId) -> bool {
&self.source == node || &self.target == node
}
/// Checks if this edge connects two specific nodes.
pub fn connects_pair(&self, a: &NodeId, b: &NodeId) -> bool {
(&self.source == a && &self.target == b)
|| (!self.directed && &self.source == b && &self.target == a)
}
/// Gets a property value.
pub fn get_property(&self, key: &str) -> Option<&JsonValue> {
self.properties.get(key)
}
/// Sets a property value.
pub fn set_property(&mut self, key: &str, value: JsonValue) {
if let Some(obj) = self.properties.as_object_mut() {
obj.insert(key.to_string(), value);
}
}
/// Checks if the edge matches a property filter.
pub fn matches_properties(&self, filter: &JsonValue) -> bool {
if let (Some(filter_obj), Some(props_obj)) =
(filter.as_object(), self.properties.as_object())
{
for (key, expected) in filter_obj {
if let Some(actual) = props_obj.get(key) {
if actual != expected {
return false;
}
} else {
return false;
}
}
true
} else {
filter == &self.properties || filter == &JsonValue::Object(serde_json::Map::new())
}
}
}
/// Builder for creating edges.
pub struct EdgeBuilder {
source: NodeId,
target: NodeId,
edge_type: String,
properties: serde_json::Map<String, JsonValue>,
directed: bool,
weight: f64,
}
impl EdgeBuilder {
/// Creates a new edge builder.
pub fn new(source: NodeId, target: NodeId, edge_type: impl Into<String>) -> Self {
Self {
source,
target,
edge_type: edge_type.into(),
properties: serde_json::Map::new(),
directed: true,
weight: 1.0,
}
}
/// Sets the edge as undirected.
pub fn undirected(mut self) -> Self {
self.directed = false;
self
}
/// Sets the weight.
pub fn weight(mut self, weight: f64) -> Self {
self.weight = weight;
self
}
/// Sets a property.
pub fn property(mut self, key: impl Into<String>, value: impl Into<JsonValue>) -> Self {
self.properties.insert(key.into(), value.into());
self
}
/// Builds the edge.
pub fn build(self) -> Edge {
let mut edge = Edge::new(
self.source,
self.target,
self.edge_type,
JsonValue::Object(self.properties),
);
edge.directed = self.directed;
edge.weight = self.weight;
edge
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_edge_id() {
let id1 = EdgeId::new();
let id2 = EdgeId::new();
assert_ne!(id1, id2);
let hex = id1.to_hex();
let id3 = EdgeId::from_hex(&hex).unwrap();
assert_eq!(id1, id3);
}
#[test]
fn test_edge_creation() {
let source = NodeId::new();
let target = NodeId::new();
let edge = Edge::new(source, target, "FRIEND", serde_json::json!({"since": 2020}));
assert_eq!(edge.source, source);
assert_eq!(edge.target, target);
assert_eq!(edge.edge_type, "FRIEND");
assert!(edge.directed);
}
#[test]
fn test_edge_builder() {
let source = NodeId::new();
let target = NodeId::new();
let edge = EdgeBuilder::new(source, target, "OWNS")
.undirected()
.weight(2.5)
.property("percentage", 50)
.build();
assert!(!edge.directed);
assert_eq!(edge.weight, 2.5);
assert_eq!(
edge.get_property("percentage"),
Some(&serde_json::json!(50))
);
}
#[test]
fn test_edge_other_end() {
let source = NodeId::new();
let target = NodeId::new();
// Directed edge
let directed = Edge::new(source, target, "A", serde_json::json!({}));
assert_eq!(directed.other_end(&source), Some(target));
assert_eq!(directed.other_end(&target), None); // Can't traverse backward
// Undirected edge
let undirected = Edge::undirected(source, target, "B", serde_json::json!({}));
assert_eq!(undirected.other_end(&source), Some(target));
assert_eq!(undirected.other_end(&target), Some(source));
}
#[test]
fn test_edge_connects() {
let a = NodeId::new();
let b = NodeId::new();
let c = NodeId::new();
let edge = Edge::new(a, b, "LINK", serde_json::json!({}));
assert!(edge.connects(&a));
assert!(edge.connects(&b));
assert!(!edge.connects(&c));
assert!(edge.connects_pair(&a, &b));
assert!(!edge.connects_pair(&b, &a)); // Directed
}
}