1use std::collections::HashSet;
19
20use ifc_model::{EntityId, Model};
21use ifc_schema::{ifc4x3, Schema, SchemaVersion};
22
23use crate::error::{AlignmentError, AlignmentResult};
24use crate::slot;
25
26mod hierarchy;
27
28pub use hierarchy::AlignmentHierarchy;
29
30#[derive(Debug, Clone, Copy)]
32pub(crate) struct RelationSlots {
33 relating: usize,
34 relating_name: &'static str,
35 related: usize,
36 related_name: &'static str,
37}
38
39impl RelationSlots {
40 pub(crate) const DECOMPOSES: Self = Self {
42 relating: slot::decomposes::RELATING_OBJECT,
43 relating_name: "RelatingObject",
44 related: slot::decomposes::RELATED_OBJECTS,
45 related_name: "RelatedObjects",
46 };
47 pub(crate) const POSITIONS: Self = Self {
49 relating: slot::rel_positions::RELATING_POSITIONING_ELEMENT,
50 relating_name: "RelatingPositioningElement",
51 related: slot::rel_positions::RELATED_PRODUCTS,
52 related_name: "RelatedProducts",
53 };
54}
55
56#[derive(Debug, Clone, Copy)]
61pub struct AlignmentView<'m> {
62 pub(crate) model: &'m Model,
63 pub(crate) schema: &'static Schema,
64}
65
66impl<'m> AlignmentView<'m> {
67 pub fn for_model(model: &'m Model) -> AlignmentResult<Self> {
69 let token = match model.header().schema.as_slice() {
70 [] => return Err(AlignmentError::MissingSchema),
71 [token] => token,
72 tokens => {
73 return Err(AlignmentError::AmbiguousSchema {
74 tokens: tokens.to_vec(),
75 });
76 }
77 };
78 let version = SchemaVersion::from_header_token(token).ok_or_else(|| {
79 AlignmentError::UnsupportedSchema {
80 token: token.clone(),
81 }
82 })?;
83 if version != SchemaVersion::Ifc4x3 {
84 return Err(AlignmentError::UnsupportedSchema {
85 token: token.clone(),
86 });
87 }
88 Ok(Self {
89 model,
90 schema: ifc4x3(),
91 })
92 }
93
94 pub(crate) fn ids_of_ancestor(&self, ancestor: &str) -> Vec<EntityId> {
96 self.model
97 .iter()
98 .filter_map(|(id, entity)| self.schema.is_a(&entity.type_name, ancestor).then_some(id))
99 .collect()
100 }
101
102 pub(crate) fn nested_children(
109 &self,
110 parent: EntityId,
111 expected: &str,
112 ) -> AlignmentResult<Vec<EntityId>> {
113 let children = self.related_by("IfcRelNests", RelationSlots::DECOMPOSES, parent)?;
114 Ok(self.filter_is_a(children, expected))
115 }
116
117 pub(crate) fn related_by(
123 &self,
124 relation: &str,
125 slots: RelationSlots,
126 relating: EntityId,
127 ) -> AlignmentResult<Vec<EntityId>> {
128 let mut result = Vec::new();
129 for id in self.ids_of_ancestor(relation) {
130 if self.relating_end(id, slots)? == relating {
131 result.extend(self.related_end(id, slots)?);
132 }
133 }
134 Ok(result)
135 }
136
137 pub(crate) fn relating_of(
144 &self,
145 relation: &str,
146 slots: RelationSlots,
147 related: EntityId,
148 ) -> AlignmentResult<Vec<EntityId>> {
149 let mut result = Vec::new();
150 for id in self.ids_of_ancestor(relation) {
151 let lists_it = self
152 .model
153 .get(id)
154 .and_then(|entity| entity.attributes.get(slots.related))
155 .and_then(|value| value.as_list())
156 .is_some_and(|values| values.iter().any(|v| v.as_ref_id() == Some(related)));
157 if lists_it {
158 result.push(self.relating_end(id, slots)?);
159 }
160 }
161 Ok(result)
162 }
163
164 pub(crate) fn filter_is_a(&self, ids: Vec<EntityId>, expected: &str) -> Vec<EntityId> {
167 ids.into_iter()
168 .filter(|id| {
169 self.model
170 .get(*id)
171 .is_some_and(|entity| self.schema.is_a(&entity.type_name, expected))
172 })
173 .collect()
174 }
175
176 fn relating_end(&self, relation: EntityId, slots: RelationSlots) -> AlignmentResult<EntityId> {
178 self.model
179 .get(relation)
180 .ok_or(AlignmentError::MissingEntity { entity: relation })?
181 .attributes
182 .get(slots.relating)
183 .and_then(|value| value.as_ref_id())
184 .ok_or(AlignmentError::InvalidAttribute {
185 entity: relation,
186 index: slots.relating,
187 name: slots.relating_name,
188 })
189 }
190
191 fn related_end(
193 &self,
194 relation: EntityId,
195 slots: RelationSlots,
196 ) -> AlignmentResult<Vec<EntityId>> {
197 let invalid = AlignmentError::InvalidAttribute {
198 entity: relation,
199 index: slots.related,
200 name: slots.related_name,
201 };
202 let values = self
203 .model
204 .get(relation)
205 .ok_or(AlignmentError::MissingEntity { entity: relation })?
206 .attributes
207 .get(slots.related)
208 .and_then(|value| value.as_list())
209 .ok_or_else(|| invalid.clone())?;
210 let mut related = Vec::with_capacity(values.len());
211 for value in values {
212 let child = value.as_ref_id().ok_or_else(|| invalid.clone())?;
213 if self.model.get(child).is_none() {
214 return Err(AlignmentError::DanglingReference {
215 entity: relation,
216 attribute: slots.related_name,
217 target: child,
218 });
219 }
220 related.push(child);
221 }
222 Ok(related)
223 }
224
225 pub(crate) fn segment_chain(
235 &self,
236 parent: EntityId,
237 design_parameters_type: &str,
238 ) -> AlignmentResult<Vec<EntityId>> {
239 let segments = self.nested_children(parent, "IfcAlignmentSegment")?;
240 let mut seen = HashSet::with_capacity(segments.len());
241 let mut result = Vec::with_capacity(segments.len());
242 for segment in segments {
243 if !seen.insert(segment) {
244 return Err(AlignmentError::SemanticViolation {
245 entity: Some(segment),
246 rule: "IfcRelNests must not list the same segment twice",
247 });
248 }
249 let entity = self
250 .model
251 .get(segment)
252 .ok_or(AlignmentError::MissingEntity { entity: segment })?;
253 let design_parameters = entity
256 .attributes
257 .get(slot::segment::DESIGN_PARAMETERS)
258 .and_then(|value| value.as_ref_id())
259 .ok_or(AlignmentError::InvalidAttribute {
260 entity: segment,
261 index: slot::segment::DESIGN_PARAMETERS,
262 name: "DesignParameters",
263 })?;
264 let parameters_entity =
265 self.model
266 .get(design_parameters)
267 .ok_or(AlignmentError::DanglingReference {
268 entity: segment,
269 attribute: "DesignParameters",
270 target: design_parameters,
271 })?;
272 if !self
273 .schema
274 .is_a(¶meters_entity.type_name, design_parameters_type)
275 {
276 return Err(AlignmentError::WrongType {
277 entity: design_parameters,
278 expected: "matches the requested design-parameters family",
279 actual: parameters_entity.type_name.to_string(),
280 });
281 }
282 result.push(design_parameters);
283 }
284 Ok(result)
285 }
286}
287
288#[cfg(test)]
289mod tests {
290 use super::*;
291 use ifc_model::Header;
292
293 fn model_with_schema(tokens: &[&str]) -> Model {
294 let mut model = Model::default();
295 *model.header_mut() = Header {
296 schema: tokens.iter().map(|s| s.to_string()).collect(),
297 ..Header::default()
298 };
299 model
300 }
301
302 #[test]
303 fn refuses_a_missing_schema_declaration() {
304 let model = model_with_schema(&[]);
305 assert!(matches!(
306 AlignmentView::for_model(&model),
307 Err(AlignmentError::MissingSchema)
308 ));
309 }
310
311 #[test]
312 fn refuses_an_ambiguous_schema_declaration() {
313 let model = model_with_schema(&["IFC4X3_ADD2", "IFC4"]);
314 assert!(matches!(
315 AlignmentView::for_model(&model),
316 Err(AlignmentError::AmbiguousSchema { .. })
317 ));
318 }
319
320 #[test]
321 fn refuses_ifc2x3_because_alignment_entities_do_not_exist_there() {
322 let model = model_with_schema(&["IFC2X3"]);
323 assert!(matches!(
324 AlignmentView::for_model(&model),
325 Err(AlignmentError::UnsupportedSchema { token }) if token == "IFC2X3"
326 ));
327 }
328
329 #[test]
330 fn refuses_ifc4_because_alignment_entities_do_not_exist_there() {
331 let model = model_with_schema(&["IFC4"]);
332 assert!(matches!(
333 AlignmentView::for_model(&model),
334 Err(AlignmentError::UnsupportedSchema { token }) if token == "IFC4"
335 ));
336 }
337
338 #[test]
339 fn accepts_ifc4x3_add2() {
340 let model = model_with_schema(&["IFC4X3_ADD2"]);
341 assert!(AlignmentView::for_model(&model).is_ok());
342 }
343
344 #[test]
345 fn accepts_the_bare_ifc4x3_token() {
346 let model = model_with_schema(&["IFC4X3"]);
347 assert!(AlignmentView::for_model(&model).is_ok());
348 }
349
350 #[test]
351 fn refuses_an_unrecognized_token() {
352 let model = model_with_schema(&["IFC5"]);
353 assert!(matches!(
354 AlignmentView::for_model(&model),
355 Err(AlignmentError::UnsupportedSchema { token }) if token == "IFC5"
356 ));
357 }
358}
359
360#[cfg(test)]
361mod intermediate_release_tests {
362 use super::*;
363
364 #[test]
367 fn ifc4x1_and_ifc4x2_are_refused_not_aliased() {
368 for token in ["IFC4X1", "IFC4X2"] {
369 let mut model = Model::new();
370 model.header_mut().schema = vec![token.to_owned()];
371 assert!(
372 matches!(
373 AlignmentView::for_model(&model),
374 Err(AlignmentError::UnsupportedSchema { token: found }) if found == token
375 ),
376 "{token} must be refused"
377 );
378 }
379 }
380}