1extern crate alloc;
4use alloc::string::String;
5use core::fmt;
6use core::str::FromStr;
7
8#[cfg(feature = "serde")]
9use serde::{Deserialize, Serialize};
10
11#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
15#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
16#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
17pub enum EdgeCategory {
18 Structure,
20 Derivation,
22 Provenance,
24 Temporal,
26 Dependency,
28 Implementation,
30 Lateral,
32 Annotation,
34 Epistemic,
36}
37
38#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
43#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
44#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
45pub enum EdgeRelation {
46 Contains,
48 PartOf,
49 InstanceOf,
50 LinksTo,
51 Extends,
53 VariantOf,
54 IntroducedBy,
55 Supersedes,
56 DerivedFrom,
58 Precedes,
60 DependsOn,
62 Enables,
63 Implements,
65 CompetesWith,
67 ComposedWith,
68 Annotates,
70 Supports,
72 Refutes,
73}
74
75impl EdgeRelation {
76 pub const ALL: [Self; 18] = [
78 Self::Contains,
79 Self::PartOf,
80 Self::InstanceOf,
81 Self::LinksTo,
82 Self::Extends,
83 Self::VariantOf,
84 Self::IntroducedBy,
85 Self::Supersedes,
86 Self::DerivedFrom,
87 Self::Precedes,
88 Self::DependsOn,
89 Self::Enables,
90 Self::Implements,
91 Self::CompetesWith,
92 Self::ComposedWith,
93 Self::Annotates,
94 Self::Supports,
95 Self::Refutes,
96 ];
97
98 pub const VALID_NAMES: &'static [&'static str] = &[
100 "contains",
101 "part_of",
102 "instance_of",
103 "links_to",
104 "extends",
105 "variant_of",
106 "introduced_by",
107 "supersedes",
108 "derived_from",
109 "precedes",
110 "depends_on",
111 "enables",
112 "implements",
113 "competes_with",
114 "composed_with",
115 "annotates",
116 "supports",
117 "refutes",
118 ];
119
120 pub const fn is_symmetric(&self) -> bool {
122 matches!(self, Self::CompetesWith | Self::ComposedWith)
123 }
124
125 pub const fn category(&self) -> EdgeCategory {
127 match self {
128 Self::Contains | Self::PartOf | Self::InstanceOf | Self::LinksTo => {
129 EdgeCategory::Structure
130 }
131 Self::Extends | Self::VariantOf | Self::IntroducedBy | Self::Supersedes => {
132 EdgeCategory::Derivation
133 }
134 Self::DerivedFrom => EdgeCategory::Provenance,
135 Self::Precedes => EdgeCategory::Temporal,
136 Self::DependsOn | Self::Enables => EdgeCategory::Dependency,
137 Self::Implements => EdgeCategory::Implementation,
138 Self::CompetesWith | Self::ComposedWith => EdgeCategory::Lateral,
139 Self::Annotates => EdgeCategory::Annotation,
140 Self::Supports | Self::Refutes => EdgeCategory::Epistemic,
141 }
142 }
143
144 pub const fn as_str(&self) -> &'static str {
146 match self {
147 Self::Contains => "contains",
148 Self::PartOf => "part_of",
149 Self::InstanceOf => "instance_of",
150 Self::LinksTo => "links_to",
151 Self::Extends => "extends",
152 Self::VariantOf => "variant_of",
153 Self::IntroducedBy => "introduced_by",
154 Self::Supersedes => "supersedes",
155 Self::DerivedFrom => "derived_from",
156 Self::Precedes => "precedes",
157 Self::DependsOn => "depends_on",
158 Self::Enables => "enables",
159 Self::Implements => "implements",
160 Self::CompetesWith => "competes_with",
161 Self::ComposedWith => "composed_with",
162 Self::Annotates => "annotates",
163 Self::Supports => "supports",
164 Self::Refutes => "refutes",
165 }
166 }
167}
168
169impl fmt::Display for EdgeRelation {
170 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
171 f.write_str(self.as_str())
172 }
173}
174
175impl FromStr for EdgeRelation {
176 type Err = crate::error::UnknownVariant;
177
178 fn from_str(s: &str) -> Result<Self, Self::Err> {
186 let mut normalised = String::with_capacity(s.len());
187 for c in s.chars() {
188 match c {
189 '-' | '_' => normalised.push('_'),
190 c if c.is_ascii_alphanumeric() => normalised.push(c.to_ascii_lowercase()),
191 _ => {
192 return Err(crate::error::UnknownVariant::new(
193 "edge_relation",
194 s,
195 Self::VALID_NAMES,
196 ));
197 }
198 }
199 }
200
201 match normalised.as_str() {
202 "contains" => Ok(Self::Contains),
203 "part_of" | "partof" => Ok(Self::PartOf),
204 "instance_of" | "instanceof" => Ok(Self::InstanceOf),
205 "links_to" | "linksto" => Ok(Self::LinksTo),
206 "extends" => Ok(Self::Extends),
207 "variant_of" | "variantof" => Ok(Self::VariantOf),
208 "introduced_by" | "introducedby" => Ok(Self::IntroducedBy),
209 "supersedes" => Ok(Self::Supersedes),
210 "derived_from" | "derivedfrom" => Ok(Self::DerivedFrom),
211 "precedes" => Ok(Self::Precedes),
212 "depends_on" | "dependson" => Ok(Self::DependsOn),
213 "enables" => Ok(Self::Enables),
214 "implements" => Ok(Self::Implements),
215 "competes_with" | "competeswith" => Ok(Self::CompetesWith),
216 "composed_with" | "composedwith" => Ok(Self::ComposedWith),
217 "annotates" => Ok(Self::Annotates),
218 "supports" => Ok(Self::Supports),
219 "refutes" => Ok(Self::Refutes),
220 _ => Err(crate::error::UnknownVariant::new(
221 "edge_relation",
222 s,
223 Self::VALID_NAMES,
224 )),
225 }
226 }
227}
228
229#[cfg(test)]
230mod tests {
231 use super::*;
232 use alloc::string::ToString;
233
234 #[test]
235 fn all_has_eighteen_variants() {
236 assert_eq!(EdgeRelation::ALL.len(), 18);
237 }
238
239 #[test]
240 fn all_nine_categories_covered() {
241 let mut cats = alloc::vec::Vec::new();
242 for r in EdgeRelation::ALL {
243 let c = r.category();
244 if !cats.contains(&c) {
245 cats.push(c);
246 }
247 }
248 assert_eq!(cats.len(), 9, "all 9 categories must be represented");
249 }
250
251 #[test]
252 fn display_roundtrip_for_all() {
253 for relation in EdgeRelation::ALL {
254 let s = relation.to_string();
255 let parsed: EdgeRelation = s.parse().expect("display output should re-parse");
256 assert_eq!(parsed, relation);
257 }
258 }
259
260 #[test]
261 fn from_str_case_insensitive() {
262 assert_eq!(
263 "Extends".parse::<EdgeRelation>().unwrap(),
264 EdgeRelation::Extends
265 );
266 assert_eq!(
267 "extends".parse::<EdgeRelation>().unwrap(),
268 EdgeRelation::Extends
269 );
270 assert_eq!(
271 "EXTENDS".parse::<EdgeRelation>().unwrap(),
272 EdgeRelation::Extends
273 );
274 }
275
276 #[test]
277 fn from_str_hyphen_tolerant() {
278 assert_eq!(
279 "part_of".parse::<EdgeRelation>().unwrap(),
280 EdgeRelation::PartOf
281 );
282 assert_eq!(
283 "part-of".parse::<EdgeRelation>().unwrap(),
284 EdgeRelation::PartOf
285 );
286 assert_eq!(
287 "partof".parse::<EdgeRelation>().unwrap(),
288 EdgeRelation::PartOf
289 );
290
291 assert_eq!(
292 "introduced_by".parse::<EdgeRelation>().unwrap(),
293 EdgeRelation::IntroducedBy
294 );
295 assert_eq!(
296 "introduced-by".parse::<EdgeRelation>().unwrap(),
297 EdgeRelation::IntroducedBy
298 );
299 }
300
301 #[test]
302 fn from_str_unknown_returns_error_with_list() {
303 let err = "related_to".parse::<EdgeRelation>().unwrap_err();
304 let msg = err.to_string();
305 assert!(
306 msg.contains("related_to"),
307 "error should mention the bad input"
308 );
309 assert!(
310 msg.contains("contains"),
311 "error should list valid relations"
312 );
313 assert!(
314 msg.contains("derived_from"),
315 "error should list derived_from"
316 );
317 assert!(msg.contains("precedes"), "error should list precedes");
318 assert!(msg.contains("annotates"), "error should list all 18");
319 }
320
321 #[test]
322 fn edge_relation_bang_rejected() {
323 for bad in ["supports!", "part/of", "depends.on", "competes with"] {
324 let err = bad
325 .parse::<EdgeRelation>()
326 .expect_err("malformed punctuation/whitespace must be rejected");
327 assert_eq!(err.domain, "edge_relation");
328 assert_eq!(err.value, bad);
329 }
330 }
331
332 #[test]
333 fn category_returns_correct_group() {
334 assert_eq!(EdgeRelation::Contains.category(), EdgeCategory::Structure);
335 assert_eq!(EdgeRelation::PartOf.category(), EdgeCategory::Structure);
336 assert_eq!(EdgeRelation::InstanceOf.category(), EdgeCategory::Structure);
337 assert_eq!(EdgeRelation::LinksTo.category(), EdgeCategory::Structure);
338
339 assert_eq!(EdgeRelation::Extends.category(), EdgeCategory::Derivation);
340 assert_eq!(EdgeRelation::VariantOf.category(), EdgeCategory::Derivation);
341 assert_eq!(
342 EdgeRelation::IntroducedBy.category(),
343 EdgeCategory::Derivation
344 );
345 assert_eq!(
346 EdgeRelation::Supersedes.category(),
347 EdgeCategory::Derivation
348 );
349
350 assert_eq!(EdgeRelation::DependsOn.category(), EdgeCategory::Dependency);
351 assert_eq!(EdgeRelation::Enables.category(), EdgeCategory::Dependency);
352
353 assert_eq!(
354 EdgeRelation::Implements.category(),
355 EdgeCategory::Implementation
356 );
357
358 assert_eq!(
359 EdgeRelation::DerivedFrom.category(),
360 EdgeCategory::Provenance
361 );
362 assert_eq!(EdgeRelation::Precedes.category(), EdgeCategory::Temporal);
363
364 assert_eq!(EdgeRelation::CompetesWith.category(), EdgeCategory::Lateral);
365 assert_eq!(EdgeRelation::ComposedWith.category(), EdgeCategory::Lateral);
366
367 assert_eq!(EdgeRelation::Annotates.category(), EdgeCategory::Annotation);
368 }
369
370 #[test]
371 fn from_str_new_relations() {
372 assert_eq!(
373 "derived_from".parse::<EdgeRelation>().unwrap(),
374 EdgeRelation::DerivedFrom
375 );
376 assert_eq!(
377 "derived-from".parse::<EdgeRelation>().unwrap(),
378 EdgeRelation::DerivedFrom
379 );
380 assert_eq!(
381 "derivedfrom".parse::<EdgeRelation>().unwrap(),
382 EdgeRelation::DerivedFrom
383 );
384 assert_eq!(
385 "precedes".parse::<EdgeRelation>().unwrap(),
386 EdgeRelation::Precedes
387 );
388 }
389
390 #[test]
391 fn from_str_links_to() {
392 assert_eq!(
393 "links_to".parse::<EdgeRelation>().unwrap(),
394 EdgeRelation::LinksTo
395 );
396 assert_eq!(
397 "links-to".parse::<EdgeRelation>().unwrap(),
398 EdgeRelation::LinksTo
399 );
400 assert_eq!(
401 "linksto".parse::<EdgeRelation>().unwrap(),
402 EdgeRelation::LinksTo
403 );
404 assert_eq!(EdgeRelation::LinksTo.to_string(), "links_to");
405 assert_eq!(EdgeRelation::LinksTo.category(), EdgeCategory::Structure);
406 }
407
408 #[test]
409 fn is_symmetric_only_for_lateral_peer_relations() {
410 assert!(EdgeRelation::CompetesWith.is_symmetric());
411 assert!(EdgeRelation::ComposedWith.is_symmetric());
412 assert!(!EdgeRelation::DependsOn.is_symmetric());
413 assert!(!EdgeRelation::DerivedFrom.is_symmetric());
414 assert!(!EdgeRelation::Precedes.is_symmetric());
415 assert!(!EdgeRelation::Extends.is_symmetric());
416 assert!(!EdgeRelation::LinksTo.is_symmetric());
417 }
418
419 #[test]
420 fn from_str_epistemic_relations() {
421 assert_eq!(
422 "supports".parse::<EdgeRelation>().unwrap(),
423 EdgeRelation::Supports
424 );
425 assert_eq!(
426 "refutes".parse::<EdgeRelation>().unwrap(),
427 EdgeRelation::Refutes
428 );
429 assert_eq!(
430 "Supports".parse::<EdgeRelation>().unwrap(),
431 EdgeRelation::Supports
432 );
433 assert_eq!(
434 "REFUTES".parse::<EdgeRelation>().unwrap(),
435 EdgeRelation::Refutes
436 );
437 assert_eq!(EdgeRelation::Supports.category(), EdgeCategory::Epistemic);
438 assert_eq!(EdgeRelation::Refutes.category(), EdgeCategory::Epistemic);
439 assert!(!EdgeRelation::Supports.is_symmetric());
440 assert!(!EdgeRelation::Refutes.is_symmetric());
441 }
442
443 #[cfg(feature = "serde")]
444 #[test]
445 fn serde_snake_case_roundtrip() {
446 let rel = EdgeRelation::IntroducedBy;
447 let json = serde_json::to_string(&rel).unwrap();
448 assert_eq!(json, "\"introduced_by\"");
449 let parsed: EdgeRelation = serde_json::from_str(&json).unwrap();
450 assert_eq!(parsed, rel);
451 }
452
453 #[cfg(feature = "serde")]
454 #[test]
455 fn serde_new_relations_roundtrip() {
456 for rel in [EdgeRelation::DerivedFrom, EdgeRelation::Precedes] {
457 let json = serde_json::to_string(&rel).unwrap();
458 let parsed: EdgeRelation = serde_json::from_str(&json).unwrap();
459 assert_eq!(parsed, rel);
460 }
461 }
462
463 #[cfg(feature = "serde")]
464 #[test]
465 fn serde_epistemic_relations_roundtrip() {
466 let sup_json = serde_json::to_string(&EdgeRelation::Supports).unwrap();
467 assert_eq!(sup_json, "\"supports\"");
468 let sup_parsed: EdgeRelation = serde_json::from_str(&sup_json).unwrap();
469 assert_eq!(sup_parsed, EdgeRelation::Supports);
470
471 let ref_json = serde_json::to_string(&EdgeRelation::Refutes).unwrap();
472 assert_eq!(ref_json, "\"refutes\"");
473 let ref_parsed: EdgeRelation = serde_json::from_str(&ref_json).unwrap();
474 assert_eq!(ref_parsed, EdgeRelation::Refutes);
475 }
476}