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