Skip to main content

axioval_engine/
path.rs

1//! The step grammar every relationship path shares, and its walk.
2//!
3//! A step is `Relationship[|Relationship…][:direction][+]`:
4//!
5//! - one or more relationship identities separated by `|`, any of which the
6//!   step may take;
7//! - an optional direction, `forward` (the default), `backward` or `either`,
8//!   applying to every alternative, written once after the last one;
9//! - an optional trailing `+`, taking the step one or more times.
10//!
11//! A step through one relationship with `+` follows that relationship's
12//! chain as the source answers it. A step through several with `+` mixes
13//! them along the chain: each hop takes any of them, so
14//! `IfcRelFillsElement|IfcRelVoidsElement:backward+` climbs from a door
15//! through the opening it fills to the wall the opening voids. Where the
16//! chain changes relationship it passes through objects of the project.
17//!
18//! A derived identity (`axioval:derived.…`) holds a colon of its own, so in
19//! the last alternative only a colon followed by a direction word ends it.
20
21use 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/// One parsed step of a relationship path.
34#[derive(Clone, Debug, PartialEq, Eq)]
35pub struct PathSegment {
36    relationships: Vec<SemanticRelationship>,
37    direction: TraversalDirection,
38    chain: bool,
39}
40
41impl PathSegment {
42    /// Parses `Relationship[|Relationship…][:direction][+]`.
43    ///
44    /// # Errors
45    ///
46    /// Returns why the text is no step: an empty alternative, one named
47    /// twice, a direction other than `forward`, `backward` or `either`, or a
48    /// direction written after an alternative other than the last.
49    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    /// The relationships the step may take, in the order written.
98    #[must_use]
99    pub fn relationships(&self) -> &[SemanticRelationship] {
100        &self.relationships
101    }
102
103    /// The direction every alternative is taken in.
104    #[must_use]
105    pub fn direction(&self) -> TraversalDirection {
106        self.direction
107    }
108
109    /// Whether the step is taken one or more times (a trailing `+`).
110    #[must_use]
111    pub fn chain(&self) -> bool {
112        self.chain
113    }
114
115    /// The same step taken the other way.
116    #[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    /// How messages name the step's relationships: `A` or `A|B`.
130    #[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    /// The objects of `scope` the step reaches from `from`, taken once or,
140    /// with `chain`, one or more times, with the service's evidence.
141    ///
142    /// A chain through several relationships hops between objects of
143    /// `everything`, each hop following any alternative's own chain, and
144    /// keeps what lies in `scope`. `from` is never among the result.
145    ///
146    /// # Errors
147    ///
148    /// Returns the service's error for the first query it cannot answer
149    /// completely.
150    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(&current, 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}