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}