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 Dependency,
24 Implementation,
26 Lateral,
28 Annotation,
30}
31
32#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
37#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
38#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
39pub enum EdgeRelation {
40 Contains,
42 PartOf,
43 InstanceOf,
44 Extends,
46 VariantOf,
47 IntroducedBy,
48 Supersedes,
49 DependsOn,
51 Enables,
52 Implements,
54 CompetesWith,
56 ComposedWith,
57 Annotates,
59}
60
61impl EdgeRelation {
62 pub const ALL: [Self; 13] = [
64 Self::Contains,
65 Self::PartOf,
66 Self::InstanceOf,
67 Self::Extends,
68 Self::VariantOf,
69 Self::IntroducedBy,
70 Self::Supersedes,
71 Self::DependsOn,
72 Self::Enables,
73 Self::Implements,
74 Self::CompetesWith,
75 Self::ComposedWith,
76 Self::Annotates,
77 ];
78
79 pub const fn category(&self) -> EdgeCategory {
81 match self {
82 Self::Contains | Self::PartOf | Self::InstanceOf => EdgeCategory::Structure,
83 Self::Extends | Self::VariantOf | Self::IntroducedBy | Self::Supersedes => {
84 EdgeCategory::Derivation
85 }
86 Self::DependsOn | Self::Enables => EdgeCategory::Dependency,
87 Self::Implements => EdgeCategory::Implementation,
88 Self::CompetesWith | Self::ComposedWith => EdgeCategory::Lateral,
89 Self::Annotates => EdgeCategory::Annotation,
90 }
91 }
92
93 pub const fn as_str(&self) -> &'static str {
95 match self {
96 Self::Contains => "contains",
97 Self::PartOf => "part_of",
98 Self::InstanceOf => "instance_of",
99 Self::Extends => "extends",
100 Self::VariantOf => "variant_of",
101 Self::IntroducedBy => "introduced_by",
102 Self::Supersedes => "supersedes",
103 Self::DependsOn => "depends_on",
104 Self::Enables => "enables",
105 Self::Implements => "implements",
106 Self::CompetesWith => "competes_with",
107 Self::ComposedWith => "composed_with",
108 Self::Annotates => "annotates",
109 }
110 }
111}
112
113impl fmt::Display for EdgeRelation {
114 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
115 f.write_str(self.as_str())
116 }
117}
118
119const EDGE_RELATION_VALID: &[&str] = &[
120 "contains",
121 "part_of",
122 "instance_of",
123 "extends",
124 "variant_of",
125 "introduced_by",
126 "supersedes",
127 "depends_on",
128 "enables",
129 "implements",
130 "competes_with",
131 "composed_with",
132 "annotates",
133];
134
135impl FromStr for EdgeRelation {
136 type Err = crate::error::UnknownVariant;
137
138 fn from_str(s: &str) -> Result<Self, Self::Err> {
139 let normalised: String = s
140 .chars()
141 .map(|c| {
142 if c == '-' {
143 '_'
144 } else {
145 c.to_ascii_lowercase()
146 }
147 })
148 .filter(|c| c.is_ascii_alphanumeric() || *c == '_')
149 .collect();
150
151 match normalised.as_str() {
152 "contains" => Ok(Self::Contains),
153 "part_of" | "partof" => Ok(Self::PartOf),
154 "instance_of" | "instanceof" => Ok(Self::InstanceOf),
155 "extends" => Ok(Self::Extends),
156 "variant_of" | "variantof" => Ok(Self::VariantOf),
157 "introduced_by" | "introducedby" => Ok(Self::IntroducedBy),
158 "supersedes" => Ok(Self::Supersedes),
159 "depends_on" | "dependson" => Ok(Self::DependsOn),
160 "enables" => Ok(Self::Enables),
161 "implements" => Ok(Self::Implements),
162 "competes_with" | "competeswith" => Ok(Self::CompetesWith),
163 "composed_with" | "composedwith" => Ok(Self::ComposedWith),
164 "annotates" => Ok(Self::Annotates),
165 _ => Err(crate::error::UnknownVariant::new(
166 "edge_relation",
167 s,
168 EDGE_RELATION_VALID,
169 )),
170 }
171 }
172}
173
174#[cfg(test)]
175mod tests {
176 use super::*;
177 use alloc::string::ToString;
178
179 #[test]
180 fn all_has_thirteen_variants() {
181 assert_eq!(EdgeRelation::ALL.len(), 13);
182 }
183
184 #[test]
185 fn display_roundtrip_for_all() {
186 for relation in EdgeRelation::ALL {
187 let s = relation.to_string();
188 let parsed: EdgeRelation = s.parse().expect("display output should re-parse");
189 assert_eq!(parsed, relation);
190 }
191 }
192
193 #[test]
194 fn from_str_case_insensitive() {
195 assert_eq!(
196 "Extends".parse::<EdgeRelation>().unwrap(),
197 EdgeRelation::Extends
198 );
199 assert_eq!(
200 "extends".parse::<EdgeRelation>().unwrap(),
201 EdgeRelation::Extends
202 );
203 assert_eq!(
204 "EXTENDS".parse::<EdgeRelation>().unwrap(),
205 EdgeRelation::Extends
206 );
207 }
208
209 #[test]
210 fn from_str_hyphen_tolerant() {
211 assert_eq!(
212 "part_of".parse::<EdgeRelation>().unwrap(),
213 EdgeRelation::PartOf
214 );
215 assert_eq!(
216 "part-of".parse::<EdgeRelation>().unwrap(),
217 EdgeRelation::PartOf
218 );
219 assert_eq!(
220 "partof".parse::<EdgeRelation>().unwrap(),
221 EdgeRelation::PartOf
222 );
223
224 assert_eq!(
225 "introduced_by".parse::<EdgeRelation>().unwrap(),
226 EdgeRelation::IntroducedBy
227 );
228 assert_eq!(
229 "introduced-by".parse::<EdgeRelation>().unwrap(),
230 EdgeRelation::IntroducedBy
231 );
232 }
233
234 #[test]
235 fn from_str_unknown_returns_error_with_list() {
236 let err = "related_to".parse::<EdgeRelation>().unwrap_err();
237 let msg = err.to_string();
238 assert!(
239 msg.contains("related_to"),
240 "error should mention the bad input"
241 );
242 assert!(
243 msg.contains("contains"),
244 "error should list valid relations"
245 );
246 assert!(msg.contains("annotates"), "error should list all 13");
247 }
248
249 #[test]
250 fn category_returns_correct_group() {
251 assert_eq!(EdgeRelation::Contains.category(), EdgeCategory::Structure);
252 assert_eq!(EdgeRelation::PartOf.category(), EdgeCategory::Structure);
253 assert_eq!(EdgeRelation::InstanceOf.category(), EdgeCategory::Structure);
254
255 assert_eq!(EdgeRelation::Extends.category(), EdgeCategory::Derivation);
256 assert_eq!(EdgeRelation::VariantOf.category(), EdgeCategory::Derivation);
257 assert_eq!(
258 EdgeRelation::IntroducedBy.category(),
259 EdgeCategory::Derivation
260 );
261 assert_eq!(
262 EdgeRelation::Supersedes.category(),
263 EdgeCategory::Derivation
264 );
265
266 assert_eq!(EdgeRelation::DependsOn.category(), EdgeCategory::Dependency);
267 assert_eq!(EdgeRelation::Enables.category(), EdgeCategory::Dependency);
268
269 assert_eq!(
270 EdgeRelation::Implements.category(),
271 EdgeCategory::Implementation
272 );
273
274 assert_eq!(EdgeRelation::CompetesWith.category(), EdgeCategory::Lateral);
275 assert_eq!(EdgeRelation::ComposedWith.category(), EdgeCategory::Lateral);
276
277 assert_eq!(EdgeRelation::Annotates.category(), EdgeCategory::Annotation);
278 }
279
280 #[test]
281 fn all_categories_covered() {
282 let mut cats = alloc::vec::Vec::new();
283 for r in EdgeRelation::ALL {
284 let c = r.category();
285 if !cats.contains(&c) {
286 cats.push(c);
287 }
288 }
289 assert_eq!(cats.len(), 6, "all 6 categories must be represented");
290 }
291
292 #[cfg(feature = "serde")]
293 #[test]
294 fn serde_snake_case_roundtrip() {
295 let rel = EdgeRelation::IntroducedBy;
296 let json = serde_json::to_string(&rel).unwrap();
297 assert_eq!(json, "\"introduced_by\"");
298 let parsed: EdgeRelation = serde_json::from_str(&json).unwrap();
299 assert_eq!(parsed, rel);
300 }
301}