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 {}