1use std::collections::BTreeSet;
22
23use axioval_ir::{Evidence, ObjectId};
24
25use crate::derived_relationships::DERIVED_RELATIONSHIP_PREFIX;
26use crate::relationships::{
27 AbsentEndPolicy, RelationshipQuery, RelationshipSelectionError, RelationshipSelectionRequest,
28 RelationshipSelectionServiceHandle, SemanticRelationship, TraversalDirection,
29};
30
31const DIRECTIONS: [&str; 3] = ["forward", "backward", "either"];
32
33#[derive(Clone, Debug, PartialEq, Eq)]
35pub struct PathSegment {
36 relationships: Vec<SemanticRelationship>,
37 direction: TraversalDirection,
38 chain: bool,
39}
40
41impl PathSegment {
42 pub fn parse(text: &str) -> Result<Self, String> {
50 let trimmed = text.trim();
51 let (body, chain) = match trimmed.strip_suffix('+') {
52 Some(body) => (body, true),
53 None => (trimmed, false),
54 };
55 let mut alternatives: Vec<&str> = body.split('|').collect();
56 let last = alternatives.pop().unwrap_or_default();
57 let derived = last.trim().starts_with(DERIVED_RELATIONSHIP_PREFIX);
58 let (last, stated) = match last.rsplit_once(':') {
59 Some((_, stated)) if derived && !DIRECTIONS.contains(&stated.trim()) => (last, None),
60 Some((relationship, stated)) => (relationship, Some(stated.trim())),
61 None => (last, None),
62 };
63 let direction = match stated {
64 None | Some("forward") => TraversalDirection::Forward,
65 Some("backward") => TraversalDirection::Backward,
66 Some("either") => TraversalDirection::Either,
67 Some(other) => return Err(format!("direction `{other}` is unsupported")),
68 };
69 alternatives.push(last);
70 let mut relationships: Vec<SemanticRelationship> = Vec::new();
71 for alternative in alternatives {
72 let alternative = alternative.trim();
73 if let Some((_, stated)) = alternative.rsplit_once(':')
74 && DIRECTIONS.contains(&stated.trim())
75 {
76 return Err(format!(
77 "step `{trimmed}` states a direction inside `{alternative}`; one direction \
78 follows the last alternative and applies to all"
79 ));
80 }
81 let relationship = SemanticRelationship::try_new(alternative)
82 .map_err(|_| format!("step `{trimmed}` names an empty relationship"))?;
83 if relationships.contains(&relationship) {
84 return Err(format!(
85 "step `{trimmed}` names `{alternative}` more than once"
86 ));
87 }
88 relationships.push(relationship);
89 }
90 Ok(Self {
91 relationships,
92 direction,
93 chain,
94 })
95 }
96
97 #[must_use]
99 pub fn relationships(&self) -> &[SemanticRelationship] {
100 &self.relationships
101 }
102
103 #[must_use]
105 pub fn direction(&self) -> TraversalDirection {
106 self.direction
107 }
108
109 #[must_use]
111 pub fn chain(&self) -> bool {
112 self.chain
113 }
114
115 #[must_use]
117 pub fn reversed(&self) -> Self {
118 Self {
119 relationships: self.relationships.clone(),
120 direction: match self.direction {
121 TraversalDirection::Forward => TraversalDirection::Backward,
122 TraversalDirection::Backward => TraversalDirection::Forward,
123 TraversalDirection::Either => TraversalDirection::Either,
124 },
125 chain: self.chain,
126 }
127 }
128
129 #[must_use]
131 pub fn shown(&self) -> String {
132 self.relationships
133 .iter()
134 .map(SemanticRelationship::as_str)
135 .collect::<Vec<_>>()
136 .join("|")
137 }
138
139 pub fn walk(
151 &self,
152 service: &RelationshipSelectionServiceHandle,
153 from: &ObjectId,
154 everything: &[ObjectId],
155 scope: &[ObjectId],
156 chain: bool,
157 absent_ends: AbsentEndPolicy,
158 ) -> Result<(BTreeSet<ObjectId>, Vec<Evidence>), RelationshipSelectionError> {
159 let mut evidence = Vec::new();
160 let mut hop = |anchor: &ObjectId,
161 universe: &[ObjectId],
162 relationship: &SemanticRelationship,
163 follow_chain: bool|
164 -> Result<Vec<ObjectId>, RelationshipSelectionError> {
165 let request = RelationshipSelectionRequest::try_new(
166 anchor.clone(),
167 universe.to_vec(),
168 RelationshipQuery::Related {
169 relationship: relationship.clone(),
170 direction: self.direction,
171 follow_chain,
172 },
173 )?
174 .with_absent_ends(absent_ends);
175 let selection = service.select(&request)?;
176 evidence.extend(selection.evidence().iter().cloned());
177 Ok(selection.candidates().to_vec())
178 };
179 let mut reached = BTreeSet::new();
180 if !chain || self.relationships.len() == 1 {
181 for relationship in &self.relationships {
182 reached.extend(hop(from, scope, relationship, chain)?);
183 }
184 } else {
185 let mut seen = BTreeSet::from([from.clone()]);
186 let mut frontier = vec![from.clone()];
187 while let Some(current) = frontier.pop() {
188 for relationship in &self.relationships {
189 for object in hop(¤t, everything, relationship, true)? {
190 if seen.insert(object.clone()) {
191 frontier.push(object);
192 }
193 }
194 }
195 }
196 seen.remove(from);
197 let scope: BTreeSet<&ObjectId> = scope.iter().collect();
198 reached.extend(seen.into_iter().filter(|object| scope.contains(object)));
199 }
200 reached.remove(from);
201 Ok((reached, evidence))
202 }
203}
204
205#[cfg(test)]
206mod tests {
207 use super::*;
208
209 fn names(step: &PathSegment) -> Vec<&str> {
210 step.relationships()
211 .iter()
212 .map(SemanticRelationship::as_str)
213 .collect()
214 }
215
216 #[test]
217 fn a_step_names_one_or_several_relationships() {
218 let step = PathSegment::parse("IfcRelAggregates").unwrap();
219 assert_eq!(names(&step), ["IfcRelAggregates"]);
220 assert_eq!(step.direction(), TraversalDirection::Forward);
221 assert!(!step.chain());
222
223 let step =
224 PathSegment::parse(" IfcRelFillsElement | IfcRelVoidsElement:backward+ ").unwrap();
225 assert_eq!(names(&step), ["IfcRelFillsElement", "IfcRelVoidsElement"]);
226 assert_eq!(step.direction(), TraversalDirection::Backward);
227 assert!(step.chain());
228 assert_eq!(step.shown(), "IfcRelFillsElement|IfcRelVoidsElement");
229 assert_eq!(step.reversed().direction(), TraversalDirection::Forward);
230 }
231
232 #[test]
233 fn derived_identities_keep_their_colons() {
234 let step = PathSegment::parse("axioval:derived.adjacent-space;reach=1").unwrap();
235 assert_eq!(names(&step), ["axioval:derived.adjacent-space;reach=1"]);
236 let step =
237 PathSegment::parse("IfcRelNests|axioval:derived.same-level;by=name:either").unwrap();
238 assert_eq!(
239 names(&step),
240 ["IfcRelNests", "axioval:derived.same-level;by=name"]
241 );
242 assert_eq!(step.direction(), TraversalDirection::Either);
243 let step = PathSegment::parse("axioval:derived.intersects|IfcRelNests").unwrap();
244 assert_eq!(names(&step), ["axioval:derived.intersects", "IfcRelNests"]);
245 }
246
247 #[test]
248 fn malformed_steps_are_refused() {
249 for text in [
250 "",
251 "IfcRelNests|",
252 "|IfcRelNests",
253 "IfcRelNests||IfcRelAggregates",
254 "IfcRelNests|IfcRelNests",
255 "IfcRelNests:sideways",
256 "IfcRelNests:backward|IfcRelAggregates",
257 "IfcRelNests:backward|IfcRelAggregates:backward",
258 ] {
259 assert!(PathSegment::parse(text).is_err(), "{text:?} parsed");
260 }
261 }
262}