Skip to main content

ifc_classification/query/
hierarchy.rs

1//! Bounded classification hierarchy and explicit occurrence/type lookup.
2
3use std::collections::HashMap;
4
5use ifc_model::{Budget, EntityId};
6
7use crate::view::{required_ref, required_refs};
8use crate::{
9    ClassificationAssignment, ClassificationError, ClassificationReference, ClassificationResult,
10    ClassificationSystem, ClassificationView,
11};
12
13/// Chain of `IfcClassificationReference` nodes from a leaf up to its root, plus the terminating `IfcClassification` system if the chain resolved fully.
14#[derive(Debug, Clone)]
15#[non_exhaustive]
16pub struct ClassificationHierarchy<'m> {
17    /// References from the queried leaf (index 0) up through its ancestors, in traversal order.
18    pub references: Vec<ClassificationReference<'m>>,
19    /// The root `IfcClassification` system, when the chain terminated there rather than at an unlinked reference.
20    pub system: Option<ClassificationSystem<'m>>,
21}
22
23/// Classification assignments effective on an object: those directly assigned plus, when the object has an `IfcRelDefinesByType` type, those inherited from that type.
24#[derive(Debug, Clone)]
25#[non_exhaustive]
26pub struct EffectiveClassifications<'m> {
27    /// Assignments made directly on the queried occurrence object.
28    pub occurrence: Vec<ClassificationAssignment<'m>>,
29    /// Id of the object's `IfcRelDefinesByType` type, when unambiguous.
30    pub type_object: Option<EntityId>,
31    /// Assignments made on the type object, inherited by the occurrence.
32    pub inherited: Vec<ClassificationAssignment<'m>>,
33}
34
35/// Check `target` against the bound release's `RelatingClassification`
36/// type: `IfcClassificationSelect` in IFC4, `IfcClassificationNotationSelect`
37/// (notation or reference) in IFC2X3.
38fn require_select(
39    view: ClassificationView<'_>,
40    source: EntityId,
41    target: EntityId,
42) -> ClassificationResult<()> {
43    const RELATION: &str = "IFCRELASSOCIATESCLASSIFICATION";
44    let entity = view
45        .model()
46        .get(target)
47        .ok_or(ClassificationError::DanglingReference {
48            entity: RELATION,
49            id: source,
50            attribute: "RelatingClassification",
51            target,
52        })?;
53    let release = view.release();
54    if release.accepts(
55        RELATION,
56        source,
57        "RelatingClassification",
58        &entity.type_name,
59    )? {
60        Ok(())
61    } else {
62        Err(ClassificationError::ReferenceType {
63            entity: RELATION,
64            id: source,
65            attribute: "RelatingClassification",
66            target,
67            expected: release.declared_type(RELATION, source, "RelatingClassification")?,
68            actual: entity.type_name.to_string(),
69        })
70    }
71}
72
73impl<'m> ClassificationView<'m> {
74    /// Walk `ReferencedSource` links from `leaf` up to its root, stopping at the enclosing `IfcClassification` if present; fails on a cycle, a dangling or mistyped source, or if the walk exceeds `budget`.
75    ///
76    /// Every distinct entity resolved counts against `budget.max_nodes`,
77    /// including the final `IfcClassification`, and every `ReferencedSource`
78    /// edge followed against `budget.max_depth`. Reaching an entity already on
79    /// the path is reported as [`ClassificationError::Cycle`], never as a
80    /// budget error.
81    pub fn hierarchy_from(
82        self,
83        leaf: EntityId,
84        budget: Budget,
85    ) -> ClassificationResult<ClassificationHierarchy<'m>> {
86        let mut current = leaf;
87        let mut from_reference = None;
88        let mut references: Vec<ClassificationReference<'m>> = Vec::new();
89        let mut positions = HashMap::new();
90        let mut visited_nodes = 0usize;
91        let mut edges_followed = 0usize;
92
93        loop {
94            if let Some(&start) = positions.get(&current) {
95                let mut path: Vec<_> = references[start..].iter().map(|r| r.id()).collect();
96                path.push(current);
97                return Err(ClassificationError::Cycle { path });
98            }
99            if visited_nodes >= budget.max_nodes {
100                return Err(ClassificationError::BudgetExceeded {
101                    max_depth: budget.max_depth,
102                    max_nodes: budget.max_nodes,
103                });
104            }
105            let entity = self.model().get(current).ok_or_else(|| {
106                from_reference.map_or(
107                    ClassificationError::UnknownEntity { id: current },
108                    |source| ClassificationError::DanglingReference {
109                        entity: "IFCCLASSIFICATIONREFERENCE",
110                        id: source,
111                        attribute: "ReferencedSource",
112                        target: current,
113                    },
114                )
115            })?;
116            visited_nodes += 1;
117
118            // A followed `ReferencedSource` must satisfy the bound release's
119            // declared type: `IfcClassificationReferenceSelect` in IFC4,
120            // `IfcClassification` only in IFC2X3 (no reference-to-reference
121            // chains there).
122            if let Some(source) = from_reference {
123                let release = self.release();
124                if !release.accepts(
125                    "IFCCLASSIFICATIONREFERENCE",
126                    source,
127                    "ReferencedSource",
128                    &entity.type_name,
129                )? {
130                    return Err(ClassificationError::ReferenceType {
131                        entity: "IFCCLASSIFICATIONREFERENCE",
132                        id: source,
133                        attribute: "ReferencedSource",
134                        target: current,
135                        expected: release.declared_type(
136                            "IFCCLASSIFICATIONREFERENCE",
137                            source,
138                            "ReferencedSource",
139                        )?,
140                        actual: entity.type_name.to_string(),
141                    });
142                }
143            }
144            if entity.is_type("IFCCLASSIFICATION") && from_reference.is_some() {
145                return Ok(ClassificationHierarchy {
146                    references,
147                    system: Some(ClassificationSystem::try_bound(
148                        current,
149                        entity,
150                        self.release(),
151                    )?),
152                });
153            }
154            if !entity.is_type("IFCCLASSIFICATIONREFERENCE") {
155                if let Some(source) = from_reference {
156                    return Err(ClassificationError::ReferenceType {
157                        entity: "IFCCLASSIFICATIONREFERENCE",
158                        id: source,
159                        attribute: "ReferencedSource",
160                        target: current,
161                        expected: "IfcClassificationReferenceSelect",
162                        actual: entity.type_name.to_string(),
163                    });
164                }
165                return Err(ClassificationError::WrongEntityType {
166                    expected: "IFCCLASSIFICATIONREFERENCE",
167                    actual: entity.type_name.to_string(),
168                });
169            }
170
171            positions.insert(current, references.len());
172            let reference = ClassificationReference::try_bound(current, entity, self.release())?;
173            let source = reference.referenced_source_id()?;
174            references.push(reference);
175            let Some(source) = source else {
176                return Ok(ClassificationHierarchy {
177                    references,
178                    system: None,
179                });
180            };
181            if edges_followed >= budget.max_depth {
182                return Err(ClassificationError::BudgetExceeded {
183                    max_depth: budget.max_depth,
184                    max_nodes: budget.max_nodes,
185                });
186            }
187            edges_followed += 1;
188            from_reference = Some(current);
189            current = source;
190        }
191    }
192
193    /// `IfcClassificationReference` instances whose `ReferencedSource` is `source`, sorted by id; fails if `source` is unknown or is not itself a classification or reference.
194    pub fn children_of(
195        self,
196        source: EntityId,
197    ) -> ClassificationResult<Vec<ClassificationReference<'m>>> {
198        let source_entity = self
199            .model()
200            .get(source)
201            .ok_or(ClassificationError::UnknownEntity { id: source })?;
202        let release = self.release();
203        if !release.accepts(
204            "IFCCLASSIFICATIONREFERENCE",
205            source,
206            "ReferencedSource",
207            &source_entity.type_name,
208        )? {
209            return Err(ClassificationError::ReferenceType {
210                entity: "IFCCLASSIFICATIONREFERENCE",
211                id: source,
212                attribute: "ReferencedSource",
213                target: source,
214                expected: release.declared_type(
215                    "IFCCLASSIFICATIONREFERENCE",
216                    source,
217                    "ReferencedSource",
218                )?,
219                actual: source_entity.type_name.to_string(),
220            });
221        }
222        let mut out = Vec::new();
223        for reference in self.references() {
224            if reference.referenced_source_id()? == Some(source) {
225                out.push(reference);
226            }
227        }
228        out.sort_by_key(|reference| reference.id());
229        Ok(out)
230    }
231
232    /// Direct classification assignments on `object`, plus any inherited from its unambiguous `IfcRelDefinesByType` type; fails if `object` is unknown, has an assignment with an invalid relating classification, or is related to more than one type.
233    pub fn effective_classifications(
234        self,
235        object: EntityId,
236    ) -> ClassificationResult<EffectiveClassifications<'m>> {
237        if self.model().get(object).is_none() {
238            return Err(ClassificationError::UnknownEntity { id: object });
239        }
240        let occurrence = self.classification_assignments_for(object)?;
241        for assignment in &occurrence {
242            require_select(
243                self,
244                assignment.id(),
245                assignment.relating_classification_id()?,
246            )?;
247        }
248
249        const TYPING: &str = "IFCRELDEFINESBYTYPE";
250        let release = self.release();
251        let mut types = Vec::new();
252        for (relationship_id, entity) in self.model().of_type(TYPING) {
253            if required_refs(
254                TYPING,
255                relationship_id,
256                entity,
257                release.slot(TYPING, relationship_id, "RelatedObjects")?,
258                "RelatedObjects",
259            )?
260            .contains(&object)
261            {
262                let type_id = required_ref(
263                    TYPING,
264                    relationship_id,
265                    entity,
266                    release.slot(TYPING, relationship_id, "RelatingType")?,
267                    "RelatingType",
268                )?;
269                self.model()
270                    .get(type_id)
271                    .ok_or(ClassificationError::DanglingReference {
272                        entity: "IFCRELDEFINESBYTYPE",
273                        id: relationship_id,
274                        attribute: "RelatingType",
275                        target: type_id,
276                    })?;
277                types.push(type_id);
278            }
279        }
280        if types.len() > 1 {
281            return Err(ClassificationError::AmbiguousType {
282                object,
283                count: types.len(),
284            });
285        }
286        let type_id = types.first().copied();
287        let inherited_from_type = if let Some(type_id) = type_id {
288            let assignments = self.classification_assignments_for(type_id)?;
289            for assignment in &assignments {
290                require_select(
291                    self,
292                    assignment.id(),
293                    assignment.relating_classification_id()?,
294                )?;
295            }
296            assignments
297        } else {
298            Vec::new()
299        };
300        Ok(EffectiveClassifications {
301            occurrence,
302            type_object: type_id,
303            inherited: inherited_from_type,
304        })
305    }
306}