ifc_alignment/error.rs
1//! Structured alignment interpretation failures.
2
3use ifc_model::EntityId;
4
5/// Why an IFC alignment entity could not be read or lowered.
6#[non_exhaustive]
7#[derive(Debug, Clone, PartialEq)]
8pub enum AlignmentError {
9 /// A referenced entity id is not present in the model.
10 MissingEntity {
11 /// The id that resolved to nothing.
12 entity: EntityId,
13 },
14 /// An entity was not the IFC type the referencing slot requires.
15 WrongType {
16 /// The entity that was read.
17 entity: EntityId,
18 /// The IFC type the slot requires.
19 expected: &'static str,
20 /// The IFC type actually declared.
21 actual: String,
22 },
23 /// A mandatory attribute was absent, so the value cannot be inferred.
24 MissingAttribute {
25 /// The entity that was read.
26 entity: EntityId,
27 /// Zero-based slot index of the absent attribute.
28 index: usize,
29 /// Schema name of the attribute.
30 name: &'static str,
31 },
32 /// An attribute was present but held the wrong kind of value.
33 InvalidAttribute {
34 /// The entity that was read.
35 entity: EntityId,
36 /// Zero-based slot index of the offending attribute.
37 index: usize,
38 /// Schema name of the attribute.
39 name: &'static str,
40 },
41 /// The project's unit assignment does not resolve to usable lengths or angles.
42 InvalidUnits {
43 /// What expectation failed.
44 detail: &'static str,
45 },
46 /// A horizontal, vertical, or cant segment carries internally inconsistent data.
47 InvalidSegment {
48 /// The segment entity.
49 entity: EntityId,
50 /// What expectation failed.
51 detail: &'static str,
52 },
53 /// A segment type or configuration this crate deliberately does not lower.
54 ///
55 /// Covers segment kinds that are legal IFC but lack the data they need in
56 /// this call (e.g. a `VIENNESEBEND` lowered without its `IfcAlignmentCant`
57 /// layout, its `GravityCenterLineHeight`, or the station a chained layout
58 /// supplies) or that have no closed-form neutral curve here (e.g.
59 /// `CUBIC`).
60 Unsupported {
61 /// The unsupported entity.
62 entity: EntityId,
63 /// The IFC type or `PredefinedType` name actually declared.
64 type_name: String,
65 /// Why it is refused rather than approximated.
66 detail: &'static str,
67 },
68 /// The assembled neutral alignment graph violates a structural invariant.
69 Graph {
70 /// What expectation failed.
71 detail: String,
72 },
73 /// The model header declares no `FILE_SCHEMA` token.
74 MissingSchema,
75 /// More than one `FILE_SCHEMA` token was declared.
76 AmbiguousSchema {
77 /// Every schema token found in the header.
78 tokens: Vec<String>,
79 },
80 /// The declared schema is not one this crate can interpret.
81 ///
82 /// Alignment entities (`IfcAlignment*`) were introduced in IFC4X3; IFC2X3
83 /// and IFC4 ADD2 TC1 do not declare them at all, so there is no version
84 /// dispatch here the way `ifc-resource`/`ifc-structural` have one --
85 /// exactly one profile is authoritative and anything else is refused.
86 UnsupportedSchema {
87 /// The rejected schema token.
88 token: String,
89 },
90 /// A relationship or nesting structure violates a stated invariant.
91 SemanticViolation {
92 /// The offending entity, when one could be identified.
93 entity: Option<EntityId>,
94 /// The invariant that was violated.
95 rule: &'static str,
96 },
97 /// An authored value was rejected before anything was staged (ADR 0011).
98 ///
99 /// Distinct from [`Self::InvalidSegment`], which describes a segment that
100 /// already exists in a file. Here there is no entity yet and therefore no
101 /// id to report: the caller passed a value that could not be written.
102 InvalidAuthoredValue {
103 /// The IFC type being authored.
104 type_name: &'static str,
105 /// The attribute whose value was rejected.
106 attribute: &'static str,
107 /// Why it cannot be written.
108 detail: String,
109 },
110 /// Two consecutive vertical segments disagree where they meet.
111 ///
112 /// The segment at `entity` restates a start height or start gradient
113 /// that differs from where `previous` ends, beyond a magnitude-scaled
114 /// tolerance. A step in height or a kink in grade is refused rather than
115 /// joined, because joining it would shift every downstream height.
116 ProfileDiscontinuity {
117 /// The segment whose start disagrees.
118 entity: EntityId,
119 /// The segment it follows.
120 previous: EntityId,
121 /// Which quantity is discontinuous.
122 seam: ProfileSeam,
123 /// The previous segment's end value, in SI units.
124 expected: f64,
125 /// This segment's authored start value, in SI units.
126 actual: f64,
127 },
128 /// A dangling reference: the target id is not present in the model.
129 DanglingReference {
130 /// The entity holding the dangling reference.
131 entity: EntityId,
132 /// The attribute that names the missing target.
133 attribute: &'static str,
134 /// The id that resolved to nothing.
135 target: EntityId,
136 },
137 /// Graph traversal exceeded an explicit depth or node-count bound.
138 BudgetExceeded {
139 /// The configured maximum traversal depth.
140 max_depth: usize,
141 /// The configured maximum node count.
142 max_nodes: usize,
143 },
144}
145
146/// Which quantity of a vertical profile breaks at a seam.
147#[non_exhaustive]
148#[derive(Debug, Clone, Copy, PartialEq, Eq)]
149pub enum ProfileSeam {
150 /// `StartHeight` differs from the previous segment's end height: a step.
151 Height,
152 /// `StartGradient` differs from the previous `EndGradient`: a kink.
153 Gradient,
154}
155
156impl std::fmt::Display for ProfileSeam {
157 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
158 f.write_str(match self {
159 Self::Height => "height",
160 Self::Gradient => "gradient",
161 })
162 }
163}
164
165/// Result of an alignment read, carrying [`AlignmentError`] on failure.
166pub type AlignmentResult<T> = Result<T, AlignmentError>;
167
168impl std::fmt::Display for AlignmentError {
169 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
170 match self {
171 Self::MissingEntity { entity } => write!(f, "missing alignment entity {entity}"),
172 Self::WrongType {
173 entity,
174 expected,
175 actual,
176 } => write!(f, "{entity} is {actual}; expected {expected}"),
177 Self::MissingAttribute {
178 entity,
179 index,
180 name,
181 } => write!(f, "{entity} misses {name} at slot {index}"),
182 Self::InvalidAttribute {
183 entity,
184 index,
185 name,
186 } => write!(f, "{entity} has invalid {name} at slot {index}"),
187 Self::InvalidUnits { detail } => write!(f, "invalid alignment units: {detail}"),
188 Self::InvalidSegment { entity, detail } => {
189 write!(f, "invalid alignment segment {entity}: {detail}")
190 }
191 Self::Unsupported {
192 entity,
193 type_name,
194 detail,
195 } => write!(f, "unsupported {type_name} at {entity}: {detail}"),
196 Self::Graph { detail } => write!(f, "invalid neutral alignment graph: {detail}"),
197 Self::MissingSchema => write!(f, "no FILE_SCHEMA token was declared"),
198 Self::AmbiguousSchema { tokens } => {
199 write!(f, "ambiguous FILE_SCHEMA tokens: {tokens:?}")
200 }
201 Self::UnsupportedSchema { token } => write!(
202 f,
203 "unsupported schema {token}: alignment entities require IFC4X3 ADD2"
204 ),
205 Self::SemanticViolation { entity, rule } => match entity {
206 Some(entity) => write!(f, "{entity} violates {rule}"),
207 None => write!(f, "violates {rule}"),
208 },
209 Self::InvalidAuthoredValue {
210 type_name,
211 attribute,
212 detail,
213 } => write!(f, "cannot author {type_name}.{attribute}: {detail}"),
214 Self::ProfileDiscontinuity {
215 entity,
216 previous,
217 seam,
218 expected,
219 actual,
220 } => write!(
221 f,
222 "vertical {seam} discontinuity at {entity}: starts at {actual}, \
223 but {previous} ends at {expected}"
224 ),
225 Self::DanglingReference {
226 entity,
227 attribute,
228 target,
229 } => write!(f, "{entity}.{attribute} references missing {target}"),
230 Self::BudgetExceeded {
231 max_depth,
232 max_nodes,
233 } => write!(
234 f,
235 "traversal exceeded bounds (max_depth={max_depth}, max_nodes={max_nodes})"
236 ),
237 }
238 }
239}
240
241impl std::error::Error for AlignmentError {}