Skip to main content

ifc_model/
entity.rs

1//! The entity record — a type name plus positional attributes.
2//!
3//! # Why entities are not Rust structs
4//!
5//! IFC4 declares 776 entity types; IFC4x3 declares 876, and renames some of
6//! IFC4's. Generating a struct per entity per schema version is what makes
7//! IfcOpenShell heavy, and it forces a recompile to support a new schema.
8//!
9//! Here an entity is a type name and an attribute vector. The schema explains
10//! what the slots mean; domain crates interpret them. The immediate benefit is
11//! requirement 3 of the design: **an entity whose type nothing understands is
12//! still stored perfectly and written back unchanged.**
13
14use crate::value::{EntityId, Value};
15use std::sync::Arc;
16
17/// One IFC entity instance.
18///
19/// # Reading the type name
20///
21/// `type_name` is a public field, not a method — there is no
22/// `entity.type_name()`. Access it directly:
23///
24/// ```
25/// # use ifc_model::Entity;
26/// let entity = Entity::new("IFCWALL", vec![]);
27/// assert_eq!(&*entity.type_name, "IFCWALL");
28/// ```
29#[derive(Debug, Clone, PartialEq)]
30pub struct Entity {
31    /// Upper-cased type name exactly as it appeared (`IFCWALL`).
32    ///
33    /// Case is normalized because STEP is case-insensitive for keywords but
34    /// real files are inconsistent; comparing normalized names avoids a class
35    /// of silent lookup misses.
36    pub type_name: Arc<str>,
37    /// Positional attributes, in declaration order.
38    ///
39    /// Order is significant: STEP records are positional, so slot 3 of
40    /// `IFCWALL` is its name regardless of what any other entity looks like.
41    pub attributes: Vec<Value>,
42}
43
44impl Entity {
45    /// Build an entity from a type name and its attributes.
46    pub fn new(type_name: impl Into<Arc<str>>, attributes: Vec<Value>) -> Self {
47        Self {
48            type_name: type_name.into(),
49            attributes,
50        }
51    }
52
53    /// Attribute at `index`, or `None` when the slot does not exist.
54    ///
55    /// Returns `None` rather than panicking because real files are routinely
56    /// short a trailing optional attribute, and a reader that panics on that
57    /// is useless in practice.
58    pub fn attribute(&self, index: usize) -> Option<&Value> {
59        self.attributes.get(index)
60    }
61
62    /// Attribute at `index` interpreted as text.
63    pub fn text(&self, index: usize) -> Option<&str> {
64        self.attribute(index)?.unwrap_typed().as_text()
65    }
66
67    /// Attribute at `index` interpreted as a number.
68    pub fn number(&self, index: usize) -> Option<f64> {
69        self.attribute(index)?.unwrap_typed().as_f64()
70    }
71
72    /// Attribute at `index` interpreted as an entity reference.
73    pub fn reference(&self, index: usize) -> Option<EntityId> {
74        self.attribute(index)?.as_ref_id()
75    }
76
77    /// Every entity this one refers to, at any nesting depth.
78    pub fn references(&self) -> Vec<EntityId> {
79        let mut out = Vec::new();
80        for attr in &self.attributes {
81            attr.for_each_ref(&mut |id| out.push(id));
82        }
83        out
84    }
85
86    /// Case-insensitive type-name test.
87    pub fn is_type(&self, name: &str) -> bool {
88        self.type_name.eq_ignore_ascii_case(name)
89    }
90}