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