1use std::collections::HashSet;
10
11use ifc_model::{EntityId, Model};
12use ifc_schema::{ifc4x3, Schema, SchemaVersion};
13
14use crate::error::{AlignmentError, AlignmentResult};
15
16#[derive(Debug, Clone, Copy)]
21pub struct AlignmentView<'m> {
22 pub(crate) model: &'m Model,
23 pub(crate) schema: &'static Schema,
24}
25
26impl<'m> AlignmentView<'m> {
27 pub fn for_model(model: &'m Model) -> AlignmentResult<Self> {
29 let token = match model.header().schema.as_slice() {
30 [] => return Err(AlignmentError::MissingSchema),
31 [token] => token,
32 tokens => {
33 return Err(AlignmentError::AmbiguousSchema {
34 tokens: tokens.to_vec(),
35 });
36 }
37 };
38 let version = SchemaVersion::from_header_token(token).ok_or_else(|| {
39 AlignmentError::UnsupportedSchema {
40 token: token.clone(),
41 }
42 })?;
43 if version != SchemaVersion::Ifc4x3 {
44 return Err(AlignmentError::UnsupportedSchema {
45 token: token.clone(),
46 });
47 }
48 Ok(Self {
49 model,
50 schema: ifc4x3(),
51 })
52 }
53
54 pub(crate) fn ids_of_ancestor(&self, ancestor: &str) -> Vec<EntityId> {
56 self.model
57 .iter()
58 .filter_map(|(id, entity)| self.schema.is_a(&entity.type_name, ancestor).then_some(id))
59 .collect()
60 }
61
62 pub(crate) fn nested_children(
69 &self,
70 parent: EntityId,
71 expected: &str,
72 ) -> AlignmentResult<Vec<EntityId>> {
73 let mut result = Vec::new();
74 for relation in self.ids_of_ancestor("IfcRelNests") {
75 let entity = self
76 .model
77 .get(relation)
78 .ok_or(AlignmentError::MissingEntity { entity: relation })?;
79 let relating = entity
80 .attributes
81 .get(4)
82 .and_then(|value| value.as_ref_id())
83 .ok_or(AlignmentError::InvalidAttribute {
84 entity: relation,
85 index: 4,
86 name: "RelatingObject",
87 })?;
88 if relating != parent {
89 continue;
90 }
91 let related = entity
92 .attributes
93 .get(5)
94 .and_then(|value| value.as_list())
95 .ok_or(AlignmentError::InvalidAttribute {
96 entity: relation,
97 index: 5,
98 name: "RelatedObjects",
99 })?;
100 for value in related {
101 let child = value.as_ref_id().ok_or(AlignmentError::InvalidAttribute {
102 entity: relation,
103 index: 5,
104 name: "RelatedObjects",
105 })?;
106 let child_entity =
107 self.model
108 .get(child)
109 .ok_or(AlignmentError::DanglingReference {
110 entity: relation,
111 attribute: "RelatedObjects",
112 target: child,
113 })?;
114 if self.schema.is_a(&child_entity.type_name, expected) {
115 result.push(child);
116 }
117 }
118 }
119 Ok(result)
120 }
121
122 pub(crate) fn segment_chain(
132 &self,
133 parent: EntityId,
134 design_parameters_type: &str,
135 ) -> AlignmentResult<Vec<EntityId>> {
136 let segments = self.nested_children(parent, "IfcAlignmentSegment")?;
137 let mut seen = HashSet::with_capacity(segments.len());
138 let mut result = Vec::with_capacity(segments.len());
139 for segment in segments {
140 if !seen.insert(segment) {
141 return Err(AlignmentError::SemanticViolation {
142 entity: Some(segment),
143 rule: "IfcRelNests must not list the same segment twice",
144 });
145 }
146 let entity = self
147 .model
148 .get(segment)
149 .ok_or(AlignmentError::MissingEntity { entity: segment })?;
150 let design_parameters = entity
154 .attributes
155 .last()
156 .and_then(|value| value.as_ref_id())
157 .ok_or(AlignmentError::InvalidAttribute {
158 entity: segment,
159 index: entity.attributes.len().saturating_sub(1),
160 name: "DesignParameters",
161 })?;
162 let parameters_entity =
163 self.model
164 .get(design_parameters)
165 .ok_or(AlignmentError::DanglingReference {
166 entity: segment,
167 attribute: "DesignParameters",
168 target: design_parameters,
169 })?;
170 if !self
171 .schema
172 .is_a(¶meters_entity.type_name, design_parameters_type)
173 {
174 return Err(AlignmentError::WrongType {
175 entity: design_parameters,
176 expected: "matches the requested design-parameters family",
177 actual: parameters_entity.type_name.to_string(),
178 });
179 }
180 result.push(design_parameters);
181 }
182 Ok(result)
183 }
184}
185
186#[cfg(test)]
187mod tests {
188 use super::*;
189 use ifc_model::Header;
190
191 fn model_with_schema(tokens: &[&str]) -> Model {
192 let mut model = Model::default();
193 *model.header_mut() = Header {
194 schema: tokens.iter().map(|s| s.to_string()).collect(),
195 ..Header::default()
196 };
197 model
198 }
199
200 #[test]
201 fn refuses_a_missing_schema_declaration() {
202 let model = model_with_schema(&[]);
203 assert!(matches!(
204 AlignmentView::for_model(&model),
205 Err(AlignmentError::MissingSchema)
206 ));
207 }
208
209 #[test]
210 fn refuses_an_ambiguous_schema_declaration() {
211 let model = model_with_schema(&["IFC4X3_ADD2", "IFC4"]);
212 assert!(matches!(
213 AlignmentView::for_model(&model),
214 Err(AlignmentError::AmbiguousSchema { .. })
215 ));
216 }
217
218 #[test]
219 fn refuses_ifc2x3_because_alignment_entities_do_not_exist_there() {
220 let model = model_with_schema(&["IFC2X3"]);
221 assert!(matches!(
222 AlignmentView::for_model(&model),
223 Err(AlignmentError::UnsupportedSchema { token }) if token == "IFC2X3"
224 ));
225 }
226
227 #[test]
228 fn refuses_ifc4_because_alignment_entities_do_not_exist_there() {
229 let model = model_with_schema(&["IFC4"]);
230 assert!(matches!(
231 AlignmentView::for_model(&model),
232 Err(AlignmentError::UnsupportedSchema { token }) if token == "IFC4"
233 ));
234 }
235
236 #[test]
237 fn accepts_ifc4x3_add2() {
238 let model = model_with_schema(&["IFC4X3_ADD2"]);
239 assert!(AlignmentView::for_model(&model).is_ok());
240 }
241
242 #[test]
243 fn accepts_the_bare_ifc4x3_token() {
244 let model = model_with_schema(&["IFC4X3"]);
245 assert!(AlignmentView::for_model(&model).is_ok());
246 }
247
248 #[test]
249 fn refuses_an_unrecognized_token() {
250 let model = model_with_schema(&["IFC5"]);
251 assert!(matches!(
252 AlignmentView::for_model(&model),
253 Err(AlignmentError::UnsupportedSchema { token }) if token == "IFC5"
254 ));
255 }
256}