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