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}