ifc_systems/error.rs
1//! Why a system query failed.
2//!
3//! Reading a system means reading relationship entities that may name
4//! entities the file never defines. That is a property of real exports, not
5//! a programming error, so it is reported rather than panicked on.
6
7use ifc_model::EntityId;
8use ifc_schema::SchemaVersion;
9
10/// A system membership the file states but cannot support.
11///
12/// Anomalies are collected instead of rejected: a file with one broken
13/// relationship still has a usable system graph, and refusing the whole
14/// model would make the crate useless on real exports.
15#[derive(Debug, Clone, PartialEq, Eq)]
16pub enum SystemAnomaly {
17 /// A relationship names an entity that is not in the file.
18 Dangling {
19 /// The relationship entity that made the claim.
20 relation: EntityId,
21 /// The id it named.
22 missing: EntityId,
23 },
24 /// An `IfcZone` member that WR1 does not permit.
25 ///
26 /// WR1 restricts zone members to `IfcZone`, `IfcSpace` and
27 /// `IfcSpatialZone`. Anything else makes the file invalid, so it is
28 /// reported and excluded rather than silently listed as zone content.
29 ZoneMemberNotSpatial {
30 /// The `IfcRelAssignsToGroup` stating it.
31 relation: EntityId,
32 /// The zone.
33 zone: EntityId,
34 /// The member WR1 rejects.
35 member: EntityId,
36 /// Its type, for diagnosis.
37 type_name: String,
38 },
39 /// An element contained by two different spatial structures.
40 ///
41 /// `ContainedInStructure` is `SET [0:1]`: an element has one home. Two
42 /// cannot both be true, so the first by id wins and the conflict is
43 /// stated rather than silently resolved.
44 ContainedTwice {
45 /// The element with two homes.
46 element: EntityId,
47 /// The structure kept.
48 first: EntityId,
49 /// The structure rejected.
50 second: EntityId,
51 },
52 /// A port is attached to two different elements.
53 ///
54 /// `IfcPort.ContainedIn` is `SET [0:1]` in the schema, so this cannot be
55 /// expressed by a valid file. It happens when an exporter writes both an
56 /// `IfcRelNests` and a legacy `IfcRelConnectsPortToElement` that disagree.
57 /// The first attachment in file order is kept so the result stays
58 /// deterministic, and the conflict is reported rather than hidden.
59 PortAttachedTwice {
60 /// The port with two owners.
61 port: EntityId,
62 /// The element that was kept.
63 kept: EntityId,
64 /// The element that was rejected.
65 rejected: EntityId,
66 },
67 /// A connection names a port that is not an `IfcPort` subtype.
68 ///
69 /// `IfcRelConnectsPorts` is typed to `IfcPort` in the schema, so this is a
70 /// malformed file rather than a modelling choice.
71 NotAPort {
72 /// The relationship entity.
73 relation: EntityId,
74 /// The entity it named as a port.
75 entity: EntityId,
76 /// That entity's declared type, upper-cased.
77 type_name: String,
78 },
79 /// `IfcRelAssignsToGroup` whose `RelatingGroup` is not a system.
80 ///
81 /// The relationship is shared with every other kind of group, so a
82 /// membership may legitimately point at something this crate does not
83 /// model. It is recorded rather than silently dropped.
84 NotASystem {
85 /// The relationship entity.
86 relation: EntityId,
87 /// The group it named.
88 group: EntityId,
89 /// The group's declared type, upper-cased.
90 type_name: String,
91 },
92}
93
94/// Why a model's declared schema release could not be resolved.
95///
96/// Every read path in this crate binds to the IFC release the file's
97/// `FILE_SCHEMA` header declares (see [`crate::schema_of`]) rather than
98/// assuming IFC4. A file that does not name a release this crate has
99/// verified semantics for is refused, not silently read under the wrong
100/// table: guessing IFC4 for an IFC2X3 file mis-classifies `IfcZone` as a
101/// system and mis-reads `IfcElectricalCircuit` as not one (issue #52).
102///
103/// `#[non_exhaustive]`: new refusal reasons (e.g. a newly-verified release
104/// gaining support) must be addable without breaking callers matching on
105/// this type.
106#[derive(Debug, Clone, PartialEq, Eq)]
107#[non_exhaustive]
108pub enum SchemaResolutionError {
109 /// `FILE_SCHEMA` names no schema at all.
110 MissingSchema,
111 /// `FILE_SCHEMA` names more than one schema; this crate reads only
112 /// single-schema files.
113 MultipleSchemas {
114 /// How many schema tokens the header carried.
115 schemas: usize,
116 },
117 /// `FILE_SCHEMA` names a release this read does not resolve.
118 ///
119 /// [`crate::schema_of`] and the system, port and flow readers resolve
120 /// IFC2X3 and IFC4. IFC4X3 is bundled in `ifc-schema` but its
121 /// distribution-system semantics have not been verified for them, so it
122 /// is refused there rather than defaulted to IFC4 -- an IFC4X3 file
123 /// assumed to be IFC4 would misread `IfcBuiltSystem` and related
124 /// IFC4X3-only entities. The zone readers ([`crate::try_zones`],
125 /// [`crate::long_name_of`]) resolve IFC4X3 too (#194), so they refuse
126 /// only a token with no bundled table.
127 UnsupportedSchema {
128 /// The header token as written, e.g. `"IFC4X3_ADD2"`.
129 schema: String,
130 },
131}
132
133impl std::fmt::Display for SchemaResolutionError {
134 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
135 match self {
136 Self::MissingSchema => write!(f, "FILE_SCHEMA declares no schema"),
137 Self::MultipleSchemas { schemas } => {
138 write!(
139 f,
140 "FILE_SCHEMA declares {schemas} schemas, expected exactly one"
141 )
142 }
143 Self::UnsupportedSchema { schema } => {
144 write!(
145 f,
146 "schema {schema:?} is not resolved by this ifc-systems read"
147 )
148 }
149 }
150 }
151}
152
153impl std::error::Error for SchemaResolutionError {}
154
155/// An accessor that reads an attribute the declared release does not
156/// define for the entity's type.
157///
158/// IFC2X3's `IfcZone` has no `LongName` slot; asking for one under that
159/// release is a different fact than the file having authored an empty
160/// value, and conflating the two (`None`) would make "not in this schema"
161/// indistinguishable from "authored empty" (issue #52).
162#[derive(Debug, Clone, Copy, PartialEq, Eq)]
163#[non_exhaustive]
164pub struct NotInSchema {
165 /// The entity whose type lacks the attribute.
166 pub entity: EntityId,
167 /// The schema release that was checked.
168 pub schema: SchemaVersion,
169}
170
171impl std::fmt::Display for NotInSchema {
172 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
173 write!(
174 f,
175 "attribute not declared for entity {:?} under {:?}",
176 self.entity, self.schema
177 )
178 }
179}
180
181impl std::error::Error for NotInSchema {}
182
183/// Why a per-attribute accessor bound to the model's declared release could
184/// not produce a value.
185///
186/// Reading an attribute that varies by release (e.g. `IfcZone.LongName`,
187/// absent in IFC2X3) needs the model's release resolved first. Either step
188/// can fail: the model's own `FILE_SCHEMA` may not resolve at all
189/// ([`SchemaResolutionError`], see [`crate::schema_of`]), or it may resolve
190/// to a release that simply does not declare the attribute
191/// ([`NotInSchema`]). Both are reported through this one error so a caller
192/// has a single type to match on.
193///
194/// `#[non_exhaustive]`: new attribute-accessor call sites reuse this type,
195/// and adding one must not be a breaking change for existing matches.
196#[derive(Debug, Clone, PartialEq, Eq)]
197#[non_exhaustive]
198pub enum SchemaGap {
199 /// The model's declared schema could not be resolved.
200 Schema(SchemaResolutionError),
201 /// The resolved release does not declare this attribute for this entity.
202 NotInSchema(NotInSchema),
203}
204
205impl From<SchemaResolutionError> for SchemaGap {
206 fn from(error: SchemaResolutionError) -> Self {
207 Self::Schema(error)
208 }
209}
210
211impl From<NotInSchema> for SchemaGap {
212 fn from(error: NotInSchema) -> Self {
213 Self::NotInSchema(error)
214 }
215}
216
217impl std::fmt::Display for SchemaGap {
218 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
219 match self {
220 Self::Schema(error) => write!(f, "{error}"),
221 Self::NotInSchema(error) => write!(f, "{error}"),
222 }
223 }
224}
225
226impl std::error::Error for SchemaGap {}