Skip to main content

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