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            // IFC4X1 and IFC4X2 are bundled by ifc-schema but not verified
154            // here. Refused, never aliased.
155            _ => {
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    /// IFC4X1 and IFC4X2 have bundled tables but no verified layout here:
478    /// refused with the unsupported-schema error, never read as IFC4/IFC4X3.
479    #[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}