Skip to main content

ifc_structural/
view.rs

1//! Shared schema-resolved borrowed view primitives.
2
3use 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/// Entry point for strict structural-analysis projections.
15#[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    /// Build a view over `model` resolved against `schema`.
24    pub fn new(model: &'m Model, schema: &'s Schema) -> Self {
25        Self { model, schema }
26    }
27
28    #[must_use]
29    /// The schema this view resolves entity types and attribute slots against.
30    pub fn schema(&self) -> &'s Schema {
31        self.schema
32    }
33
34    /// Project `id` as an `IfcStructuralAnalysisModel`.
35    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    /// Project `id` as an `IfcStructuralLoadGroup`.
42    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    /// Project `id` as an `IfcStructuralResultGroup`.
49    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    /// Project `id` as an `IfcStructuralMember`, dispatching on its curve/surface subtype.
56    pub fn member(&self, id: EntityId) -> StructuralResult<Member<'m, 's>> {
57        Member::from_record(self.record(id, "IfcStructuralMember")?)
58    }
59
60    /// Project `id` as an `IfcStructuralConnection`, dispatching on its point/curve/surface subtype.
61    pub fn connection(&self, id: EntityId) -> StructuralResult<StructuralConnection<'m, 's>> {
62        StructuralConnection::from_record(self.record(id, "IfcStructuralConnection")?)
63    }
64
65    /// Project a concrete boundary condition.
66    pub fn boundary_condition(&self, id: EntityId) -> StructuralResult<BoundaryCondition<'m, 's>> {
67        BoundaryCondition::from_record(self.record(id, "IfcBoundaryCondition")?)
68    }
69
70    /// Project a concrete structural connection condition.
71    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    /// Project `id` as an `IfcStructuralAction`, dispatching on its point/curve/surface subtype.
79    pub fn action(&self, id: EntityId) -> StructuralResult<StructuralAction<'m, 's>> {
80        StructuralAction::from_record(self.record(id, "IfcStructuralAction")?)
81    }
82
83    /// Project `id` as an `IfcStructuralLoadStatic` value.
84    pub fn load(&self, id: EntityId) -> StructuralResult<StaticLoad<'m, 's>> {
85        StaticLoad::from_record(self.record(id, "IfcStructuralLoad")?)
86    }
87
88    /// Project an IFC4+ structural load configuration.
89    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    /// Project a concrete structural reaction.
96    pub fn reaction(&self, id: EntityId) -> StructuralResult<Reaction<'m, 's>> {
97        Reaction::from_record(self.record(id, "IfcStructuralReaction")?)
98    }
99
100    /// Equivalent to [`StructuralView::load`], provided for callers that name the projection explicitly.
101    pub fn static_load(&self, id: EntityId) -> StructuralResult<StaticLoad<'m, 's>> {
102        self.load(id)
103    }
104
105    /// Build a resolved [`Record`] for `id`, failing if it does not exist or is not a subtype of `expected`.
106    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    /// Every entity id in the model whose declared type is a subtype of `ancestor`.
115    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    /// Build a view by resolving the schema from the model's `FILE_SCHEMA` header token.
131    ///
132    /// Fails if the header declares zero or multiple schema tokens, or a token
133    /// this crate does not recognise (only IFC2X3, IFC4 and IFC4X3 are known).
134    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        };
154        Ok(Self::new(model, schema))
155    }
156}
157
158#[derive(Debug, Clone, Copy)]
159pub(crate) struct Record<'m, 's> {
160    pub(crate) model: &'m Model,
161    pub(crate) schema: &'s Schema,
162    pub(crate) id: EntityId,
163    pub(crate) entity: &'m Entity,
164}
165
166impl<'m, 's> Record<'m, 's> {
167    pub(crate) fn new(
168        model: &'m Model,
169        schema: &'s Schema,
170        id: EntityId,
171        expected: &'static str,
172    ) -> StructuralResult<Self> {
173        let entity = model
174            .get(id)
175            .ok_or(StructuralError::EntityNotFound { id })?;
176        if !schema.is_a(&entity.type_name, expected) {
177            return Err(StructuralError::WrongType {
178                id,
179                expected,
180                actual: entity.type_name.to_string(),
181            });
182        }
183        Ok(Self {
184            model,
185            schema,
186            id,
187            entity,
188        })
189    }
190
191    pub(crate) fn has_attribute(&self, attribute: &str) -> bool {
192        self.schema
193            .attribute_names(&self.entity.type_name)
194            .iter()
195            .any(|name| name.eq_ignore_ascii_case(attribute))
196    }
197
198    fn slot(&self, attribute: &'static str) -> StructuralResult<usize> {
199        self.schema
200            .attribute_names(&self.entity.type_name)
201            .iter()
202            .position(|name| name.eq_ignore_ascii_case(attribute))
203            .ok_or(StructuralError::MissingAttribute {
204                entity: self.id,
205                attribute,
206            })
207    }
208
209    pub(crate) fn value(&self, attribute: &'static str) -> StructuralResult<&'m Value> {
210        let slot = self.slot(attribute)?;
211        self.entity
212            .attributes
213            .get(slot)
214            .ok_or(StructuralError::MissingAttribute {
215                entity: self.id,
216                attribute,
217            })
218    }
219
220    pub(crate) fn optional_text(
221        &self,
222        attribute: &'static str,
223    ) -> StructuralResult<Option<&'m str>> {
224        match self.value(attribute)?.unwrap_typed() {
225            Value::Null | Value::Derived => Ok(None),
226            Value::Text(value) => Ok(Some(value)),
227            _ => Err(self.invalid(attribute, "text or null")),
228        }
229    }
230
231    pub(crate) fn require_object_type_if(
232        &self,
233        condition: bool,
234        rule: &'static str,
235    ) -> StructuralResult<()> {
236        if condition
237            && self
238                .optional_text("ObjectType")?
239                .is_none_or(|value| value.trim().is_empty())
240        {
241            return Err(StructuralError::SemanticViolation {
242                entity: Some(self.id),
243                rule,
244            });
245        }
246        Ok(())
247    }
248
249    pub(crate) fn required_enum(&self, attribute: &'static str) -> StructuralResult<&'m str> {
250        match self.value(attribute)?.unwrap_typed() {
251            Value::Enum(value) if self.declares_enum_member(attribute, value) => Ok(value),
252            _ => Err(self.invalid(attribute, "enumeration")),
253        }
254    }
255
256    pub(crate) fn optional_enum(
257        &self,
258        attribute: &'static str,
259    ) -> StructuralResult<Option<&'m str>> {
260        match self.value(attribute)?.unwrap_typed() {
261            Value::Null | Value::Derived => Ok(None),
262            Value::Enum(value) if self.declares_enum_member(attribute, value) => Ok(Some(value)),
263            _ => Err(self.invalid(attribute, "enumeration or null")),
264        }
265    }
266
267    fn declares_enum_member(&self, attribute: &str, value: &str) -> bool {
268        let attributes = self.schema.attributes(&self.entity.type_name);
269        let Some(declaration) = attributes
270            .iter()
271            .find(|declaration| declaration.name.eq_ignore_ascii_case(attribute))
272        else {
273            return false;
274        };
275        let mut type_name = declaration.type_name.as_str();
276        for _ in 0..16 {
277            let Some(type_def) = self.schema.type_def(type_name) else {
278                return false;
279            };
280            match &type_def.kind {
281                TypeKind::Enumeration(members) => {
282                    return members
283                        .iter()
284                        .any(|member| member.eq_ignore_ascii_case(value));
285                }
286                TypeKind::Defined(alias) => type_name = alias,
287                TypeKind::Select(_) => return false,
288            }
289        }
290        false
291    }
292
293    pub(crate) fn optional_bool(&self, attribute: &'static str) -> StructuralResult<Option<bool>> {
294        match self.value(attribute)?.unwrap_typed() {
295            Value::Null | Value::Derived => Ok(None),
296            Value::Bool(value) => Ok(Some(*value)),
297            _ => Err(self.invalid(attribute, "boolean or null")),
298        }
299    }
300
301    pub(crate) fn required_bool(&self, attribute: &'static str) -> StructuralResult<bool> {
302        self.optional_bool(attribute)?
303            .ok_or_else(|| self.invalid(attribute, "boolean"))
304    }
305
306    pub(crate) fn optional_number(&self, attribute: &'static str) -> StructuralResult<Option<f64>> {
307        let value = match self.value(attribute)?.unwrap_typed() {
308            Value::Null | Value::Derived => Ok(None),
309            Value::Integer(value) => Ok(Some(*value as f64)),
310            Value::Real(value) => Ok(Some(*value)),
311            _ => Err(self.invalid(attribute, "number or null")),
312        }?;
313        if value.is_some_and(|number| !number.is_finite()) {
314            return Err(self.invalid(attribute, "finite number or null"));
315        }
316        Ok(value)
317    }
318
319    pub(crate) fn optional_ref(
320        &self,
321        attribute: &'static str,
322        expected: &'static str,
323    ) -> StructuralResult<Option<EntityId>> {
324        let target = match self.value(attribute)?.unwrap_typed() {
325            Value::Null | Value::Derived => return Ok(None),
326            Value::Ref(target) => *target,
327            _ => return Err(self.invalid(attribute, "entity reference or null")),
328        };
329        self.check_reference(attribute, target, &[expected], expected)?;
330        Ok(Some(target))
331    }
332
333    pub(crate) fn required_ref(
334        &self,
335        attribute: &'static str,
336        expected: &'static str,
337    ) -> StructuralResult<EntityId> {
338        self.optional_ref(attribute, expected)?
339            .ok_or_else(|| self.invalid(attribute, "entity reference"))
340    }
341
342    pub(crate) fn required_ref_select(
343        &self,
344        attribute: &'static str,
345        expected: &'static str,
346        members: &[&str],
347    ) -> StructuralResult<EntityId> {
348        let target = match self.value(attribute)?.unwrap_typed() {
349            Value::Ref(target) => *target,
350            _ => return Err(self.invalid(attribute, "entity reference")),
351        };
352        self.check_reference(attribute, target, members, expected)?;
353        Ok(target)
354    }
355
356    pub(crate) fn optional_set_refs(
357        &self,
358        attribute: &'static str,
359        expected: &'static str,
360        minimum_when_present: usize,
361    ) -> StructuralResult<Vec<EntityId>> {
362        let values = match self.value(attribute)?.unwrap_typed() {
363            Value::Null | Value::Derived => return Ok(Vec::new()),
364            Value::List(values) => values,
365            _ => return Err(self.invalid(attribute, "aggregate of entity references or null")),
366        };
367        if values.len() < minimum_when_present {
368            return Err(StructuralError::InvalidCardinality {
369                entity: self.id,
370                attribute,
371                minimum: minimum_when_present,
372                maximum: None,
373                actual: values.len(),
374            });
375        }
376        let mut targets = Vec::with_capacity(values.len());
377        let mut unique = HashSet::with_capacity(values.len());
378        for value in values {
379            let Value::Ref(target) = value.unwrap_typed() else {
380                return Err(self.invalid(attribute, "aggregate of entity references"));
381            };
382            if !unique.insert(*target) {
383                return Err(self.invalid(attribute, "SET of unique entity references"));
384            }
385            self.check_reference(attribute, *target, &[expected], expected)?;
386            targets.push(*target);
387        }
388        Ok(targets)
389    }
390
391    pub(crate) fn required_set_refs_select(
392        &self,
393        attribute: &'static str,
394        expected: &'static str,
395        members: &[&str],
396        minimum: usize,
397    ) -> StructuralResult<Vec<EntityId>> {
398        let values = match self.value(attribute)?.unwrap_typed() {
399            Value::List(values) => values,
400            _ => return Err(self.invalid(attribute, "aggregate of entity references")),
401        };
402        if values.len() < minimum {
403            return Err(StructuralError::InvalidCardinality {
404                entity: self.id,
405                attribute,
406                minimum,
407                maximum: None,
408                actual: values.len(),
409            });
410        }
411        let mut targets = Vec::with_capacity(values.len());
412        let mut unique = HashSet::with_capacity(values.len());
413        for value in values {
414            let Value::Ref(target) = value.unwrap_typed() else {
415                return Err(self.invalid(attribute, "aggregate of entity references"));
416            };
417            if !unique.insert(*target) {
418                return Err(self.invalid(attribute, "SET of unique entity references"));
419            }
420            self.check_reference(attribute, *target, members, expected)?;
421            targets.push(*target);
422        }
423        Ok(targets)
424    }
425
426    fn check_reference(
427        &self,
428        attribute: &'static str,
429        target: EntityId,
430        members: &[&str],
431        expected: &'static str,
432    ) -> StructuralResult<()> {
433        let target_entity = self
434            .model
435            .get(target)
436            .ok_or(StructuralError::DanglingReference {
437                entity: self.id,
438                attribute,
439                target,
440            })?;
441        if !members
442            .iter()
443            .any(|member| self.schema.is_a(&target_entity.type_name, member))
444        {
445            return Err(StructuralError::WrongReferenceType {
446                entity: self.id,
447                attribute,
448                target,
449                expected,
450                actual: target_entity.type_name.to_string(),
451            });
452        }
453        Ok(())
454    }
455
456    fn invalid(&self, attribute: &'static str, expected: &'static str) -> StructuralError {
457        StructuralError::InvalidValue {
458            entity: self.id,
459            attribute,
460            expected,
461        }
462    }
463}