1use crate::error::{DbError, Result};
2use crate::util::timestamp::{self, OPEN_SENTINEL};
3
4#[derive(Debug, Clone, PartialEq)]
6pub struct EdgeAssertion {
7 pub source: String,
8 pub target: String,
9 pub edge_type: String,
10 pub valid_from: String,
11 pub valid_to: String,
12 pub weight: f64,
13 pub properties: String,
14}
15
16impl EdgeAssertion {
17 pub fn new(
18 source: impl Into<String>,
19 target: impl Into<String>,
20 edge_type: impl Into<String>,
21 ) -> Self {
22 Self {
23 source: source.into(),
24 target: target.into(),
25 edge_type: edge_type.into(),
26 valid_from: String::new(),
27 valid_to: OPEN_SENTINEL.to_string(),
28 weight: 1.0,
29 properties: "{}".to_string(),
30 }
31 }
32
33 pub fn valid_from(mut self, ts: impl Into<String>) -> Self {
35 self.valid_from = ts.into();
36 self
37 }
38
39 pub fn valid_to(mut self, ts: impl Into<String>) -> Self {
41 self.valid_to = ts.into();
42 self
43 }
44
45 pub fn weight(mut self, weight: f64) -> Self {
46 self.weight = weight;
47 self
48 }
49
50 pub fn properties(mut self, json: impl Into<String>) -> Self {
51 self.properties = json.into();
52 self
53 }
54
55 pub fn normalized(mut self) -> Result<Self> {
62 crate::util::ids::validate_id(&self.source)?;
65 crate::util::ids::validate_id(&self.target)?;
66 validate_edge_type(&self.edge_type)?;
67 self.valid_from = timestamp::normalize(&self.valid_from)?;
68 self.valid_to = timestamp::normalize(&self.valid_to)?;
69 Ok(self)
70 }
71}
72
73pub fn validate_edge_type(edge_type: &str) -> Result<()> {
86 let ok = !edge_type.is_empty()
87 && edge_type
88 .bytes()
89 .all(|b| b.is_ascii_uppercase() || b.is_ascii_digit());
90 if ok {
91 Ok(())
92 } else {
93 Err(DbError::InvalidEdgeType(edge_type.to_string()))
94 }
95}
96
97#[cfg(test)]
98mod tests {
99 use super::*;
100
101 #[test]
102 fn edge_types_are_uppercase_alphanumeric() {
103 assert!(validate_edge_type("KNOWS").is_ok());
104 assert!(validate_edge_type("REL2").is_ok());
105
106 for bad in [
107 "",
108 "knows",
109 "KNOWS_WELL",
110 "KNOWS-WELL",
111 "A|B",
112 "O'BRIEN",
113 "ÉTAT",
114 ] {
115 assert!(
116 validate_edge_type(bad).is_err(),
117 "{bad:?} should be rejected"
118 );
119 }
120 }
121
122 #[test]
123 fn normalizing_widens_timestamps_and_rejects_bad_types() {
124 let e = EdgeAssertion::new("a", "b", "KNOWS")
125 .valid_from("2026-01-01T00:00:00Z")
126 .normalized()
127 .unwrap();
128 assert_eq!(e.valid_from, "2026-01-01T00:00:00.000000Z");
129 assert_eq!(e.valid_to, OPEN_SENTINEL);
130
131 assert!(EdgeAssertion::new("a", "b", "bad")
132 .valid_from("2026-01-01T00:00:00.000000Z")
133 .normalized()
134 .is_err());
135 }
136}