Skip to main content

ifc_properties/
error.rs

1//! Why a property lookup failed, and what a file got wrong.
2//!
3//! Anomalies describe a MALFORMED file, not a reader failure. They are
4//! returned alongside results rather than replacing them: one broken
5//! relationship must not hide every valid property in the model.
6
7use ifc_model::EntityId;
8use ifc_schema::SchemaVersion;
9
10/// A structural problem found while reading properties.
11///
12/// `#[non_exhaustive]`: new structural checks add variants without breaking
13/// callers that match on this type.
14#[derive(Debug, Clone, PartialEq)]
15#[non_exhaustive]
16pub enum PropertyAnomaly {
17    /// An object assigned two different types by `IfcRelDefinesByType`.
18    ///
19    /// IFC4 `IfcObject.IsTypedBy` is `SET [0:1]`; IFC2X3 `IfcObject` WR1
20    /// allows at most one. The first relationship by id is kept, and only
21    /// its type's sets are inherited.
22    TypedTwice {
23        /// The object with two types.
24        object: EntityId,
25        /// The type kept.
26        kept: EntityId,
27        /// The type rejected.
28        rejected: EntityId,
29        /// The `IfcRelDefinesByType` that was rejected.
30        relation: EntityId,
31    },
32    /// Two property sets of the same name on one owner.
33    ///
34    /// IFC4 forbids it on both occurrences (`IfcObject.UniquePropertySetNames`)
35    /// and types (`IfcTypeObject.UniquePropertySetNames`); IFC2X3 states no
36    /// such rule. Either way the resolved view is keyed by name, so the set
37    /// with the lower id is kept and the other is reported here rather than
38    /// silently overwriting it.
39    DuplicateSetName {
40        /// The occurrence or type holding both sets.
41        owner: EntityId,
42        /// The set kept.
43        kept: EntityId,
44        /// The set not resolved.
45        rejected: EntityId,
46    },
47    /// Two properties of the same name in one property set, or two property
48    /// templates of the same name in one set or complex template.
49    ///
50    /// Forbidden in IFC4 (`IfcPropertySet.UniquePropertyNames`) and IFC2X3
51    /// (`WR32`). [`PropertySet::property`](crate::PropertySet::property)
52    /// answers with the first in `HasProperties` order. For templates,
53    /// `IfcPropertySetTemplate` and `IfcComplexPropertyTemplate` both carry
54    /// `UniquePropertyNames`; `set` is then the template, and the first
55    /// template in `HasPropertyTemplates` order is the one a check uses.
56    DuplicatePropertyName {
57        /// The property set.
58        set: EntityId,
59        /// The property kept.
60        kept: EntityId,
61        /// The property shadowed.
62        rejected: EntityId,
63    },
64    /// An `IfcTypeObject` attached by `IfcRelDefinesByProperties`.
65    ///
66    /// Forbidden by the `NoRelatedTypeObject` WHERE rule: a type carries its
67    /// sets in `HasPropertySets`. The set is still reported, because refusing
68    /// to read a common exporter bug helps nobody.
69    TypeAttachedByRelationship {
70        /// The offending relationship.
71        relationship: EntityId,
72        /// The type object that must not be there.
73        type_object: EntityId,
74    },
75    /// A relationship names a property definition absent from the file.
76    MissingDefinition {
77        /// The relationship.
78        relationship: EntityId,
79        /// The id it named.
80        definition: EntityId,
81    },
82    /// A relationship names an object absent from the file.
83    MissingObject {
84        /// The relationship.
85        relationship: EntityId,
86        /// The id it named.
87        object: EntityId,
88    },
89    /// A quantity states a unit whose type contradicts the quantity kind.
90    ///
91    /// `IfcQuantityLength.WR21` requires a LENGTHUNIT, and the sibling
92    /// quantities carry the same rule. A file breaking it has stated two
93    /// different things about the same number.
94    QuantityUnitMismatch {
95        /// The quantity entity.
96        quantity: EntityId,
97        /// The unit it named.
98        unit: EntityId,
99        /// The `IfcUnitEnum` the schema requires.
100        expected: &'static str,
101        /// The `IfcUnitEnum` the file stated.
102        found: String,
103    },
104    /// A simple quantity states a negative value.
105    ///
106    /// Every `IfcQuantity*` carries `WR22 : Value >= 0.` (count included). A
107    /// negative area is not a small error; it is a value no consumer should
108    /// use for takeoff.
109    NegativeQuantity {
110        /// The quantity entity.
111        quantity: EntityId,
112        /// The value stated.
113        value: f64,
114    },
115    /// A simple quantity states no value.
116    ///
117    /// The value attribute (`LengthValue`, `AreaValue`, ...) is not
118    /// `OPTIONAL` on any `IfcQuantity*`. The quantity stays in its set's
119    /// `quantities` as `Quantity::Unresolved` with
120    /// `UnresolvedValue::Missing`, and is named here.
121    QuantityValueMissing {
122        /// The quantity entity.
123        quantity: EntityId,
124    },
125    /// A simple quantity's value attribute holds something other than a
126    /// number, such as text or a reference.
127    ///
128    /// The quantity stays in its set's `quantities` as
129    /// `Quantity::Unresolved` with `UnresolvedValue::NotNumeric`, and is
130    /// named here.
131    QuantityValueNotNumeric {
132        /// The quantity entity.
133        quantity: EntityId,
134        /// The value found, rendered for the message.
135        found: String,
136    },
137    /// A property set, quantity set, complex property or complex quantity,
138    /// or a property set or complex template, lists a member id that is not
139    /// in the file.
140    ///
141    /// The member cannot be read, so it is absent from the resolved value.
142    MissingMember {
143        /// The set or complex entity listing the member.
144        container: EntityId,
145        /// The id it named.
146        member: EntityId,
147    },
148    /// A member list holds an item that is not an entity reference.
149    ///
150    /// `IfcPropertySet.HasProperties`, `IfcComplexProperty.HasProperties`,
151    /// `IfcElementQuantity.Quantities` and
152    /// `IfcPhysicalComplexQuantity.HasQuantities` are sets of entity
153    /// references. An item such as a string or number names no member, so
154    /// nothing is read for it.
155    MemberNotReference {
156        /// The set or complex entity holding the list.
157        container: EntityId,
158        /// The list attribute, e.g. `"HasProperties"`.
159        attribute: &'static str,
160        /// The item found, rendered for the message.
161        found: String,
162    },
163    /// A member list names the same entity more than once.
164    ///
165    /// Each of those lists is an EXPRESS `SET`, which cannot hold one
166    /// instance twice. The member is read once, at its first position.
167    DuplicateMember {
168        /// The set or complex entity holding the list.
169        container: EntityId,
170        /// The list attribute, e.g. `"Quantities"`.
171        attribute: &'static str,
172        /// The member listed again.
173        member: EntityId,
174    },
175    /// A complex property, complex quantity or complex property template
176    /// reaches itself again through its members.
177    ///
178    /// The schema forbids only a DIRECT self-member (`IfcComplexProperty`
179    /// `WR21`, `IfcPhysicalComplexQuantity.NoSelfReference`,
180    /// `IfcComplexPropertyTemplate.NoSelfReference`); a longer cycle is just
181    /// as unresolvable. `member` is already being read higher
182    /// up the same path, so it is left out of `complex`'s resolved members.
183    ComplexCycle {
184        /// The complex entity whose member list closes the cycle.
185        complex: EntityId,
186        /// The member that re-enters the path.
187        member: EntityId,
188    },
189    /// Complex nesting deeper than the reader follows.
190    ///
191    /// `complex` is resolved with its name and usage, but its members are
192    /// not read.
193    ComplexTooDeep {
194        /// The complex entity whose members were not read.
195        complex: EntityId,
196        /// The nesting depth followed.
197        limit: usize,
198    },
199    /// One read followed more nested member references than its budget.
200    ///
201    /// Members may legally be shared between complex properties, so a
202    /// small file can expand into an enormous tree. Once the budget is
203    /// spent, the remaining members of `complex` are not read.
204    ComplexBudgetExceeded {
205        /// The complex entity whose remaining members were not read.
206        complex: EntityId,
207        /// The nested member references followed.
208        limit: usize,
209    },
210    /// A template's member list, or an `IfcRelDefinesByTemplate`, names an
211    /// entity that is not a template of the required kind.
212    ///
213    /// `HasPropertyTemplates` is a `SET [1:?] OF IfcPropertyTemplate` and
214    /// `RelatingTemplate` an `IfcPropertySetTemplate`. The entity is not
215    /// read as a template.
216    NotATemplate {
217        /// The template or relationship naming it.
218        container: EntityId,
219        /// The entity named.
220        member: EntityId,
221        /// Its IFC type name.
222        type_name: String,
223    },
224    /// A record's attribute count differs from what the bound release
225    /// declares for its entity.
226    ///
227    /// Attributes are read by name from that release's table, so a short
228    /// record would otherwise read its missing trailing attributes as unset.
229    /// The attributes present are still read.
230    SlotCountMismatch {
231        /// The malformed record.
232        entity: EntityId,
233        /// Its IFC type name.
234        type_name: String,
235        /// The attribute count the release declares.
236        expected: usize,
237        /// The attribute count in the file.
238        actual: usize,
239    },
240    /// An attribute holds a value its declared type does not admit: a
241    /// number where a label is declared, an enumeration constant that is not
242    /// a member of the release's enumeration, or a reference to an entity
243    /// outside the declared type.
244    ///
245    /// The value is still reported as written where the view has a place
246    /// for it (an enumeration constant, a reference id), and left unset
247    /// otherwise.
248    MalformedAttribute {
249        /// The entity holding the attribute.
250        entity: EntityId,
251        /// The attribute, as the schema names it.
252        attribute: &'static str,
253        /// The value found, rendered for the message.
254        found: String,
255    },
256}
257
258/// Why property templates could not be read or checked against the
259/// release a model declares.
260///
261/// Distinct from [`PropertyAnomaly`]: an anomaly is a malformed fact inside
262/// a file that can still be read; this refuses the whole request.
263#[derive(Debug, Clone, PartialEq, Eq)]
264#[non_exhaustive]
265pub enum TemplateError {
266    /// The model cannot be bound to one release, for the reason
267    /// [`exact_schema`](crate::exact_schema) gives: STEP diagnostics, no or
268    /// several `FILE_SCHEMA` entries, or an unsupported one.
269    Release(crate::ExactPropertyError),
270    /// The declared release defines no property templates.
271    ///
272    /// `IfcPropertySetTemplate` and its property templates are new in IFC4;
273    /// IFC2X3 TC1 declares none of them, so nothing in such a file can be a
274    /// template.
275    NoTemplates {
276        /// The release the header declares.
277        schema: ifc_schema::SchemaVersion,
278    },
279    /// The entity is not in the model.
280    MissingEntity {
281        /// The id named by the caller.
282        id: EntityId,
283    },
284    /// The entity is not the kind of template asked for.
285    NotATemplate {
286        /// The entity.
287        id: EntityId,
288        /// Its IFC type name.
289        type_name: String,
290    },
291}
292
293impl std::fmt::Display for TemplateError {
294    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
295        match self {
296            Self::Release(error) => write!(f, "{error}"),
297            Self::NoTemplates { schema } => {
298                write!(f, "{schema:?} defines no property templates")
299            }
300            Self::MissingEntity { id } => write!(f, "#{} is not in the model", id.0),
301            Self::NotATemplate { id, type_name } => {
302                write!(f, "#{} is a {type_name}, not the template asked for", id.0)
303            }
304        }
305    }
306}
307
308impl std::error::Error for TemplateError {}
309
310/// A refused authoring request.
311///
312/// Distinct from [`PropertyAnomaly`], which reports what a FILE got wrong.
313/// These say the CALLER asked for something the schema does not allow, and
314/// are returned before anything is staged so a rejected edit never reaches
315/// a transaction.
316#[derive(Debug, Clone, PartialEq, Eq)]
317#[non_exhaustive]
318pub enum PropertyError {
319    /// The entity is not in the model.
320    MissingEntity {
321        /// The id named by the caller.
322        id: EntityId,
323    },
324    /// The entity is not a simple quantity type.
325    NotAQuantity {
326        /// The entity.
327        id: EntityId,
328        /// What it actually is.
329        type_name: String,
330    },
331    /// The entity is not an `IfcElementQuantity`.
332    NotAQuantitySet {
333        /// The entity.
334        id: EntityId,
335        /// What it actually is.
336        type_name: String,
337    },
338    /// An authored attribute value is not valid for its slot.
339    ///
340    /// Raised before staging, so a rejected draft never reaches the model.
341    AuthoringInvalid {
342        /// The entity type being authored.
343        entity: &'static str,
344        /// The attribute that failed.
345        attribute: &'static str,
346        /// The offending value, rendered for the message.
347        value: String,
348    },
349    /// The model's header declares several schemas; authoring binds to
350    /// exactly one release.
351    MultipleSchemas {
352        /// Number of `FILE_SCHEMA` declarations.
353        schemas: usize,
354    },
355    /// The model's header declares one schema with no bundled table, so no
356    /// layout can be trusted.
357    UnsupportedSchema {
358        /// The `FILE_SCHEMA` token as written.
359        schema: String,
360    },
361    /// The model's release does not declare this entity, such as
362    /// `IfcQuantityNumber` (IFC4X3 only) in an IFC2X3 or IFC4 model.
363    EntityNotInSchema {
364        /// The entity type, in schema casing.
365        entity: &'static str,
366        /// The release the model declares.
367        schema: SchemaVersion,
368    },
369    /// An authoring call supplied a value for an attribute the model's
370    /// release does not declare, such as a `Formula` for an IFC2X3
371    /// quantity. It is refused rather than dropped.
372    AuthoringNotInSchema {
373        /// The entity type, in schema casing.
374        entity: &'static str,
375        /// The attribute.
376        attribute: &'static str,
377        /// The release the model declares.
378        schema: SchemaVersion,
379    },
380    /// The record to edit does not have the attribute count its release
381    /// declares, so no slot in it can be trusted.
382    MalformedEntitySlots {
383        /// The record.
384        id: EntityId,
385        /// Its type.
386        type_name: String,
387        /// Attribute count the release declares.
388        expected: usize,
389        /// Attribute count the record has.
390        actual: usize,
391    },
392    /// The model's release requires an attribute the authoring call leaves
393    /// unset, such as the IFC2X3 `IfcRoot.OwnerHistory` (#191). It is refused
394    /// rather than written as `$`; the `*_with_owner_history` writers take
395    /// the `IfcOwnerHistory` IFC2X3 needs.
396    //
397    // Last, so no earlier variant's implicit discriminant moves.
398    AuthoringRequired {
399        /// The entity type being authored.
400        entity: &'static str,
401        /// The required attribute, as the release names it.
402        attribute: &'static str,
403        /// The release the model declares.
404        schema: SchemaVersion,
405    },
406}
407
408impl std::fmt::Display for PropertyError {
409    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
410        match self {
411            Self::MissingEntity { id } => write!(f, "#{} is not in the model", id.0),
412            Self::NotAQuantity { id, type_name } => {
413                write!(f, "#{} is a {type_name}, not a simple quantity", id.0)
414            }
415            Self::NotAQuantitySet { id, type_name } => {
416                write!(f, "#{} is a {type_name}, not an IfcElementQuantity", id.0)
417            }
418            Self::AuthoringInvalid {
419                entity,
420                attribute,
421                value,
422            } => write!(
423                f,
424                "{entity}.{attribute} rejected the authored value: {value}"
425            ),
426            Self::MultipleSchemas { schemas } => write!(
427                f,
428                "the header declares {schemas} schemas; authoring binds to exactly one"
429            ),
430            Self::UnsupportedSchema { schema } => {
431                write!(
432                    f,
433                    "the header declares {schema}, which has no bundled table"
434                )
435            }
436            Self::EntityNotInSchema { entity, schema } => {
437                write!(f, "{entity} is not an entity of {schema:?}")
438            }
439            Self::AuthoringNotInSchema {
440                entity,
441                attribute,
442                schema,
443            } => write!(
444                f,
445                "cannot author {entity}.{attribute}: not defined by {schema:?}"
446            ),
447            Self::AuthoringRequired {
448                entity,
449                attribute,
450                schema,
451            } => write!(f, "cannot author {entity}: {schema:?} requires {attribute}"),
452            Self::MalformedEntitySlots {
453                id,
454                type_name,
455                expected,
456                actual,
457            } => write!(
458                f,
459                "#{} {type_name} has {actual} attributes; its release declares {expected}",
460                id.0
461            ),
462        }
463    }
464}
465
466/// Result alias for authoring and reading helpers in this crate.
467pub type PropertyResult<T> = Result<T, PropertyError>;
468
469impl std::error::Error for PropertyError {}