ifc-classification 0.2.2

Classification systems, document references, libraries, external references.
Documentation
//! Bounded classification hierarchy and explicit occurrence/type lookup.

use std::collections::HashMap;

use ifc_model::{Budget, EntityId};

use crate::view::{required_ref, required_refs};
use crate::{
    ClassificationAssignment, ClassificationError, ClassificationReference, ClassificationResult,
    ClassificationSystem, ClassificationView,
};

/// Chain of `IfcClassificationReference` nodes from a leaf up to its root, plus the terminating `IfcClassification` system if the chain resolved fully.
#[derive(Debug, Clone)]
pub struct ClassificationHierarchy<'m> {
    /// References from the queried leaf (index 0) up through its ancestors, in traversal order.
    pub references: Vec<ClassificationReference<'m>>,
    /// The root `IfcClassification` system, when the chain terminated there rather than at an unlinked reference.
    pub system: Option<ClassificationSystem<'m>>,
}

/// Classification assignments effective on an object: those directly assigned plus, when the object has an `IfcRelDefinesByType` type, those inherited from that type.
#[derive(Debug, Clone)]
pub struct EffectiveClassifications<'m> {
    /// Assignments made directly on the queried occurrence object.
    pub occurrence: Vec<ClassificationAssignment<'m>>,
    /// Id of the object's `IfcRelDefinesByType` type, when unambiguous.
    pub type_object: Option<EntityId>,
    /// Assignments made on the type object, inherited by the occurrence.
    pub inherited: Vec<ClassificationAssignment<'m>>,
}

/// Check `target` against the bound release's `RelatingClassification`
/// type: `IfcClassificationSelect` in IFC4, `IfcClassificationNotationSelect`
/// (notation or reference) in IFC2X3.
fn require_select(
    view: ClassificationView<'_>,
    source: EntityId,
    target: EntityId,
) -> ClassificationResult<()> {
    const RELATION: &str = "IFCRELASSOCIATESCLASSIFICATION";
    let entity = view
        .model()
        .get(target)
        .ok_or(ClassificationError::DanglingReference {
            entity: RELATION,
            id: source,
            attribute: "RelatingClassification",
            target,
        })?;
    let release = view.release();
    if release.accepts(
        RELATION,
        source,
        "RelatingClassification",
        &entity.type_name,
    )? {
        Ok(())
    } else {
        Err(ClassificationError::ReferenceType {
            entity: RELATION,
            id: source,
            attribute: "RelatingClassification",
            target,
            expected: release.declared_type(RELATION, source, "RelatingClassification")?,
            actual: entity.type_name.to_string(),
        })
    }
}

impl<'m> ClassificationView<'m> {
    /// 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`.
    ///
    /// Every distinct entity resolved counts against `budget.max_nodes`,
    /// including the final `IfcClassification`, and every `ReferencedSource`
    /// edge followed against `budget.max_depth`. Reaching an entity already on
    /// the path is reported as [`ClassificationError::Cycle`], never as a
    /// budget error.
    pub fn hierarchy_from(
        self,
        leaf: EntityId,
        budget: Budget,
    ) -> ClassificationResult<ClassificationHierarchy<'m>> {
        let mut current = leaf;
        let mut from_reference = None;
        let mut references: Vec<ClassificationReference<'m>> = Vec::new();
        let mut positions = HashMap::new();
        let mut visited_nodes = 0usize;
        let mut edges_followed = 0usize;

