1use std::fmt;
6use std::str::FromStr;
7
8use serde::{Deserialize, Serialize};
9
10use super::error::{IdError, validate};
11use super::node::NodeId;
12
13#[derive(
29 Debug,
30 Clone,
31 PartialEq,
32 Eq,
33 Hash,
34 Serialize,
35 Deserialize,
36 zerompk::ToMessagePack,
37 zerompk::FromMessagePack,
38 rkyv::Archive,
39 rkyv::Serialize,
40 rkyv::Deserialize,
41)]
42pub struct EdgeId {
43 pub src: NodeId,
45 pub dst: NodeId,
47 pub label: String,
49 pub seq: u64,
55}
56
57impl EdgeId {
58 pub fn try_first(src: NodeId, dst: NodeId, label: impl Into<String>) -> Result<Self, IdError> {
65 let label = label.into();
66 validate(&label)?;
67 Ok(Self {
68 src,
69 dst,
70 label,
71 seq: 0,
72 })
73 }
74
75 pub fn try_with_seq(
80 src: NodeId,
81 dst: NodeId,
82 label: impl Into<String>,
83 seq: u64,
84 ) -> Result<Self, IdError> {
85 let label = label.into();
86 validate(&label)?;
87 Ok(Self {
88 src,
89 dst,
90 label,
91 seq,
92 })
93 }
94}
95
96#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
98pub enum EdgeIdParseError {
99 #[error(
100 "missing segment: expected format '{{src_len}}:{{src}}|{{label_len}}:{{label}}|{{dst_len}}:{{dst}}|{{seq}}'"
101 )]
102 MissingSegment,
103 #[error("invalid length prefix in segment '{segment}': {reason}")]
104 InvalidLengthPrefix { segment: String, reason: String },
105 #[error("invalid seq value '{value}': {reason}")]
106 InvalidSeq { value: String, reason: String },
107 #[error("invalid label: {0}")]
109 InvalidLabel(IdError),
110}
111
112impl fmt::Display for EdgeId {
113 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
117 let src = self.src.as_str();
118 let label = &self.label;
119 let dst = self.dst.as_str();
120 write!(
121 f,
122 "{}:{}|{}:{}|{}:{}|{}",
123 src.len(),
124 src,
125 label.len(),
126 label,
127 dst.len(),
128 dst,
129 self.seq
130 )
131 }
132}
133
134impl FromStr for EdgeId {
135 type Err = EdgeIdParseError;
136
137 fn from_str(s: &str) -> Result<Self, Self::Err> {
138 fn take_segment(input: &str, separator: char) -> Result<(&str, &str), EdgeIdParseError> {
141 let colon = input.find(':').ok_or(EdgeIdParseError::MissingSegment)?;
142 let len_str = &input[..colon];
143 let len: usize =
144 len_str
145 .parse()
146 .map_err(|e| EdgeIdParseError::InvalidLengthPrefix {
147 segment: len_str.to_owned(),
148 reason: format!("{e}"),
149 })?;
150 let after_colon = &input[colon + 1..];
151 if after_colon.len() < len {
152 return Err(EdgeIdParseError::MissingSegment);
153 }
154 let value = &after_colon[..len];
155 let rest = &after_colon[len..];
156 let rest = if rest.starts_with(separator) {
158 &rest[1..]
159 } else if rest.is_empty() {
160 rest
161 } else {
162 return Err(EdgeIdParseError::MissingSegment);
163 };
164 Ok((value, rest))
165 }
166
167 let (src, rest) = take_segment(s, '|')?;
168 let (label, rest) = take_segment(rest, '|')?;
169 let (dst, rest) = take_segment(rest, '|')?;
170
171 let seq: u64 = rest.parse().map_err(|e| EdgeIdParseError::InvalidSeq {
173 value: rest.to_owned(),
174 reason: format!("{e}"),
175 })?;
176
177 validate(label).map_err(EdgeIdParseError::InvalidLabel)?;
179
180 Ok(EdgeId {
181 src: NodeId::from_validated(src.to_owned()),
183 dst: NodeId::from_validated(dst.to_owned()),
184 label: label.to_owned(),
185 seq,
186 })
187 }
188}
189
190#[cfg(test)]
191mod tests {
192 use super::super::error::ID_MAX_LEN;
193 use super::*;
194
195 #[test]
196 fn try_first_accepts_valid() {
197 let src = NodeId::try_new("alice").expect("valid");
198 let dst = NodeId::try_new("bob").expect("valid");
199 let e = EdgeId::try_first(src, dst, "KNOWS").expect("valid label");
200 assert_eq!(e.seq, 0);
201 assert_eq!(e.label, "KNOWS");
202 }
203
204 #[test]
205 fn edge_id_label_validation_propagates_empty() {
206 let src = NodeId::try_new("x").expect("valid");
207 let dst = NodeId::try_new("y").expect("valid");
208 assert_eq!(EdgeId::try_first(src, dst, ""), Err(IdError::Empty));
209 }
210
211 #[test]
212 fn edge_id_label_validation_propagates_too_long() {
213 let src = NodeId::try_new("x").expect("valid");
214 let dst = NodeId::try_new("y").expect("valid");
215 let long = "L".repeat(ID_MAX_LEN + 1);
216 assert!(matches!(
217 EdgeId::try_first(src, dst, long),
218 Err(IdError::TooLong { .. })
219 ));
220 }
221
222 #[test]
223 fn edge_id_label_validation_propagates_nul() {
224 let src = NodeId::try_new("x").expect("valid");
225 let dst = NodeId::try_new("y").expect("valid");
226 assert_eq!(
227 EdgeId::try_first(src, dst, "la\0bel"),
228 Err(IdError::ContainsNul)
229 );
230 }
231
232 #[test]
233 fn edge_id_no_collision_with_dashes_in_label() {
234 let a = EdgeId::try_first(
235 NodeId::try_new("alice").expect("v"),
236 NodeId::try_new("bob").expect("v"),
237 "--",
238 )
239 .expect("valid");
240 let b = EdgeId::try_first(
241 NodeId::try_new("alice").expect("v"),
242 NodeId::try_new("bob").expect("v"),
243 "-->",
244 )
245 .expect("valid");
246 assert_ne!(
247 a, b,
248 "labels '--' and '-->' must not produce the same EdgeId"
249 );
250 }
251
252 #[test]
253 fn edge_id_parallel_edges_distinguished_by_seq() {
254 let a = EdgeId::try_first(
255 NodeId::try_new("x").expect("v"),
256 NodeId::try_new("y").expect("v"),
257 "KNOWS",
258 )
259 .expect("valid");
260 let b = EdgeId::try_with_seq(
261 NodeId::try_new("x").expect("v"),
262 NodeId::try_new("y").expect("v"),
263 "KNOWS",
264 1,
265 )
266 .expect("valid");
267 assert_ne!(a, b, "different seq values must produce distinct EdgeIds");
268 assert_eq!(a.seq, 0);
269 assert_eq!(b.seq, 1);
270 }
271
272 #[test]
273 fn edge_id_display_fromstr_roundtrip() {
274 let e = EdgeId::try_first(
275 NodeId::try_new("alice").expect("v"),
276 NodeId::try_new("bob").expect("v"),
277 "KNOWS",
278 )
279 .expect("valid");
280 let s = e.to_string();
281 let parsed: EdgeId = s.parse().expect("round-trip must succeed");
282 assert_eq!(e, parsed);
283
284 let e2 = EdgeId::try_with_seq(
285 NodeId::try_new("a|b:c").expect("v"),
286 NodeId::try_new("d-->e").expect("v"),
287 "label--weird-->one",
288 42,
289 )
290 .expect("valid");
291 let s2 = e2.to_string();
292 let parsed2: EdgeId = s2
293 .parse()
294 .expect("round-trip with weird chars must succeed");
295 assert_eq!(e2, parsed2);
296
297 let e3 = EdgeId::try_first(
298 NodeId::try_new("n1").expect("v"),
299 NodeId::try_new("n2").expect("v"),
300 "REL",
301 )
302 .expect("valid");
303 let s3 = e3.to_string();
304 let parsed3: EdgeId = s3.parse().expect("seq=0 round-trip must succeed");
305 assert_eq!(e3, parsed3);
306 }
307
308 #[test]
309 fn edge_id_serde_roundtrip() {
310 let e = EdgeId::try_with_seq(
311 NodeId::try_new("src").expect("v"),
312 NodeId::try_new("dst").expect("v"),
313 "EDGE",
314 7,
315 )
316 .expect("valid");
317 let bytes = zerompk::to_msgpack_vec(&e).expect("msgpack serialization must succeed");
318 let decoded: EdgeId =
319 zerompk::from_msgpack(&bytes).expect("msgpack deserialization must succeed");
320 assert_eq!(e, decoded);
321 }
322
323 #[test]
324 fn from_str_rejects_empty_label() {
325 let e = EdgeId {
330 src: NodeId::from_validated("a".to_owned()),
331 dst: NodeId::from_validated("b".to_owned()),
332 label: String::new(),
333 seq: 0,
334 };
335 let s = e.to_string();
336 let err = s
337 .parse::<EdgeId>()
338 .expect_err("empty label must fail parse");
339 assert!(matches!(
340 err,
341 EdgeIdParseError::InvalidLabel(IdError::Empty)
342 ));
343 }
344}