321 lines
8.7 KiB
Rust
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
|
|
}
|
|
}
|