use std::collections::BTreeSet;
use axioval_ir::{Evidence, ObjectId};
use crate::derived_relationships::DERIVED_RELATIONSHIP_PREFIX;
use crate::relationships::{
AbsentEndPolicy, RelationshipQuery, RelationshipSelectionError, RelationshipSelectionRequest,
RelationshipSelectionServiceHandle, SemanticRelationship, TraversalDirection,
};
const DIRECTIONS: [&str; 3] = ["forward", "backward", "either"];
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PathSegment {
relationships: Vec<SemanticRelationship>,
direction: TraversalDirection,
chain: bool,
}
impl PathSegment {
pub fn parse(text: &str) -> Result<Self, String> {
let trimmed = text.trim();
let (body, chain) = match trimmed.strip_suffix('+') {
Some(body) => (body, true),
None => (trimmed, false),
};
let mut alternatives: Vec<&str> = body.split('|').collect();
let last = alternatives.pop().unwrap_or_default();
let derived = last.trim().starts_with(DERIVED_RELATIONSHIP_PREFIX);
let (last, stated) = match last.rsplit_once(':') {
Some((_, stated)) if derived && !DIRECTIONS.contains(&stated.trim()) => (last, None),
Some((relationship, stated)) => (relationship, Some(stated.trim())),
None => (last, None),
};
let direction = match stated {
None | Some("forward") => TraversalDirection::Forward,
Some("backward") => TraversalDirection::Backward,
Some("either") => TraversalDirection::Either,
Some(other) => return Err(format!("direction `{other}` is unsupported")),
};
alternatives.push(last);
let mut relationships: Vec<SemanticRelationship> = Vec::new();
for alternative in alternatives {
let alternative = alternative.trim();
if let Some((_, stated)) = alternative.rsplit_once(':')
&& DIRECTIONS.contains(&stated.trim())
{
return Err(format!(
"step `{trimmed}` states a direction inside `{alternative}`; one direction \
follows the last alternative and applies to all"
));
}
let relationship = SemanticRelationship::try_new(alternative)
.map_err(|_| format!("step `{trimmed}` names an empty relationship"))?;
if relationships.contains(&relationship) {
return Err(format!(
"step `{trimmed}` names `{alternative}` more than once"
));
}
relationships.push(relationship);
}
Ok(Self {
relationships,
direction,
chain,
})
}
#[must_use]
pub fn relationships(&self) -> &[SemanticRelationship] {
&self.relationships
}
#[must_use]
pub fn direction(&self) -> TraversalDirection {
self.direction
}
#[must_use]
pub fn chain(&self) -> bool {
self.chain
}
#[must_use]
pub fn reversed(&self) -> Self {
Self {
relationships: self.relationships.clone(),
direction: match self.direction {
TraversalDirection::Forward => TraversalDirection::Backward,
TraversalDirection::Backward => TraversalDirection::Forward,
TraversalDirection::Either => TraversalDirection::Either,
},
chain: self.chain,
}
}
#[must_use]
pub fn shown(&self) -> String {
self.relationships
.iter()
.map(SemanticRelationship::as_str)
.collect::<Vec<_>>()
.join("|")
}
pub fn walk(
&self,
service: &RelationshipSelectionServiceHandle,
from: &ObjectId,
everything: &[ObjectId],
scope: &[ObjectId],
chain: bool,
absent_ends: AbsentEndPolicy,
) -> Result<(BTreeSet<ObjectId>, Vec<Evidence>), RelationshipSelectionError> {
let mut evidence = Vec::new();
let mut hop = |anchor: &ObjectId,
universe: &[ObjectId],
relationship: &SemanticRelationship,
follow_chain: bool|
-> Result<Vec<ObjectId>, RelationshipSelectionError> {
let request = RelationshipSelectionRequest::try_new(
anchor.clone(),
universe.to_vec(),
RelationshipQuery::Related {
relationship: relationship.clone(),
direction: self.direction,
follow_chain,
},
)?
.with_absent_ends(absent_ends);
let selection = service.select(&request)?;
evidence.extend(selection.evidence().iter().cloned());
Ok(selection.candidates().to_vec())
};
let mut reached = BTreeSet::new();
if !chain || self.relationships.len() == 1 {
for relationship in &self.relationships {
reached.extend(hop(from, scope, relationship, chain)?);
}
} else {
let mut seen = BTreeSet::from([from.clone()]);
let mut frontier = vec![from.clone()];
while let Some(current) = frontier.pop() {
for relationship in &self.relationships {
for object in hop(¤t, everything, relationship, true)? {
if seen.insert(object.clone()) {
frontier.push(object);
}
}
}
}
seen.remove(from);
let scope: BTreeSet<&ObjectId> = scope.iter().collect();
reached.extend(seen.into_iter().filter(|object| scope.contains(object)));
}
reached.remove(from);
Ok((reached, evidence))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn names(step: &PathSegment) -> Vec<&str> {
step.relationships()
.iter()
.map(SemanticRelationship::as_str)
.collect()
}
#[test]
fn a_step_names_one_or_several_relationships() {
let step = PathSegment::parse("IfcRelAggregates").unwrap();
assert_eq!(names(&step), ["IfcRelAggregates"]);
assert_eq!(step.direction(), TraversalDirection::Forward);
assert!(!step.chain());
let step =
PathSegment::parse(" IfcRelFillsElement | IfcRelVoidsElement:backward+ ").unwrap();
assert_eq!(names(&step), ["IfcRelFillsElement", "IfcRelVoidsElement"]);
assert_eq!(step.direction(), TraversalDirection::Backward);
assert!(step.chain());
assert_eq!(step.shown(), "IfcRelFillsElement|IfcRelVoidsElement");
assert_eq!(step.reversed().direction(), TraversalDirection::Forward);
}
#[test]
fn derived_identities_keep_their_colons() {
let step = PathSegment::parse("axioval:derived.adjacent-space;reach=1").unwrap();
assert_eq!(names(&step), ["axioval:derived.adjacent-space;reach=1"]);
let step =
PathSegment::parse("IfcRelNests|axioval:derived.same-level;by=name:either").unwrap();
assert_eq!(
names(&step),
["IfcRelNests", "axioval:derived.same-level;by=name"]
);
assert_eq!(step.direction(), TraversalDirection::Either);
let step = PathSegment::parse("axioval:derived.intersects|IfcRelNests").unwrap();
assert_eq!(names(&step), ["axioval:derived.intersects", "IfcRelNests"]);
}
#[test]
fn malformed_steps_are_refused() {
for text in [
"",
"IfcRelNests|",
"|IfcRelNests",
"IfcRelNests||IfcRelAggregates",
"IfcRelNests|IfcRelNests",
"IfcRelNests:sideways",
"IfcRelNests:backward|IfcRelAggregates",
"IfcRelNests:backward|IfcRelAggregates:backward",
] {
assert!(PathSegment::parse(text).is_err(), "{text:?} parsed");
}
}
}