Skip to main content

ifc_model/
value.rs

1//! The IFC value model — serialization-independent.
2//!
3//! # Why this lives in `ifc-model`, not in a codec
4//!
5//! IFC data arrives as STEP/SPF, ifcXML, and prospectively IFC-JSON. If the
6//! value type belonged to the STEP codec, every other codec would need its own
7//! parallel value type and cross-format conversion would be lossy by
8//! construction. The data model owns the values; codecs only translate.
9//!
10//! # Owned, not borrowed
11//!
12//! Values own their data rather than borrowing from an mmap. A model must
13//! outlive the bytes it was read from — otherwise it cannot be mutated and
14//! written back, which is the entire point of round-tripping.
15
16use std::sync::Arc;
17
18/// One attribute slot in an entity.
19///
20/// This is deliberately a *structural* representation: it records what the file
21/// said, not what it means. Interpretation is the job of the domain crates.
22#[derive(Debug, Clone, PartialEq)]
23pub enum Value {
24    /// `$` — the attribute is not set.
25    Null,
26    /// `*` — inherited/derived in a supertype; distinct from `$`.
27    Derived,
28    /// `.T.` / `.F.`
29    Bool(bool),
30    /// `.U.` — logical unknown, the third STEP boolean state.
31    LogicalUnknown,
32    /// An integer literal.
33    Integer(i64),
34    /// A real literal.
35    Real(f64),
36    /// A quoted string, already unescaped to UTF-8.
37    Text(Arc<str>),
38    /// A binary literal (`"0123ABC"`).
39    Binary(Arc<str>),
40    /// An unquoted enumeration constant such as `.ELEMENT.`
41    Enum(Arc<str>),
42    /// A reference to another entity by its in-file id (`#42`).
43    Ref(EntityId),
44    /// An ordered aggregate: list, set, array or bag.
45    List(Vec<Value>),
46    /// A typed wrapper such as `IFCLENGTHMEASURE(2.5)`.
47    Typed {
48        /// The declared type name, upper-cased as written.
49        type_name: Arc<str>,
50        /// The wrapped value.
51        value: Box<Value>,
52    },
53}
54
55/// An entity's identifier as it appeared in the file (`#42` → `42`).
56///
57/// Preserved verbatim so that a re-exported file keeps its original numbering;
58/// stable ids make diffs between two exports readable.
59#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
60pub struct EntityId(pub u64);
61
62impl std::fmt::Display for EntityId {
63    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
64        write!(f, "#{}", self.0)
65    }
66}
67
68impl Value {
69    /// The referenced entity, if this value is a reference.
70    pub fn as_ref_id(&self) -> Option<EntityId> {
71        match self {
72            Value::Ref(id) => Some(*id),
73            _ => None,
74        }
75    }
76
77    /// The boolean, if this value is `.T.` or `.F.`.
78    ///
79    /// `.U.` (logical unknown) returns `None`: it is a third state, not a
80    /// missing boolean, and collapsing it to either would lose that.
81    pub fn as_bool(&self) -> Option<bool> {
82        match self {
83            Value::Bool(b) => Some(*b),
84            _ => None,
85        }
86    }
87
88    /// The integer, if this value is an integer literal.
89    ///
90    /// Does not accept a real: IFC distinguishes `IfcInteger` from `IfcReal`,
91    /// and silently truncating 2.7 to 2 would invent precision the file did
92    /// not state.
93    pub fn as_i64(&self) -> Option<i64> {
94        match self {
95            Value::Integer(i) => Some(*i),
96            _ => None,
97        }
98    }
99
100    /// The text content, if this is a string.
101    pub fn as_text(&self) -> Option<&str> {
102        match self {
103            Value::Text(s) => Some(s),
104            _ => None,
105        }
106    }
107
108    /// The numeric value, accepting either integer or real.
109    ///
110    /// IFC files are inconsistent about writing `1` versus `1.0` for the same
111    /// attribute, so a consumer that insists on one variant will misread real
112    /// files.
113    pub fn as_f64(&self) -> Option<f64> {
114        match self {
115            Value::Real(r) => Some(*r),
116            Value::Integer(i) => Some(*i as f64),
117            _ => None,
118        }
119    }
120
121    /// The items, if this is an aggregate.
122    pub fn as_list(&self) -> Option<&[Value]> {
123        match self {
124            Value::List(items) => Some(items),
125            _ => None,
126        }
127    }
128
129    /// Unwrap a [`Value::Typed`] to the value inside, otherwise `self`.
130    ///
131    /// Callers almost always want the payload; the type name matters only to
132    /// validation and to writers.
133    pub fn unwrap_typed(&self) -> &Value {
134        match self {
135            Value::Typed { value, .. } => value.unwrap_typed(),
136            other => other,
137        }
138    }
139
140    /// Visit every entity reference reachable from this value.
141    ///
142    /// References nest arbitrarily deep inside aggregates, so link rewriting
143    /// and integrity checks need a recursive walk rather than a shallow scan.
144    pub fn for_each_ref(&self, f: &mut impl FnMut(EntityId)) {
145        match self {
146            Value::Ref(id) => f(*id),
147            Value::List(items) => items.iter().for_each(|v| v.for_each_ref(f)),
148            Value::Typed { value, .. } => value.for_each_ref(f),
149            _ => {}
150        }
151    }
152}