1use std::collections::HashSet;
4
5use ifc_model::{Entity, EntityId, Model, Value};
6use ifc_schema::{ifc2x3, ifc4, ifc4x3, Schema, SchemaVersion, TypeKind};
7
8use crate::error::{StructuralError, StructuralResult};
9use crate::{
10 AnalysisModel, BoundaryCondition, ConnectionCondition, LoadConfiguration, LoadGroup, Member,
11 Reaction, ResultGroup, StaticLoad, StructuralAction, StructuralConnection,
12};
13
14#[derive(Debug, Clone, Copy)]
16pub struct StructuralView<'m, 's> {
17 pub(crate) model: &'m Model,
18 pub(crate) schema: &'s Schema,
19}
20
21impl<'m, 's> StructuralView<'m, 's> {
22 #[must_use]
23 pub fn new(model: &'m Model, schema: &'s Schema) -> Self {
25 Self { model, schema }
26 }
27
28 #[must_use]
29 pub fn schema(&self) -> &'s Schema {
31 self.schema
32 }
33
34 pub fn analysis_model(&self, id: EntityId) -> StructuralResult<AnalysisModel<'m, 's>> {
36 Ok(AnalysisModel::from_record(
37 self.record(id, "IfcStructuralAnalysisModel")?,
38 ))
39 }
40
41 pub fn load_group(&self, id: EntityId) -> StructuralResult<LoadGroup<'m, 's>> {
43 Ok(LoadGroup::from_record(
44 self.record(id, "IfcStructuralLoadGroup")?,
45 ))
46 }
47
48 pub fn result_group(&self, id: EntityId) -> StructuralResult<ResultGroup<'m, 's>> {
50 Ok(ResultGroup::from_record(
51 self.record(id, "IfcStructuralResultGroup")?,
52 ))
53 }
54
55 pub fn member(&self, id: EntityId) -> StructuralResult<Member<'m, 's>> {
57 Member::from_record(self.record(id, "IfcStructuralMember")?)
58 }
59
60 pub fn connection(&self, id: EntityId) -> StructuralResult<StructuralConnection<'m, 's>> {
62 StructuralConnection::from_record(self.record(id, "IfcStructuralConnection")?)
63 }
64
65 pub fn boundary_condition(&self, id: EntityId) -> StructuralResult<BoundaryCondition<'m, 's>> {
67 BoundaryCondition::from_record(self.record(id, "IfcBoundaryCondition")?)
68 }
69
70 pub fn connection_condition(
72 &self,
73 id: EntityId,
74 ) -> StructuralResult<ConnectionCondition<'m, 's>> {
75 ConnectionCondition::from_record(self.record(id, "IfcStructuralConnectionCondition")?)
76 }
77
78 pub fn action(&self, id: EntityId) -> StructuralResult<StructuralAction<'m, 's>> {
80 StructuralAction::from_record(self.record(id, "IfcStructuralAction")?)
81 }
82
83 pub fn load(&self, id: EntityId) -> StructuralResult<StaticLoad<'m, 's>> {
85 StaticLoad::from_record(self.record(id, "IfcStructuralLoad")?)
86 }
87
88 pub fn load_configuration(&self, id: EntityId) -> StructuralResult<LoadConfiguration<'m, 's>> {
90 Ok(LoadConfiguration::from_record(
91 self.record(id, "IfcStructuralLoadConfiguration")?,
92 ))
93 }
94
95 pub fn reaction(&self, id: EntityId) -> StructuralResult<Reaction<'m, 's>> {
97 Reaction::from_record(self.record(id, "IfcStructuralReaction")?)
98 }
99
100 pub fn static_load(&self, id: EntityId) -> StructuralResult<StaticLoad<'m, 's>> {
102 self.load(id)
103 }
104
105 pub(crate) fn record(
107 &self,
108 id: EntityId,
109 expected: &'static str,
110 ) -> StructuralResult<Record<'m, 's>> {
111 Record::new(self.model, self.schema, id, expected)
112 }
113
114 pub(crate) fn ids_of_ancestor(&self, ancestor: &str) -> Vec<EntityId> {
116 let matching_types: HashSet<_> = self
117 .model
118 .type_histogram()
119 .into_iter()
120 .filter_map(|(type_name, _)| self.schema.is_a(type_name, ancestor).then_some(type_name))
121 .collect();
122 self.model
123 .iter()
124 .filter_map(|(id, entity)| matching_types.contains(&*entity.type_name).then_some(id))
125 .collect()
126 }
127}
128
129impl<'m> StructuralView<'m, 'static> {
130 pub fn for_model(model: &'m Model) -> StructuralResult<Self> {
135 let token = match model.header().schema.as_slice() {
136 [] => return Err(StructuralError::MissingSchema),
137 [token] => token,
138 tokens => {
139 return Err(StructuralError::AmbiguousSchema {
140 tokens: tokens.to_vec(),
141 })
142 }
143 };
144 let version = SchemaVersion::from_header_token(token).ok_or_else(|| {
145 StructuralError::UnsupportedSchema {
146 token: token.clone(),
147 }
148 })?;
149 let schema = match version {
150 SchemaVersion::Ifc2x3 => ifc2x3(),
151 SchemaVersion::Ifc4 => ifc4(),
152 SchemaVersion::Ifc4x3 => ifc4x3(),
153 _ => {
156 return Err(StructuralError::UnsupportedSchema {
157 token: token.clone(),
158 })
159 }
160 };
161 Ok(Self::new(model, schema))
162 }
163}
164
165#[derive(Debug, Clone, Copy)]
166pub(crate) struct Record<'m, 's> {
167 pub(crate) model: &'m Model,
168 pub(crate) schema: &'s Schema,
169 pub(crate) id: EntityId,
170 pub(crate) entity: &'m Entity,
171}
172
173impl<'m, 's> Record<'m, 's> {
174 pub(crate) fn new(
175 model: &'m Model,
176 schema: &'s Schema,
177 id: EntityId,
178 expected: &'static str,
179 ) -> StructuralResult<Self> {
180 let entity = model
181 .get(id)
182 .ok_or(StructuralError::EntityNotFound { id })?;
183 if !schema.is_a(&entity.type_name, expected) {
184 return Err(StructuralError::WrongType {
185 id,
186 expected,
187 actual: entity.type_name.to_string(),
188 });
189 }
190 Ok(Self {
191 model,
192 schema,
193 id,
194 entity,
195 })
196 }
197
198 pub(crate) fn has_attribute(&self, attribute: &str) -> bool {
199 self.schema
200 .attribute_names(&self.entity.type_name)
201 .iter()
202 .any(|name| name.eq_ignore_ascii_case(attribute))
203 }
204
205 fn slot(&self, attribute: &'static str) -> StructuralResult<usize> {
206 self.schema
207 .attribute_names(&self.entity.type_name)
208 .iter()
209 .position(|name| name.eq_ignore_ascii_case(attribute))
210 .ok_or(StructuralError::MissingAttribute {
211 entity: self.id,
212 attribute,
213 })
214 }
215
216 pub(crate) fn value(&self, attribute: &'static str) -> StructuralResult<&'m Value> {
217 let slot = self.slot(attribute)?;
218 self.entity
219 .attributes
220 .get(slot)
221 .ok_or(StructuralError::MissingAttribute {
222 entity: self.id,
223 attribute,
224 })
225 }
226
227 pub(crate) fn optional_text(
228 &self,
229 attribute: &'static str,
230 ) -> StructuralResult<Option<&'m str>> {
231 match self.value(attribute)?.unwrap_typed() {
232 Value::Null | Value::Derived => Ok(None),
233 Value::Text(value) => Ok(Some(value)),
234 _ => Err(self.invalid(attribute, "text or null")),
235 }
236 }
237
238 pub(crate) fn require_object_type_if(
239 &self,
240 condition: bool,
241 rule: &'static str,
242 ) -> StructuralResult<()> {
243 if condition
244 && self
245 .optional_text("ObjectType")?
246 .is_none_or(|value| value.trim().is_empty())
247 {
248 return Err(StructuralError::SemanticViolation {
249 entity: Some(self.id),
250 rule,
251 });
252 }
253 Ok(())
254 }
255
256 pub(crate) fn required_enum(&self, attribute: &'static str) -> StructuralResult<&'m str> {
257 match self.value(attribute)?.unwrap_typed() {
258 Value::Enum(value) if self.declares_enum_member(attribute, value) => Ok(value),
259 _ => Err(self.invalid(attribute, "enumeration")),
260 }
261 }
262
263 pub(crate) fn optional_enum(
264 &self,
265 attribute: &'static str,
266 ) -> StructuralResult<Option<&'m str>> {
267 match self.value(attribute)?.unwrap_typed() {
268 Value::Null | Value::Derived => Ok(None),
269 Value::Enum(value) if self.declares_enum_member(attribute, value) => Ok(Some(value)),
270 _ => Err(self.invalid(attribute, "enumeration or null")),
271 }
272 }
273
274 fn declares_enum_member(&self, attribute: &str, value: &str) -> bool {
275 let attributes = self.schema.attributes(&self.entity.type_name);
276 let Some(declaration) = attributes
277 .iter()
278 .find(|declaration| declaration.name.eq_ignore_ascii_case(attribute))
279 else {
280 return false;
281 };
282 let mut type_name = declaration.type_name.as_str();
283 for _ in 0..16 {
284 let Some(type_def) = self.schema.type_def(type_name) else {
285 return false;
286 };
287 match &type_def.kind {
288 TypeKind::Enumeration(members) => {
289 return members
290 .iter()
291 .any(|member| member.eq_ignore_ascii_case(value));
292 }
293 TypeKind::Defined(alias) => type_name = alias,
294 TypeKind::Select(_) => return false,
295 _ => return false,
296 }
297 }
298 false
299 }
300
301 pub(crate) fn optional_bool(&self, attribute: &'static str) -> StructuralResult<Option<bool>> {
302 match self.value(attribute)?.unwrap_typed() {
303 Value::Null | Value::Derived => Ok(None),
304 Value::Bool(value) => Ok(Some(*value)),
305 _ => Err(self.invalid(attribute, "boolean or null")),
306 }
307 }
308
309 pub(crate) fn required_bool(&self, attribute: &'static str) -> StructuralResult<bool> {
310 self.optional_bool(attribute)?
311 .ok_or_else(|| self.invalid(attribute, "boolean"))
312 }
313
314 pub(crate) fn optional_number(&self, attribute: &'static str) -> StructuralResult<Option<f64>> {
315 let value = match self.value(attribute)?.unwrap_typed() {
316 Value::Null | Value::Derived => Ok(None),
317 Value::Integer(value) => Ok(Some(*value as f64)),
318 Value::Real(value) => Ok(Some(*value)),
319 _ => Err(self.invalid(attribute, "number or null")),
320 }?;
321 if value.is_some_and(|number| !number.is_finite()) {
322 return Err(self.invalid(attribute, "finite number or null"));
323 }
324 Ok(value)
325 }
326
327 pub(crate) fn optional_ref(
328 &self,
329 attribute: &'static str,
330 expected: &'static str,
331 ) -> StructuralResult<Option<EntityId>> {
332 let target = match self.value(attribute)?.unwrap_typed() {
333 Value::Null | Value::Derived => return Ok(None),
334 Value::Ref(target) => *target,
335 _ => return Err(self.invalid(attribute, "entity reference or null")),
336 };
337 self.check_reference(attribute, target, &[expected], expected)?;
338 Ok(Some(target))
339 }
340
341 pub(crate) fn required_ref(
342 &self,
343 attribute: &'static str,
344 expected: &'static str,
345 ) -> StructuralResult<EntityId> {
346 self.optional_ref(attribute, expected)?
347 .ok_or_else(|| self.invalid(attribute, "entity reference"))
348 }
349
350 pub(crate) fn required_ref_select(
351 &self,
352 attribute: &'static str,
353 expected: &'static str,
354 members: &[&str],
355 ) -> StructuralResult<EntityId> {
356 let target = match self.value(attribute)?.unwrap_typed() {
357 Value::Ref(target) => *target,
358 _ => return Err(self.invalid(attribute, "entity reference")),
359 };
360 self.check_reference(attribute, target, members, expected)?;
361 Ok(target)
362 }
363
364 pub(crate) fn optional_set_refs(
365 &self,
366 attribute: &'static str,
367 expected: &'static str,
368 minimum_when_present: usize,
369 ) -> StructuralResult<Vec<EntityId>> {
370 let values = match self.value(attribute)?.unwrap_typed() {
371 Value::Null | Value::Derived => return Ok(Vec::new()),
372 Value::List(values) => values,
373 _ => return Err(self.invalid(attribute, "aggregate of entity references or null")),
374 };
375 if values.len() < minimum_when_present {
376 return Err(StructuralError::InvalidCardinality {
377 entity: self.id,
378 attribute,
379 minimum: minimum_when_present,
380 maximum: None,
381 actual: values.len(),
382 });
383 }
384 let mut targets = Vec::with_capacity(values.len());
385 let mut unique = HashSet::with_capacity(values.len());
386 for value in values {
387 let Value::Ref(target) = value.unwrap_typed() else {
388 return Err(self.invalid(attribute, "aggregate of entity references"));
389 };
390 if !unique.insert(*target) {
391 return Err(self.invalid(attribute, "SET of unique entity references"));
392 }
393 self.check_reference(attribute, *target, &[expected], expected)?;
394 targets.push(*target);
395 }
396 Ok(targets)
397 }
398
399 pub(crate) fn required_set_refs_select(
400 &self,
401 attribute: &'static str,
402 expected: &'static str,
403 members: &[&str],
404 minimum: usize,
405 ) -> StructuralResult<Vec<EntityId>> {
406 let values = match self.value(attribute)?.unwrap_typed() {
407 Value::List(values) => values,
408 _ => return Err(self.invalid(attribute, "aggregate of entity references")),
409 };
410 if values.len() < minimum {
411 return Err(StructuralError::InvalidCardinality {
412 entity: self.id,
413 attribute,
414 minimum,
415 maximum: None,
416 actual: values.len(),
417 });
418 }
419 let mut targets = Vec::with_capacity(values.len());
420 let mut unique = HashSet::with_capacity(values.len());
421 for value in values {
422 let Value::Ref(target) = value.unwrap_typed() else {
423 return Err(self.invalid(attribute, "aggregate of entity references"));
424 };
425 if !unique.insert(*target) {
426 return Err(self.invalid(attribute, "SET of unique entity references"));
427 }
428 self.check_reference(attribute, *target, members, expected)?;
429 targets.push(*target);
430 }
431 Ok(targets)
432 }
433
434 fn check_reference(
435 &self,
436 attribute: &'static str,
437 target: EntityId,
438 members: &[&str],
439 expected: &'static str,
440 ) -> StructuralResult<()> {
441 let target_entity = self
442 .model
443 .get(target)
444 .ok_or(StructuralError::DanglingReference {
445 entity: self.id,
446 attribute,
447 target,
448 })?;
449 if !members
450 .iter()
451 .any(|member| self.schema.is_a(&target_entity.type_name, member))
452 {
453 return Err(StructuralError::WrongReferenceType {
454 entity: self.id,
455 attribute,
456 target,
457 expected,
458 actual: target_entity.type_name.to_string(),
459 });
460 }
461 Ok(())
462 }
463
464 fn invalid(&self, attribute: &'static str, expected: &'static str) -> StructuralError {
465 StructuralError::InvalidValue {
466 entity: self.id,
467 attribute,
468 expected,
469 }
470 }
471}
472
473#[cfg(test)]
474mod intermediate_release_tests {
475 use super::*;
476
477 #[test]
480 fn ifc4x1_and_ifc4x2_are_refused_not_aliased() {
481 for token in ["IFC4X1", "IFC4X2"] {
482 let mut model = Model::new();
483 model.header_mut().schema = vec![token.to_owned()];
484 assert!(
485 matches!(
486 StructuralView::for_model(&model),
487 Err(StructuralError::UnsupportedSchema { token: found }) if found == token
488 ),
489 "{token} must be refused"
490 );
491 }
492 }
493}