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