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 Extends,
52 VariantOf,
53 IntroducedBy,
54 Supersedes,
55 DerivedFrom,
57 Precedes,
59 DependsOn,
61 Enables,
62 Implements,
64 CompetesWith,
66 ComposedWith,
67 Annotates,
69 Supports,
71 Refutes,
72}
73
74impl EdgeRelation {
75 pub const ALL: [Self; 17] = [
77 Self::Contains,
78 Self::PartOf,
79 Self::InstanceOf,
80 Self::Extends,
81 Self::VariantOf,
82 Self::IntroducedBy,
83 Self::Supersedes,
84 Self::DerivedFrom,
85 Self::Precedes,
86 Self::DependsOn,
87 Self::Enables,
88 Self::Implements,
89 Self::CompetesWith,
90 Self::ComposedWith,
91 Self::Annotates,
92 Self::Supports,
93 Self::Refutes,
94 ];
95
96 pub const VALID_NAMES: &'static [&'static str] = &[
98 "contains",
99 "part_of",
100 "instance_of",
101 "extends",
102 "variant_of",
103 "introduced_by",
104 "supersedes",
105 "derived_from",
106 "precedes",
107 "depends_on",
108 "enables",
109 "implements",
110 "competes_with",
111 "composed_with",
112 "annotates",
113 "supports",
114 "refutes",
115 ];
116
117 pub const fn is_symmetric(&self) -> bool {
119 matches!(self, Self::CompetesWith | Self::ComposedWith)
120 }
121
122 pub const fn category(&self) -> EdgeCategory {
124 match self {
125 Self::Contains | Self::PartOf | Self::InstanceOf => EdgeCategory::Structure,
126 Self::Extends | Self::VariantOf | Self::IntroducedBy | Self::Supersedes => {
127 EdgeCategory::Derivation
128 }
129 Self::DerivedFrom => EdgeCategory::Provenance,
130 Self::Precedes => EdgeCategory::Temporal,
131 Self::DependsOn | Self::Enables => EdgeCategory::Dependency,
132 Self::Implements => EdgeCategory::Implementation,
133 Self::CompetesWith | Self::ComposedWith => EdgeCategory::Lateral,
134 Self::Annotates => EdgeCategory::Annotation,
135 Self::Supports | Self::Refutes => EdgeCategory::Epistemic,
136 }
137 }
138
139 pub const fn as_str(&self) -> &'static str {
141 match self {
142 Self::Contains => "contains",
143 Self::PartOf => "part_of",
144 Self::InstanceOf => "instance_of",
145 Self::Extends => "extends",
146 Self::VariantOf => "variant_of",
147 Self::IntroducedBy => "introduced_by",
148 Self::Supersedes => "supersedes",
149 Self::DerivedFrom => "derived_from",
150 Self::Precedes => "precedes",
151 Self::DependsOn => "depends_on",
152 Self::Enables => "enables",
153 Self::Implements => "implements",
154 Self::CompetesWith => "competes_with",
155 Self::ComposedWith => "composed_with",
156 Self::Annotates => "annotates",
157 Self::Supports => "supports",
158 Self::Refutes => "refutes",
159 }
160 }
161}
162
163impl fmt::Display for EdgeRelation {
164 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
165 f.write_str(self.as_str())
166 }
167}
168
169impl FromStr for EdgeRelation {
170 type Err = crate::error::UnknownVariant;
171
172 fn from_str(s: &str) -> Result<Self, Self::Err> {
180 let normalised: String = s
181 .chars()
182 .map(|c| {
183 if c == '-' {
184 '_'
185 } else {
186 c.to_ascii_lowercase()
187 }
188 })
189 .filter(|c| c.is_ascii_alphanumeric() || *c == '_')
190 .collect();
191
192 match normalised.as_str() {
193 "contains" => Ok(Self::Contains),
194 "part_of" | "partof" => Ok(Self::PartOf),
195 "instance_of" | "instanceof" => Ok(Self::InstanceOf),
196 "extends" => Ok(Self::Extends),
197 "variant_of" | "variantof" => Ok(Self::VariantOf),
198 "introduced_by" | "introducedby" => Ok(Self::IntroducedBy),
199 "supersedes" => Ok(Self::Supersedes),
200 "derived_from" | "derivedfrom" => Ok(Self::DerivedFrom),
201 "precedes" => Ok(Self::Precedes),
202 "depends_on" | "dependson" => Ok(Self::DependsOn),
203 "enables" => Ok(Self::Enables),
204 "implements" => Ok(Self::Implements),
205 "competes_with" | "competeswith" => Ok(Self::CompetesWith),
206 "composed_with" | "composedwith" => Ok(Self::ComposedWith),
207 "annotates" => Ok(Self::Annotates),
208 "supports" => Ok(Self::Supports),
209 "refutes" => Ok(Self::Refutes),
210 _ => Err(crate::error::UnknownVariant::new(
211 "edge_relation",
212 s,
213 Self::VALID_NAMES,
214 )),
215 }
216 }
217}
218
219#[cfg(test)]
220mod tests {
221 use super::*;
222 use alloc::string::ToString;
223
224 #[test]
225 fn all_has_seventeen_variants() {
226 assert_eq!(EdgeRelation::ALL.len(), 17);
227 }
228
229 #[test]
230 fn all_nine_categories_covered() {
231 let mut cats = alloc::vec::Vec::new();
232 for r in EdgeRelation::ALL {
233 let c = r.category();
234 if !cats.contains(&c) {
235 cats.push(c);
236 }
237 }
238 assert_eq!(cats.len(), 9, "all 9 categories must be represented");
239 }
240
241 #[test]
242 fn display_roundtrip_for_all() {
243 for relation in EdgeRelation::ALL {
244 let s = relation.to_string();
245 let parsed: EdgeRelation = s.parse().expect("display output should re-parse");
246 assert_eq!(parsed, relation);
247 }
248 }
249
250 #[test]
251 fn from_str_case_insensitive() {
252 assert_eq!(
253 "Extends".parse::<EdgeRelation>().unwrap(),
254 EdgeRelation::Extends
255 );
256 assert_eq!(
257 "extends".parse::<EdgeRelation>().unwrap(),
258 EdgeRelation::Extends
259 );
260 assert_eq!(
261 "EXTENDS".parse::<EdgeRelation>().unwrap(),
262 EdgeRelation::Extends
263 );
264 }
265
266 #[test]
267 fn from_str_hyphen_tolerant() {
268 assert_eq!(
269 "part_of".parse::<EdgeRelation>().unwrap(),
270 EdgeRelation::PartOf
271 );
272 assert_eq!(
273 "part-of".parse::<EdgeRelation>().unwrap(),
274 EdgeRelation::PartOf
275 );
276 assert_eq!(
277 "partof".parse::<EdgeRelation>().unwrap(),
278 EdgeRelation::PartOf
279 );
280
281 assert_eq!(
282 "introduced_by".parse::<EdgeRelation>().unwrap(),
283 EdgeRelation::IntroducedBy
284 );
285 assert_eq!(
286 "introduced-by".parse::<EdgeRelation>().unwrap(),
287 EdgeRelation::IntroducedBy
288 );
289 }
290
291 #[test]
292 fn from_str_unknown_returns_error_with_list() {
293 let err = "related_to".parse::<EdgeRelation>().unwrap_err();
294 let msg = err.to_string();
295 assert!(
296 msg.contains("related_to"),
297 "error should mention the bad input"
298 );
299 assert!(
300 msg.contains("contains"),
301 "error should list valid relations"
302 );
303 assert!(
304 msg.contains("derived_from"),
305 "error should list derived_from"
306 );
307 assert!(msg.contains("precedes"), "error should list precedes");
308 assert!(msg.contains("annotates"), "error should list all 17");
309 }
310
311 #[test]
312 fn category_returns_correct_group() {
313 assert_eq!(EdgeRelation::Contains.category(), EdgeCategory::Structure);
314 assert_eq!(EdgeRelation::PartOf.category(), EdgeCategory::Structure);
315 assert_eq!(EdgeRelation::InstanceOf.category(), EdgeCategory::Structure);
316
317 assert_eq!(EdgeRelation::Extends.category(), EdgeCategory::Derivation);
318 assert_eq!(EdgeRelation::VariantOf.category(), EdgeCategory::Derivation);
319 assert_eq!(
320 EdgeRelation::IntroducedBy.category(),
321 EdgeCategory::Derivation
322 );
323 assert_eq!(
324 EdgeRelation::Supersedes.category(),
325 EdgeCategory::Derivation
326 );
327
328 assert_eq!(EdgeRelation::DependsOn.category(), EdgeCategory::Dependency);
329 assert_eq!(EdgeRelation::Enables.category(), EdgeCategory::Dependency);
330
331 assert_eq!(
332 EdgeRelation::Implements.category(),
333 EdgeCategory::Implementation
334 );
335
336 assert_eq!(
337 EdgeRelation::DerivedFrom.category(),
338 EdgeCategory::Provenance
339 );
340 assert_eq!(EdgeRelation::Precedes.category(), EdgeCategory::Temporal);
341
342 assert_eq!(EdgeRelation::CompetesWith.category(), EdgeCategory::Lateral);
343 assert_eq!(EdgeRelation::ComposedWith.category(), EdgeCategory::Lateral);
344
345 assert_eq!(EdgeRelation::Annotates.category(), EdgeCategory::Annotation);
346 }
347
348 #[test]
349 fn from_str_new_relations() {
350 assert_eq!(
351 "derived_from".parse::<EdgeRelation>().unwrap(),
352 EdgeRelation::DerivedFrom
353 );
354 assert_eq!(
355 "derived-from".parse::<EdgeRelation>().unwrap(),
356 EdgeRelation::DerivedFrom
357 );
358 assert_eq!(
359 "derivedfrom".parse::<EdgeRelation>().unwrap(),
360 EdgeRelation::DerivedFrom
361 );
362 assert_eq!(
363 "precedes".parse::<EdgeRelation>().unwrap(),
364 EdgeRelation::Precedes
365 );
366 }
367
368 #[test]
369 fn is_symmetric_only_for_lateral_peer_relations() {
370 assert!(EdgeRelation::CompetesWith.is_symmetric());
371 assert!(EdgeRelation::ComposedWith.is_symmetric());
372 assert!(!EdgeRelation::DependsOn.is_symmetric());
373 assert!(!EdgeRelation::DerivedFrom.is_symmetric());
374 assert!(!EdgeRelation::Precedes.is_symmetric());
375 assert!(!EdgeRelation::Extends.is_symmetric());
376 }
377
378 #[test]
379 fn from_str_epistemic_relations() {
380 assert_eq!(
381 "supports".parse::<EdgeRelation>().unwrap(),
382 EdgeRelation::Supports
383 );
384 assert_eq!(
385 "refutes".parse::<EdgeRelation>().unwrap(),
386 EdgeRelation::Refutes
387 );
388 assert_eq!(
389 "Supports".parse::<EdgeRelation>().unwrap(),
390 EdgeRelation::Supports
391 );
392 assert_eq!(
393 "REFUTES".parse::<EdgeRelation>().unwrap(),
394 EdgeRelation::Refutes
395 );
396 assert_eq!(EdgeRelation::Supports.category(), EdgeCategory::Epistemic);
397 assert_eq!(EdgeRelation::Refutes.category(), EdgeCategory::Epistemic);
398 assert!(!EdgeRelation::Supports.is_symmetric());
399 assert!(!EdgeRelation::Refutes.is_symmetric());
400 }
401
402 #[cfg(feature = "serde")]
403 #[test]
404 fn serde_snake_case_roundtrip() {
405 let rel = EdgeRelation::IntroducedBy;
406 let json = serde_json::to_string(&rel).unwrap();
407 assert_eq!(json, "\"introduced_by\"");
408 let parsed: EdgeRelation = serde_json::from_str(&json).unwrap();
409 assert_eq!(parsed, rel);
410 }
411
412 #[cfg(feature = "serde")]
413 #[test]
414 fn serde_new_relations_roundtrip() {
415 for rel in [EdgeRelation::DerivedFrom, EdgeRelation::Precedes] {
416 let json = serde_json::to_string(&rel).unwrap();
417 let parsed: EdgeRelation = serde_json::from_str(&json).unwrap();
418 assert_eq!(parsed, rel);
419 }
420 }
421
422 #[cfg(feature = "serde")]
423 #[test]
424 fn serde_epistemic_relations_roundtrip() {
425 let sup_json = serde_json::to_string(&EdgeRelation::Supports).unwrap();
426 assert_eq!(sup_json, "\"supports\"");
427 let sup_parsed: EdgeRelation = serde_json::from_str(&sup_json).unwrap();
428 assert_eq!(sup_parsed, EdgeRelation::Supports);
429
430 let ref_json = serde_json::to_string(&EdgeRelation::Refutes).unwrap();
431 assert_eq!(ref_json, "\"refutes\"");
432 let ref_parsed: EdgeRelation = serde_json::from_str(&ref_json).unwrap();
433 assert_eq!(ref_parsed, EdgeRelation::Refutes);
434 }
435}