        loop {
            if let Some(&start) = positions.get(&current) {
                let mut path: Vec<_> = references[start..].iter().map(|r| r.id()).collect();
                path.push(current);
                return Err(ClassificationError::Cycle { path });
            }
            if visited_nodes >= budget.max_nodes {
                return Err(ClassificationError::BudgetExceeded {
                    max_depth: budget.max_depth,
                    max_nodes: budget.max_nodes,
                });
            }
            let entity = self.model().get(current).ok_or_else(|| {
                from_reference.map_or(
                    ClassificationError::UnknownEntity { id: current },
                    |source| ClassificationError::DanglingReference {
                        entity: "IFCCLASSIFICATIONREFERENCE",
                        id: source,
                        attribute: "ReferencedSource",
                        target: current,
                    },
                )
            })?;
            visited_nodes += 1;

            // A followed `ReferencedSource` must satisfy the bound release's
            // declared type: `IfcClassificationReferenceSelect` in IFC4,
            // `IfcClassification` only in IFC2X3 (no reference-to-reference
            // chains there).
            if let Some(source) = from_reference {
                let release = self.release();
                if !release.accepts(
                    "IFCCLASSIFICATIONREFERENCE",
                    source,
                    "ReferencedSource",
                    &entity.type_name,
                )? {
                    return Err(ClassificationError::ReferenceType {
                        entity: "IFCCLASSIFICATIONREFERENCE",
                        id: source,
                        attribute: "ReferencedSource",
                        target: current,
                        expected: release.declared_type(
                            "IFCCLASSIFICATIONREFERENCE",
                            source,
                            "ReferencedSource",
                        )?,
                        actual: entity.type_name.to_string(),
                    });
                }
            }
            if entity.is_type("IFCCLASSIFICATION") && from_reference.is_some() {
                return Ok(ClassificationHierarchy {
                    references,
                    system: Some(ClassificationSystem::try_bound(
                        current,
                        entity,
                        self.release(),
                    )?),
                });
            }
            if !entity.is_type("IFCCLASSIFICATIONREFERENCE") {
                if let Some(source) = from_reference {
                    return Err(ClassificationError::ReferenceType {
                        entity: "IFCCLASSIFICATIONREFERENCE",
                        id: source,
                        attribute: "ReferencedSource",
                        target: current,
                        expected: "IfcClassificationReferenceSelect",
                        actual: entity.type_name.to_string(),
                    });
                }
                return Err(ClassificationError::WrongEntityType {
                    expected: "IFCCLASSIFICATIONREFERENCE",
                    actual: entity.type_name.to_string(),
                });
            }

            positions.insert(current, references.len());
            let reference = ClassificationReference::try_bound(current, entity, self.release())?;
            let source = reference.referenced_source_id()?;
            references.push(reference);
            let Some(source) = source else {
                return Ok(ClassificationHierarchy {
                    references,
                    system: None,
                });
            };
            if edges_followed >= budget.max_depth {
                return Err(ClassificationError::BudgetExceeded {
                    max_depth: budget.max_depth,
                    max_nodes: budget.max_nodes,
                });
            }
            edges_followed += 1;
            from_reference = Some(current);
            current = source;
        }
    }

    /// `IfcClassificationReference` instances whose `ReferencedSource` is `source`, sorted by id; fails if `source` is unknown or is not itself a classification or reference.
    pub fn children_of(
        self,
        source: EntityId,
    ) -> ClassificationResult<Vec<ClassificationReference<'m>>> {
        let source_entity = self
            .model()
            .get(source)
            .ok_or(ClassificationError::UnknownEntity { id: source })?;
        let release = self.release();
        if !release.accepts(
            "IFCCLASSIFICATIONREFERENCE",
            source,
            "ReferencedSource",
            &source_entity.type_name,
        )? {
            return Err(ClassificationError::ReferenceType {
                entity: "IFCCLASSIFICATIONREFERENCE",
                id: source,
                attribute: "ReferencedSource",
                target: source,
                expected: release.declared_type(
                    "IFCCLASSIFICATIONREFERENCE",
                    source,
                    "ReferencedSource",
                )?,
                actual: source_entity.type_name.to_string(),
            });
        }
        let mut out = Vec::new();
        for reference in self.references() {
            if reference.referenced_source_id()? == Some(source) {
                out.push(reference);
            }
        }
        out.sort_by_key(|reference| reference.id());
        Ok(out)
    }

    /// 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.
    pub fn effective_classifications(
        self,
        object: EntityId,
    ) -> ClassificationResult<EffectiveClassifications<'m>> {
        if self.model().get(object).is_none() {
            return Err(ClassificationError::UnknownEntity { id: object });
        }
        let occurrence = self.classification_assignments_for(object)?;
        for assignment in &occurrence {
            require_select(
                self,
                assignment.id(),
                assignment.relating_classification_id()?,
            )?;
        }

        const TYPING: &str = "IFCRELDEFINESBYTYPE";
        let release = self.release();
        let mut types = Vec::new();
        for (relationship_id, entity) in self.model().of_type(TYPING) {
            if required_refs(
                TYPING,
                relationship_id,
                entity,
                release.slot(TYPING, relationship_id, "RelatedObjects")?,
                "RelatedObjects",
            )?
            .contains(&object)
            {
                let type_id = required_ref(
                    TYPING,
                    relationship_id,
                    entity,
                    release.slot(TYPING, relationship_id, "RelatingType")?,
                    "RelatingType",
                )?;
                self.model()
                    .get(type_id)
                    .ok_or(ClassificationError::DanglingReference {
                        entity: "IFCRELDEFINESBYTYPE",
                        id: relationship_id,
                        attribute: "RelatingType",
                        target: type_id,
                    })?;
                types.push(type_id);
            }
        }
        if types.len() > 1 {
            return Err(ClassificationError::AmbiguousType {
                object,
                count: types.len(),
            });
        }
        let type_id = types.first().copied();
        let inherited_from_type = if let Some(type_id) = type_id {
            let assignments = self.classification_assignments_for(type_id)?;
            for assignment in &assignments {
                require_select(
                    self,
                    assignment.id(),
                    assignment.relating_classification_id()?,
                )?;
            }
            assignments
        } else {
            Vec::new()
        };
        Ok(EffectiveClassifications {
            occurrence,
            type_object: type_id,
            inherited: inherited_from_type,
        })
    }
}