Skip to main content

ifc_resource/
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::{ifc4, ifc4x3, Schema, SchemaVersion, TypeKind};
7
8use crate::error::{ResourceError, ResourceResult};
9use crate::{ConstructionResource, ResourceTime};
10
11#[derive(Debug, Clone, Copy)]
12/// Borrowed, schema-resolved entry point for the bounded IFC4 resource
13/// slice: pairs a model with the schema selected for it and exposes
14/// per-entity projection and query methods.
15pub struct ResourceView<'m, 's> {
16    pub(crate) model: &'m Model,
17    pub(crate) schema: &'s Schema,
18}
19
20impl<'m, 's> ResourceView<'m, 's> {
21    /// Builds a view over `model` using an explicit `schema`, failing if
22    /// `schema` is not IFC4 or IFC4X3, or does not match the model's
23    /// declared `FILE_SCHEMA` token.
24    pub fn new(model: &'m Model, schema: &'s Schema) -> ResourceResult<Self> {
25        let Some(version) = schema.version() else {
26            return Err(ResourceError::UnsupportedSchema {
27                token: schema.name().to_owned(),
28            });
29        };
30        if !matches!(version, SchemaVersion::Ifc4 | SchemaVersion::Ifc4x3) {
31            return Err(ResourceError::UnsupportedSchema {
32                token: schema.name().to_owned(),
33            });
34        }
35        match model.header().schema.as_slice() {
36            [] => {}
37            [token] if SchemaVersion::from_header_token(token) == Some(version) => {}
38            [token] => {
39                return Err(ResourceError::UnsupportedSchema {
40                    token: token.clone(),
41                });
42            }
43            tokens => {
44                return Err(ResourceError::AmbiguousSchema {
45                    tokens: tokens.to_vec(),
46                });
47            }
48        }
49        Ok(Self { model, schema })
50    }
51
52    /// The bundled schema this view resolves attributes against.
53    #[must_use]
54    pub fn schema(&self) -> &'s Schema {
55        self.schema
56    }
57
58    /// Projects a concrete `IfcConstructionResource` occurrence by entity
59    /// id.
60    pub fn resource(&self, id: EntityId) -> ResourceResult<ConstructionResource<'m, 's>> {
61        ConstructionResource::from_record(self.record(id, "IfcConstructionResource")?)
62    }
63
64    /// Projects an `IfcResourceTime` by entity id.
65    pub fn resource_time(&self, id: EntityId) -> ResourceResult<ResourceTime<'m, 's>> {
66        Ok(ResourceTime::from_record(
67            self.record(id, "IfcResourceTime")?,
68        ))
69    }
70
71    pub(crate) fn record(
72        &self,
73        id: EntityId,
74        expected: &'static str,
75    ) -> ResourceResult<Record<'m, 's>> {
76        Record::new(self.model, self.schema, id, expected)
77    }
78
79    pub(crate) fn ids_of_ancestor(&self, ancestor: &str) -> Vec<EntityId> {
80        self.model
81            .iter()
82            .filter_map(|(id, entity)| self.schema.is_a(&entity.type_name, ancestor).then_some(id))
83            .collect()
84    }
85}
86
87impl<'m> ResourceView<'m, 'static> {
88    /// Selects the bundled schema matching the model's declared
89    /// `FILE_SCHEMA` token (IFC4 ADD2 TC1 or IFC4X3 ADD2), failing if the
90    /// header names no schema, more than one, or an unsupported one.
91    pub fn for_model(model: &'m Model) -> ResourceResult<Self> {
92        let token = match model.header().schema.as_slice() {
93            [] => return Err(ResourceError::MissingSchema),
94            [token] => token,
95            tokens => {
96                return Err(ResourceError::AmbiguousSchema {
97                    tokens: tokens.to_vec(),
98                });
99            }
100        };
101        let version = SchemaVersion::from_header_token(token).ok_or_else(|| {
102            ResourceError::UnsupportedSchema {
103                token: token.clone(),
104            }
105        })?;
106        let schema = match version {
107            SchemaVersion::Ifc4 => ifc4(),
108            SchemaVersion::Ifc4x3 => ifc4x3(),
109            SchemaVersion::Ifc2x3 => {
110                return Err(ResourceError::UnsupportedSchema {
111                    token: token.clone(),
112                });
113            }
114        };
115        Self::new(model, schema)
116    }
117}
118
119pub(crate) fn validate_object_assignment(
120    model: &Model,
121    schema: &Schema,
122    relation: Option<EntityId>,
123    related_objects_type: Option<&str>,
124    related_objects: &[EntityId],
125) -> ResourceResult<()> {
126    let Some(category) = related_objects_type else {
127        return Ok(());
128    };
129    let expected = if category.eq_ignore_ascii_case("NOTDEFINED") {
130        return Ok(());
131    } else if category.eq_ignore_ascii_case("PRODUCT") {
132        "IfcProduct"
133    } else if category.eq_ignore_ascii_case("PROCESS") {
134        "IfcProcess"
135    } else if category.eq_ignore_ascii_case("CONTROL") {
136        "IfcControl"
137    } else if category.eq_ignore_ascii_case("RESOURCE") {
138        "IfcResource"
139    } else if category.eq_ignore_ascii_case("ACTOR") {
140        "IfcActor"
141    } else if category.eq_ignore_ascii_case("GROUP") {
142        "IfcGroup"
143    } else if category.eq_ignore_ascii_case("PROJECT") {
144        "IfcProject"
145    } else {
146        return Err(ResourceError::InvalidEnumeration {
147            entity: relation,
148            attribute: "RelatedObjectsType",
149            value: category.to_owned(),
150        });
151    };
152    if related_objects.iter().any(|target| {
153        model
154            .get(*target)
155            .is_none_or(|entity| !schema.is_a(&entity.type_name, expected))
156    }) {
157        return Err(ResourceError::SemanticViolation {
158            entity: relation,
159            rule: "IfcRelAssigns.WR1_IfcCorrectObjectAssignment",
160        });
161    }
162    Ok(())
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    ) -> ResourceResult<Self> {
180        let entity = model.get(id).ok_or(ResourceError::EntityNotFound { id })?;
181        if !schema.is_a(&entity.type_name, expected) {
182            return Err(ResourceError::WrongType {
183                id,
184                expected,
185                actual: entity.type_name.to_string(),
186            });
187        }
188        Ok(Self {
189            model,
190            schema,
191            id,
192            entity,
193        })
194    }
195
196    fn slot(&self, attribute: &'static str) -> ResourceResult<usize> {
197        self.schema
198            .attribute_names(&self.entity.type_name)
199            .iter()
200            .position(|name| name.eq_ignore_ascii_case(attribute))
201            .ok_or(ResourceError::MissingAttribute {
202                entity: self.id,
203                attribute,
204            })
205    }
206
207    pub(crate) fn value(&self, attribute: &'static str) -> ResourceResult<&'m Value> {
208        self.entity
209            .attributes
210            .get(self.slot(attribute)?)
211            .ok_or(ResourceError::MissingAttribute {
212                entity: self.id,
213                attribute,
214            })
215    }
216
217    pub(crate) fn required_text(&self, attribute: &'static str) -> ResourceResult<&'m str> {
218        match self.value(attribute)?.unwrap_typed() {
219            Value::Text(value) => Ok(value),
220            _ => Err(self.invalid(attribute, "text")),
221        }
222    }
223
224    pub(crate) fn optional_text_list(
225        &self,
226        attribute: &'static str,
227        minimum: usize,
228    ) -> ResourceResult<Vec<&'m str>> {
229        let values = match self.value(attribute)?.unwrap_typed() {
230            Value::Null | Value::Derived => return Ok(Vec::new()),
231            Value::List(values) => values,
232            _ => return Err(self.invalid(attribute, "aggregate of text or null")),
233        };
234        if values.len() < minimum {
235            return Err(ResourceError::InvalidCardinality {
236                entity: self.id,
237                attribute,
238                minimum,
239                actual: values.len(),
240            });
241        }
242        values
243            .iter()
244            .map(|value| match value.unwrap_typed() {
245                Value::Text(value) => Ok(value.as_ref()),
246                _ => Err(self.invalid(attribute, "aggregate of text or null")),
247            })
248            .collect()
249    }
250
251    pub(crate) fn optional_text(&self, attribute: &'static str) -> ResourceResult<Option<&'m str>> {
252        match self.value(attribute)?.unwrap_typed() {
253            Value::Null | Value::Derived => Ok(None),
254            Value::Text(value) => Ok(Some(value)),
255            _ => Err(self.invalid(attribute, "text or null")),
256        }
257    }
258
259    pub(crate) fn optional_bool(&self, attribute: &'static str) -> ResourceResult<Option<bool>> {
260        match self.value(attribute)?.unwrap_typed() {
261            Value::Null | Value::Derived => Ok(None),
262            Value::Bool(value) => Ok(Some(*value)),
263            _ => Err(self.invalid(attribute, "boolean or null")),
264        }
265    }
266
267    pub(crate) fn optional_positive_number(
268        &self,
269        attribute: &'static str,
270    ) -> ResourceResult<Option<f64>> {
271        let value = match self.value(attribute)?.unwrap_typed() {
272            Value::Null | Value::Derived => return Ok(None),
273            Value::Integer(value) => *value as f64,
274            Value::Real(value) => *value,
275            _ => return Err(self.invalid(attribute, "finite positive number or null")),
276        };
277        if !value.is_finite() || value <= 0.0 {
278            return Err(self.invalid(attribute, "finite positive number or null"));
279        }
280        Ok(Some(value))
281    }
282
283    pub(crate) fn required_non_negative_number(
284        &self,
285        attribute: &'static str,
286    ) -> ResourceResult<f64> {
287        let value = match self.value(attribute)?.unwrap_typed() {
288            Value::Integer(value) => *value as f64,
289            Value::Real(value) => *value,
290            _ => return Err(self.invalid(attribute, "finite non-negative number")),
291        };
292        if !value.is_finite() || value < 0.0 {
293            return Err(self.invalid(attribute, "finite non-negative number"));
294        }
295        Ok(value)
296    }
297
298    pub(crate) fn required_enum(&self, attribute: &'static str) -> ResourceResult<&'m str> {
299        let value = match self.value(attribute)?.unwrap_typed() {
300            Value::Enum(value) => value,
301            _ => return Err(self.invalid(attribute, "declared enumeration")),
302        };
303        if !self.declares_enum_member(attribute, value) {
304            return Err(ResourceError::InvalidEnumeration {
305                entity: Some(self.id),
306                attribute,
307                value: value.to_string(),
308            });
309        }
310        Ok(value)
311    }
312
313    pub(crate) fn optional_enum(&self, attribute: &'static str) -> ResourceResult<Option<&'m str>> {
314        let value = match self.value(attribute)?.unwrap_typed() {
315            Value::Null | Value::Derived => return Ok(None),
316            Value::Enum(value) => value,
317            _ => return Err(self.invalid(attribute, "declared enumeration or null")),
318        };
319        if !self.declares_enum_member(attribute, value) {
320            return Err(ResourceError::InvalidEnumeration {
321                entity: Some(self.id),
322                attribute,
323                value: value.to_string(),
324            });
325        }
326        Ok(Some(value))
327    }
328
329    fn declares_enum_member(&self, attribute: &str, value: &str) -> bool {
330        let declarations = self.schema.attributes(&self.entity.type_name);
331        let Some(declaration) = declarations
332            .iter()
333            .find(|candidate| candidate.name.eq_ignore_ascii_case(attribute))
334        else {
335            return false;
336        };
337        let mut type_name = declaration.type_name.as_str();
338        for _ in 0..16 {
339            let Some(definition) = self.schema.type_def(type_name) else {
340                return false;
341            };
342            match &definition.kind {
343                TypeKind::Enumeration(members) => {
344                    return members
345                        .iter()
346                        .any(|member| member.eq_ignore_ascii_case(value));
347                }
348                TypeKind::Defined(alias) => type_name = alias,
349                TypeKind::Select(_) => return false,
350            }
351        }
352        false
353    }
354
355    pub(crate) fn optional_ref(
356        &self,
357        attribute: &'static str,
358        expected: &'static str,
359    ) -> ResourceResult<Option<EntityId>> {
360        let target = match self.value(attribute)?.unwrap_typed() {
361            Value::Null | Value::Derived => return Ok(None),
362            Value::Ref(target) => *target,
363            _ => return Err(self.invalid(attribute, "entity reference or null")),
364        };
365        self.check_reference(attribute, target, &[expected], expected)?;
366        Ok(Some(target))
367    }
368
369    pub(crate) fn required_ref(
370        &self,
371        attribute: &'static str,
372        expected: &'static str,
373    ) -> ResourceResult<EntityId> {
374        let Value::Ref(target) = self.value(attribute)?.unwrap_typed() else {
375            return Err(self.invalid(attribute, "entity reference"));
376        };
377        self.check_reference(attribute, *target, &[expected], expected)?;
378        Ok(*target)
379    }
380
381    pub(crate) fn optional_ref_select(
382        &self,
383        attribute: &'static str,
384        expected: &'static str,
385        members: &[&str],
386    ) -> ResourceResult<Option<EntityId>> {
387        let target = match self.value(attribute)?.unwrap_typed() {
388            Value::Null | Value::Derived => return Ok(None),
389            Value::Ref(target) => *target,
390            _ => return Err(self.invalid(attribute, "entity reference or null")),
391        };
392        self.check_reference(attribute, target, members, expected)?;
393        Ok(Some(target))
394    }
395
396    pub(crate) fn required_ref_select(
397        &self,
398        attribute: &'static str,
399        expected: &'static str,
400        members: &[&str],
401    ) -> ResourceResult<EntityId> {
402        let Value::Ref(target) = self.value(attribute)?.unwrap_typed() else {
403            return Err(self.invalid(attribute, "entity reference"));
404        };
405        self.check_reference(attribute, *target, members, expected)?;
406        Ok(*target)
407    }
408
409    pub(crate) fn refs(
410        &self,
411        attribute: &'static str,
412        expected: &'static str,
413        minimum: usize,
414        optional: bool,
415        unique: bool,
416    ) -> ResourceResult<Vec<EntityId>> {
417        let values = match self.value(attribute)?.unwrap_typed() {
418            Value::Null | Value::Derived if optional => return Ok(Vec::new()),
419            Value::List(values) => values,
420            _ => return Err(self.invalid(attribute, "aggregate of entity references")),
421        };
422        if values.len() < minimum {
423            return Err(ResourceError::InvalidCardinality {
424                entity: self.id,
425                attribute,
426                minimum,
427                actual: values.len(),
428            });
429        }
430        let mut targets = Vec::with_capacity(values.len());
431        let mut seen = HashSet::with_capacity(values.len());
432        for value in values {
433            let Value::Ref(target) = value.unwrap_typed() else {
434                return Err(self.invalid(attribute, "aggregate of entity references"));
435            };
436            if unique && !seen.insert(*target) {
437                return Err(ResourceError::DuplicateReference {
438                    entity: self.id,
439                    attribute,
440                    target: *target,
441                });
442            }
443            self.check_reference(attribute, *target, &[expected], expected)?;
444            targets.push(*target);
445        }
446        Ok(targets)
447    }
448
449    pub(crate) fn check_reference(
450        &self,
451        attribute: &'static str,
452        target: EntityId,
453        members: &[&str],
454        expected: &'static str,
455    ) -> ResourceResult<()> {
456        let entity = self
457            .model
458            .get(target)
459            .ok_or(ResourceError::DanglingReference {
460                entity: self.id,
461                attribute,
462                target,
463            })?;
464        if !members
465            .iter()
466            .any(|member| self.schema.is_a(&entity.type_name, member))
467        {
468            return Err(ResourceError::WrongReferenceType {
469                entity: self.id,
470                attribute,
471                target,
472                expected,
473                actual: entity.type_name.to_string(),
474            });
475        }
476        Ok(())
477    }
478
479    pub(crate) fn require_object_type_if(
480        &self,
481        condition: bool,
482        rule: &'static str,
483    ) -> ResourceResult<()> {
484        if condition
485            && self
486                .optional_text("ObjectType")?
487                .is_none_or(|value| value.trim().is_empty())
488        {
489            return Err(ResourceError::SemanticViolation {
490                entity: Some(self.id),
491                rule,
492            });
493        }
494        Ok(())
495    }
496
497    fn invalid(&self, attribute: &'static str, expected: &'static str) -> ResourceError {
498        ResourceError::InvalidValue {
499            entity: self.id,
500            attribute,
501            expected,
502        }
503    }
504}