use crate::error::{DbError, Result};
use crate::util::timestamp::{self, OPEN_SENTINEL};
#[derive(Debug, Clone, PartialEq)]
pub struct EdgeAssertion {
pub source: String,
pub target: String,
pub edge_type: String,
pub valid_from: String,
pub valid_to: String,
pub weight: f64,
pub properties: String,
}
impl EdgeAssertion {
pub fn new(
source: impl Into<String>,
target: impl Into<String>,
edge_type: impl Into<String>,
) -> Self {
Self {
source: source.into(),
target: target.into(),
edge_type: edge_type.into(),
valid_from: String::new(),
valid_to: OPEN_SENTINEL.to_string(),
weight: 1.0,
properties: "{}".to_string(),
}
}
pub fn valid_from(mut self, ts: impl Into<String>) -> Self {
self.valid_from = ts.into();
self
}
pub fn valid_to(mut self, ts: impl Into<String>) -> Self {
self.valid_to = ts.into();
self
}
pub fn weight(mut self, weight: f64) -> Self {
self.weight = weight;
self
}
pub fn properties(mut self, json: impl Into<String>) -> Self {
self.properties = json.into();
self
}
pub fn normalized(mut self) -> Result<Self> {
crate::util::ids::validate_id(&self.source)?;
crate::util::ids::validate_id(&self.target)?;
validate_edge_type(&self.edge_type)?;
self.valid_from = timestamp::normalize(&self.valid_from)?;
self.valid_to = timestamp::normalize(&self.valid_to)?;
Ok(self)
}
}
pub fn validate_edge_type(edge_type: &str) -> Result<()> {
let ok = !edge_type.is_empty()
&& edge_type
.bytes()
.all(|b| b.is_ascii_uppercase() || b.is_ascii_digit());
if ok {
Ok(())
} else {
Err(DbError::InvalidEdgeType(edge_type.to_string()))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn edge_types_are_uppercase_alphanumeric() {
assert!(validate_edge_type("KNOWS").is_ok());
assert!(validate_edge_type("REL2").is_ok());
for bad in [
"",
"knows",
"KNOWS_WELL",
"KNOWS-WELL",
"A|B",
"O'BRIEN",
"ÉTAT",
] {
assert!(
validate_edge_type(bad).is_err(),
"{bad:?} should be rejected"
);
}
}
#[test]
fn normalizing_widens_timestamps_and_rejects_bad_types() {
let e = EdgeAssertion::new("a", "b", "KNOWS")
.valid_from("2026-01-01T00:00:00Z")
.normalized()
.unwrap();
assert_eq!(e.valid_from, "2026-01-01T00:00:00.000000Z");
assert_eq!(e.valid_to, OPEN_SENTINEL);
assert!(EdgeAssertion::new("a", "b", "bad")
.valid_from("2026-01-01T00:00:00.000000Z")
.normalized()
.is_err());
}
}