ifc_properties/exact.rs
1//! Exact, fail-closed IFC2X3/IFC4/IFC4X3 property resolution.
2//!
3//! Unlike the permissive views, this traversal rejects any incomplete or
4//! malformed assignment data before it can claim an exact absence. Every
5//! structural fact comes from the table of the one release the header
6//! declares; nothing is aliased across releases.
7//!
8//! # Exact versus permissive
9//!
10//! The permissive views (`property_set`, `quantity_sets`, `query`) are for
11//! interactive inspection. A rule engine, validator or checker uses this
12//! exact API and must never map one of its errors to "absent": an error
13//! means the file could not prove the answer, which is not the same as the
14//! property being missing.
15//!
16//! Exact values keep their declared IFC value type and explicit unit
17//! identity. A downstream adapter that cannot project a value category or
18//! unit losslessly into its own model must reject it rather than coerce it.
19
20mod assignment;
21mod complex;
22mod composite;
23mod enumerate;
24mod index;
25mod material;
26mod measure;
27mod predefined;
28mod quantity;
29mod refs;
30mod release;
31mod set;
32mod unit;
33mod value;
34mod values;
35
36use std::{fmt, sync::Arc};
37
38use ifc_model::{EntityId, Model};
39use ifc_schema::SchemaVersion;
40
41use assignment::{assigned_sets, Relations};
42pub use enumerate::{
43 exact_properties, exact_properties_where, exact_property_sets_where, ExactPropertyEntry,
44 ExactPropertySetEntry,
45};
46pub use index::PropertyIndex;
47pub use material::{
48 exact_material_properties_where, exact_material_property, exact_material_property_sets_where,
49};
50pub use predefined::{exact_predefined_sets, ExactPredefinedSet};
51use release::{validate_model, Release};
52use set::find_property;
53pub use unit::{exact_unit, ExactUnit, ExactUnitError};
54pub use values::{
55 ExactBoundedValue, ExactComplexMember, ExactComplexValue, ExactEntityRef, ExactEnumeratedValue,
56 ExactEnumeration, ExactReferenceValue, ExactTableRow, ExactTableValue, ExactTypedValue,
57};
58
59/// Provenance of an exact result.
60#[derive(Debug, Clone, Copy, PartialEq, Eq)]
61#[non_exhaustive]
62pub enum ExactSource {
63 /// Resolved from a property set assigned directly to the occurrence
64 /// via `IfcRelDefinesByProperties`.
65 Occurrence,
66 /// Resolved from a set in `HasPropertySets` of the `IfcTypeObject` with
67 /// this entity id: inherited through the occurrence's type, or, when a
68 /// type object is queried, that object's own set (#193), so the id is
69 /// then the queried object's.
70 Type(EntityId),
71 /// Resolved from a material property set of the material definition
72 /// with this entity id (#218): an `IfcMaterialProperties`, or in IFC2X3
73 /// one of its subtypes, whose `Material` is that definition.
74 Material(EntityId),
75}
76
77/// Exact IFC logical value without collapsing unknown into a boolean.
78#[derive(Debug, Clone, Copy, PartialEq, Eq)]
79pub enum ExactLogical {
80 /// `IfcLogical` value `.F.`.
81 False,
82 /// `IfcLogical` value `.U.` — genuinely undetermined, not absent.
83 Unknown,
84 /// `IfcLogical` value `.T.`.
85 True,
86}
87
88/// Values accepted by the exact resolver.
89///
90/// An `IfcPropertySingleValue` or a simple quantity resolves to one of the
91/// scalar variants. The other `IfcSimpleProperty` kinds resolve to a
92/// composite variant whose scalars carry their own declared types
93/// ([`ExactTypedValue`]), and a complex property or quantity to
94/// [`ExactValue::Complex`]; for those, [`ExactProperty::value_type`] is
95/// `None`.
96#[derive(Debug, Clone, PartialEq)]
97#[non_exhaustive]
98pub enum ExactValue {
99 /// The property carried no value (`$`), distinct from an absent property.
100 Null,
101 /// An `IFCBOOLEAN` payload.
102 Bool(bool),
103 /// An `IFCLOGICAL` payload, keeping the tri-state distinction.
104 Logical(ExactLogical),
105 /// An `IFCBINARY` payload, stored as its literal encoded text.
106 Binary(Arc<str>),
107 /// An `IFCINTEGER` payload.
108 Integer(i64),
109 /// An `IFCREAL` (or compatible measure) payload; always finite.
110 Real(f64),
111 /// An `IFCTEXT`/`IFCLABEL`/`IFCIDENTIFIER`-family string payload.
112 Text(Arc<str>),
113 /// An `IfcPropertyEnumeratedValue`: its selected values and its
114 /// reference enumeration.
115 Enumerated(ExactEnumeratedValue),
116 /// An `IfcPropertyListValue`: its `ListValues` in file order, all of
117 /// one declared type; empty only when the attribute is `$` (IFC4 and
118 /// IFC4X3). The shared `Unit` is [`ExactProperty::unit_id`].
119 List(Vec<ExactTypedValue>),
120 /// An `IfcPropertyBoundedValue`: lower and upper bound and set point.
121 Bounded(Box<ExactBoundedValue>),
122 /// An `IfcPropertyTableValue`: its rows, expression, units and
123 /// interpolation.
124 Table(ExactTableValue),
125 /// An `IfcPropertyReferenceValue`: its usage name and target entity.
126 Reference(ExactReferenceValue),
127 /// An enumeration constant held by a predefined set's attribute, as
128 /// written without its dots, e.g. `SWINGING` for
129 /// `IfcDoorPanelProperties.PanelOperation`; always a member of the
130 /// declared enumeration in the bound release.
131 Enum(Arc<str>),
132 /// An entity held by a predefined set's attribute, e.g. the
133 /// `IfcShapeAspect` of `ShapeAspectStyle`; checked, not followed.
134 Entity(ExactEntityRef),
135 /// An `IfcComplexProperty` or `IfcPhysicalComplexQuantity` (#208): its
136 /// members, each resolved as a set member is. The complex is present
137 /// but is no value of any type, so [`ExactProperty::value_type`] and
138 /// [`ExactProperty::unit_id`] are `None`.
139 Complex(ExactComplexValue),
140}
141
142/// A uniquely resolved property with IFC identity and provenance.
143#[derive(Debug, Clone, PartialEq)]
144#[non_exhaustive]
145pub struct ExactProperty {
146 /// Whether the value came from the occurrence or from a type object's
147 /// `HasPropertySets` (inherited, or the queried type object's own).
148 pub source: ExactSource,
149 /// The owning `IfcPropertySet.Name` or `IfcElementQuantity.Name`; for a
150 /// predefined set its `Name`, or its entity name in the release's
151 /// spelling (`IfcDoorLiningProperties`) when it states none.
152 pub property_set: Arc<str>,
153 /// Entity id of the `IfcPropertySet`, `IfcElementQuantity` or
154 /// predefined set.
155 pub set_id: EntityId,
156 /// Entity id of the `IfcProperty` (single, enumerated, list, bounded,
157 /// table or reference value, or complex), or of the
158 /// `IfcPhysicalQuantity` (e.g. `IfcQuantityLength`, or a complex
159 /// quantity). For an attribute of a predefined set, which is no entity
160 /// of its own, the set's id.
161 pub property_id: EntityId,
162 /// Declared IFC value type (for example `IFCINTEGER` or `IFCLENGTHMEASURE`).
163 ///
164 /// `None` for a single value whose `NominalValue` is `$`, for a
165 /// composite value, whose scalars carry their own types, and for a
166 /// complex property or quantity, which has no value type at all.
167 ///
168 /// For a quantity it is the declared type of its value attribute in the
169 /// bound release (`LengthValue : IfcLengthMeasure` gives
170 /// `IFCLENGTHMEASURE`), since a quantity stores a bare number. For a
171 /// predefined set's attribute it is the attribute's declared type
172 /// (`LiningDepth : IfcPositiveLengthMeasure` gives
173 /// `IFCPOSITIVELENGTHMEASURE`), also when the value is `$`, except that a
174 /// select-typed attribute reports the member type the file wrote.
175 pub value_type: Option<Arc<str>>,
176 /// The explicit unit that applies to every value, if stated:
177 /// `IfcPropertySingleValue.Unit`, `IfcPropertyListValue.Unit`,
178 /// `IfcPropertyBoundedValue.Unit`, the `Unit` of an enumerated value's
179 /// `IfcPropertyEnumeration`, or `IfcPhysicalSimpleQuantity.Unit`.
180 /// A table's two units are in [`ExactTableValue`]; a reference value has
181 /// none.
182 pub unit_id: Option<EntityId>,
183 /// The resolved `NominalValue`, composite value, or the quantity's value.
184 pub value: ExactValue,
185}
186
187/// Exact lookup result.
188#[derive(Debug, Clone, PartialEq)]
189#[non_exhaustive]
190pub enum ExactResolution {
191 /// The property was found exactly once across occurrence and inherited sets.
192 Present(ExactProperty),
193 /// No occurrence or inherited property set, quantity set or predefined
194 /// set carried a matching property, quantity or attribute; this is a
195 /// proven absence, not a lookup failure.
196 ///
197 /// A predefined set's attribute that is `$` is not absent: it is
198 /// `Present` with [`ExactValue::Null`].
199 Absent,
200}
201
202/// Why a property cannot be resolved exactly.
203#[derive(Debug, Clone, PartialEq, Eq)]
204#[non_exhaustive]
205pub enum ExactPropertyError {
206 /// The model carries STEP-level diagnostics, so exactness cannot be
207 /// guaranteed; count of diagnostics is reported for context.
208 IncompleteModel {
209 /// Number of diagnostics recorded against the model.
210 diagnostics: usize,
211 },
212 /// The model header declares no `FILE_SCHEMA`.
213 MissingSchema,
214 /// The model header declares more than one schema.
215 MultipleSchemas {
216 /// Number of schemas declared in the header.
217 schemas: usize,
218 },
219 /// The model header declares a schema other than IFC2X3, IFC4 or
220 /// IFC4X3 (`IFC4X3_ADD2`).
221 UnsupportedSchema {
222 /// The declared schema token.
223 schema: String,
224 },
225 /// An entity reference points at an id absent from the model.
226 MissingReference {
227 /// The entity holding the reference.
228 from: EntityId,
229 /// The entity id that could not be resolved.
230 to: EntityId,
231 },
232 /// An aggregate attribute (list/set) was `$`, empty when required, or
233 /// not encoded as a list at all.
234 MalformedAggregate {
235 /// The entity whose attribute was malformed.
236 entity: EntityId,
237 /// Name of the offending attribute.
238 attribute: &'static str,
239 },
240 /// The occurrence is assigned to more than one `IfcTypeObject` via
241 /// `IfcRelDefinesByType`, which IFC forbids.
242 MultipleTypeAssignments {
243 /// The occurrence with conflicting type assignments.
244 object: EntityId,
245 /// The first `IfcTypeObject` found.
246 first: EntityId,
247 /// The second, conflicting `IfcTypeObject` found.
248 second: EntityId,
249 },
250 /// `IfcRelDefinesByProperties.RelatedObjects` names an object that is
251 /// not a non-type `IfcObjectDefinition`. A type object there is refused
252 /// even when it is the queried object: its sets belong in its
253 /// `HasPropertySets` (IFC4 and IFC4X3 `NoRelatedTypeObject`; IFC2X3
254 /// admits only `IfcObject`).
255 InvalidOccurrenceTarget {
256 /// The `IfcRelDefinesByProperties` relationship.
257 relationship: EntityId,
258 /// The invalid related object.
259 object: EntityId,
260 },
261 /// `IfcRelDefinesByType.RelatedObjects` names an object that is not an
262 /// `IfcObject`.
263 InvalidTypeTarget {
264 /// The `IfcRelDefinesByType` relationship.
265 relationship: EntityId,
266 /// The invalid related object.
267 object: EntityId,
268 },
269 /// The queried entity can carry no property sets in the declared
270 /// release: it is neither an object `IfcRelDefinesByProperties` may
271 /// relate (`IfcObject` in IFC2X3, a non-type `IfcObjectDefinition` in
272 /// IFC4 and IFC4X3) nor an `IfcTypeObject` (accepted since #193). For
273 /// a material query (#218): the entity is no material definition the
274 /// release's `IfcMaterialProperties.Material` accepts.
275 InvalidQueryObject {
276 /// The rejected query object.
277 object: EntityId,
278 /// The object's actual IFC type name.
279 type_name: Arc<str>,
280 },
281 /// The same entity id appears more than once in an aggregate attribute
282 /// that must have unique members.
283 DuplicateAggregateMember {
284 /// The entity holding the aggregate.
285 entity: EntityId,
286 /// Name of the offending attribute.
287 attribute: &'static str,
288 /// The entity id that appeared more than once.
289 member: EntityId,
290 },
291 /// Two property sets with the same name matched the query for the same
292 /// source (occurrence or type), making the result ambiguous.
293 DuplicateMatchingSets {
294 /// Whether the ambiguity arose among occurrence or type sets.
295 source: ExactSource,
296 /// The first matching property set.
297 first: EntityId,
298 /// The second, conflicting matching property set.
299 second: EntityId,
300 },
301 /// Two properties with the same name matched within the same set.
302 DuplicateMatchingProperties {
303 /// The `IfcPropertySet` containing the ambiguous properties.
304 set: EntityId,
305 /// The first matching property.
306 first: EntityId,
307 /// The second, conflicting matching property.
308 second: EntityId,
309 },
310 /// A `Name` attribute expected to be a string was `$`, a reference, or
311 /// otherwise not text.
312 MalformedName {
313 /// The entity whose name attribute was malformed.
314 entity: EntityId,
315 /// Name of the offending attribute (normally `"Name"`).
316 attribute: &'static str,
317 },
318 /// A `RelatingPropertyDefinition` reference resolves to an entity that
319 /// is not an `IfcPropertySetDefinition`; or a predefined property set
320 /// holds the requested name in an attribute this resolver cannot read
321 /// exactly (an aggregate, such as
322 /// `IfcReinforcementDefinitionProperties.ReinforcementSectionDefinitions`);
323 /// or a predefined set that states no `Name`, and so cannot be ruled out
324 /// by a set name, has an attribute of the requested name (#66).
325 UnsupportedDefinition {
326 /// The rejected entity.
327 entity: EntityId,
328 /// The entity's actual IFC type name.
329 type_name: Arc<str>,
330 },
331 /// A member of `IfcPropertySet.HasProperties` or
332 /// `IfcComplexProperty.HasProperties` is not an `IfcProperty`, or a
333 /// member of `IfcElementQuantity.Quantities` or
334 /// `IfcPhysicalComplexQuantity.HasQuantities` is not an
335 /// `IfcPhysicalQuantity`.
336 UnsupportedProperty {
337 /// The rejected entity.
338 entity: EntityId,
339 /// The entity's actual IFC type name.
340 type_name: Arc<str>,
341 },
342 /// A value the release requires was `$`: a quantity's value, or a
343 /// required value of another kind (IFC2X3
344 /// `IfcPropertyReferenceValue.PropertyReference`).
345 MissingValueSlot {
346 /// The property with the missing value.
347 property: EntityId,
348 },
349 /// An entity's attribute count does not match what the declared
350 /// release's schema declares for its type (a malformed or truncated STEP record).
351 MalformedEntitySlots {
352 /// The malformed entity.
353 entity: EntityId,
354 /// The entity's IFC type name.
355 type_name: Arc<str>,
356 /// Attribute count the schema declares for this type.
357 expected: usize,
358 /// Attribute count actually present on the entity.
359 actual: usize,
360 },
361 /// `IfcPropertySingleValue.Unit` references an entity that is not a
362 /// member of the `IfcUnit` select.
363 UnsupportedUnit {
364 /// The property with the invalid unit reference.
365 property: EntityId,
366 },
367 /// `IfcPropertySingleValue.NominalValue` carries a typed value whose
368 /// declared type is not accepted by `IFCVALUE`, or whose payload does
369 /// not match its declared type.
370 UnsupportedValue {
371 /// The property with the invalid value.
372 property: EntityId,
373 },
374 /// `IfcPropertySingleValue.NominalValue` is an `IFCREAL` that is NaN or
375 /// infinite, which IFC does not permit.
376 NonFiniteReal {
377 /// The property with the non-finite real value.
378 property: EntityId,
379 },
380 /// A traversed record, typed value, or select member names a construct
381 /// that the release declared in `FILE_SCHEMA` does not define, for
382 /// example an `IfcDoorType` or an `IfcPropertySetDefinitionSet` in an
383 /// IFC2X3 file. The file mixes releases, so nothing it says about the
384 /// property is trusted.
385 NotInSchema {
386 /// The entity holding or being the foreign construct.
387 entity: EntityId,
388 /// The construct's name as written in the file.
389 name: Arc<str>,
390 /// The release the header declares.
391 schema: SchemaVersion,
392 },
393 /// Values that the property's release constrains contradict one of
394 /// those constraints, so no one reading of them is exact: list members
395 /// or bounds of different types, table columns of unequal length, a
396 /// selected value missing from the referenced enumeration, or a repeated
397 /// member of a `LIST OF UNIQUE`.
398 InconsistentValues {
399 /// The property or `IfcPropertyEnumeration` holding the values.
400 entity: EntityId,
401 /// The release's label of the violated WHERE rule (for example
402 /// `WR31`, or `SameUnitUpperLower` in IFC4), or `<Attribute> UNIQUE`
403 /// for a repeated member of a unique list.
404 rule: &'static str,
405 },
406 /// The entity name given to [`exact_predefined_sets`] is not a
407 /// predefined property set in the declared release: not declared there
408 /// at all, or an `IfcPropertySet`, a quantity set, or one of their
409 /// supertypes.
410 NotAPredefinedSet {
411 /// The entity name as requested.
412 name: Arc<str>,
413 /// The release the header declares.
414 schema: SchemaVersion,
415 },
416 /// A proper subtype of `IfcRelDefinesByProperties` or
417 /// `IfcRelDefinesByType` relates the queried object, such as IFC2X3
418 /// `IfcRelOverridesProperties`. Its semantics change which value applies,
419 /// and the exact resolver does not interpret them, so it refuses rather
420 /// than answer as if the relationship were absent.
421 UnsupportedRelationship {
422 /// The relationship instance.
423 relationship: EntityId,
424 /// Its IFC type name.
425 type_name: Arc<str>,
426 },
427 /// A complex property or quantity reaches itself again through its
428 /// members (#208). The schema forbids only a direct self-member; a
429 /// longer cycle has no finite resolution either.
430 ComplexCycle {
431 /// The complex whose member list closes the cycle.
432 complex: EntityId,
433 /// The member already being resolved higher up the path.
434 member: EntityId,
435 },
436 /// A complex property or quantity nested deeper than the resolver
437 /// follows below one set member (#208).
438 ComplexTooDeep {
439 /// The complex whose members would exceed the depth.
440 complex: EntityId,
441 /// The nesting depth followed.
442 limit: usize,
443 },
444 /// Resolving one set member followed more nested member references
445 /// than its budget (#208). Members may be shared between complexes,
446 /// so a small file can expand into an enormous tree.
447 ComplexBudgetExceeded {
448 /// The complex whose member exceeded the budget.
449 complex: EntityId,
450 /// The nested member references followed.
451 limit: usize,
452 },
453}
454impl fmt::Display for ExactPropertyError {
455 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
456 write!(f, "exact IFC property resolution failed: {self:?}")
457 }
458}
459
460/// The IFC release a model's properties are resolved against.
461///
462/// This is the release `exact_property` binds to: the single `FILE_SCHEMA`
463/// token, if it names a release the exact resolver supports (IFC2X3 TC1,
464/// IFC4 ADD2 TC1 or IFC4X3 ADD2, the bundled tables). A consumer binds its vocabulary per release with this answer
465/// instead of re-parsing the header. It fails exactly as `exact_property`
466/// fails at model level: diagnostics, no schema, several schemas, or an
467/// unsupported one.
468///
469/// # Errors
470///
471/// [`ExactPropertyError::IncompleteModel`], [`ExactPropertyError::MissingSchema`],
472/// [`ExactPropertyError::MultipleSchemas`], or
473/// [`ExactPropertyError::UnsupportedSchema`].
474pub fn exact_schema(model: &Model) -> Result<SchemaVersion, ExactPropertyError> {
475 validate_model(model).map(|release| release.version)
476}
477
478impl std::error::Error for ExactPropertyError {}
479
480/// Resolve a property or quantity by exact set and property name.
481///
482/// Single, enumerated, list, bounded, table and reference values resolve,
483/// the last five as composite [`ExactValue`]s. An `IfcComplexProperty` or
484/// `IfcPhysicalComplexQuantity` resolves as [`ExactValue::Complex`]: present,
485/// with no value type (#208).
486///
487/// A predefined property set (`IfcDoorLiningProperties` and the like) is
488/// searched too: its members are the attributes its entity declares, by
489/// schema name (`LiningDepth`), and its set name is its `Name`, or its
490/// entity name (`IfcDoorLiningProperties`) when it states none. A door with
491/// one `IfcDoorPanelProperties` per leaf is ambiguous here; list such sets
492/// with [`exact_predefined_sets`].
493///
494/// The model is resolved against the single release its `FILE_SCHEMA`
495/// declares, IFC2X3, IFC4 or IFC4X3 (see [`exact_schema`]); every domain,
496/// select and slot count is that release's. With `set_name == None`, all
497/// assigned sets are searched. Occurrence values override matching inherited
498/// values at property level. To enumerate every property instead of naming
499/// one, use [`exact_properties`].
500///
501/// `object` may also be an `IfcTypeObject` of the release (an `IfcWallType`,
502/// an IFC2X3 `IfcDoorStyle`, ...). Its own `HasPropertySets` are then
503/// searched, with the validation an occurrence's inherited type sets get,
504/// and a result carries [`ExactSource::Type`] with `object`'s id. `$` states
505/// no sets, a proven absence; an empty list is refused as for an inherited
506/// type. A type object named in an `IfcRelDefinesByProperties` is refused
507/// ([`ExactPropertyError::InvalidOccurrenceTarget`]), never ignored.
508///
509/// Quantity sets are searched like property sets, as buildingSMART IDS
510/// treats a quantity as a property: `Qto_WallBaseQuantities.Length` resolves
511/// to the `IfcQuantityLength` of that name. A property set and a quantity set
512/// of the same matching name on one source are ambiguous and refused.
513/// WHERE rules such as `LengthValue >= 0` are not evaluated; the value is
514/// the file's.
515///
516/// # Errors
517///
518/// Any [`ExactPropertyError`]: the answer is refused rather than guessed
519/// whenever the evidence is incomplete, ambiguous, or foreign to the
520/// declared release.
521pub fn exact_property(
522 model: &Model,
523 object: EntityId,
524 set_name: Option<&str>,
525 property_name: &str,
526) -> Result<ExactResolution, ExactPropertyError> {
527 let release = validate_model(model)?;
528 property_in(model, release, None, object, set_name, property_name)
529}
530
531/// [`exact_property`] once the model is bound to `release`, from a scan of
532/// every relationship when one is given.
533fn property_in(
534 model: &Model,
535 release: Release,
536 relations: Option<&Relations>,
537 object: EntityId,
538 set_name: Option<&str>,
539 property_name: &str,
540) -> Result<ExactResolution, ExactPropertyError> {
541 let assigned = assigned_sets(model, release, relations, object)?;
542 let occurrence = find_property(
543 model,
544 release,
545 &assigned.occurrence_sets,
546 ExactSource::Occurrence,
547 set_name,
548 property_name,
549 )?;
550 let inherited = match &assigned.type_sets {
551 Some((type_id, sets)) => find_property(
552 model,
553 release,
554 sets,
555 ExactSource::Type(*type_id),
556 set_name,
557 property_name,
558 )?,
559 None => None,
560 };
561 Ok(occurrence
562 .or(inherited)
563 .map_or(ExactResolution::Absent, ExactResolution::Present))
564}