Skip to main content

ifc_material/
view.rs

1//! Shared borrowed-view mechanics and strict IFC slot decoders.
2
3use ifc_model::{Entity, EntityId, Model, Value};
4
5use crate::release::Release;
6use crate::{LogicalValue, MaterialError, MaterialResult, SchemaVersion};
7
8const MAX_TYPED_WRAPPERS: usize = 8;
9
10/// Borrowed MaterialResource interpretation of a model.
11///
12/// Every projection the view hands out reads against the release the
13/// model's header declares (see [`crate::material_schema`]): slot positions
14/// come from that release's bundled table, never from IFC4 by assumption.
15#[derive(Debug, Clone, Copy)]
16pub struct MaterialView<'m> {
17    model: &'m Model,
18    release: Release<'m>,
19}
20
21impl<'m> MaterialView<'m> {
22    /// Wraps a model reference for MaterialResource projection, bound to the
23    /// release its header declares.
24    pub fn new(model: &'m Model) -> Self {
25        Self {
26            model,
27            release: Release::of(model),
28        }
29    }
30
31    /// Returns the underlying model this view borrows from.
32    pub fn model(self) -> &'m Model {
33        self.model
34    }
35
36    /// The release this view's projections read against.
37    ///
38    /// # Errors
39    ///
40    /// [`MaterialError::MultipleSchemas`] or
41    /// [`MaterialError::UnsupportedSchema`] when the header binds none.
42    pub fn schema(self) -> MaterialResult<SchemaVersion> {
43        self.release.bound().map(|(version, _)| version)
44    }
45
46    pub(crate) fn release(self) -> Release<'m> {
47        self.release
48    }
49
50    pub(crate) fn entity(self, source: EntityId, target: EntityId) -> MaterialResult<&'m Entity> {
51        self.model
52            .get(target)
53            .ok_or(MaterialError::DanglingReference {
54                source_id: source,
55                target,
56            })
57    }
58}
59
60macro_rules! borrowed_entity {
61    ($name:ident, $ifc_name:literal) => {
62        #[doc = concat!("Borrowed projection of `", $ifc_name, "`.")]
63        #[doc = ""]
64        #[doc = "Projections handed out by a [`crate::MaterialView`] read against the"]
65        #[doc = "release the model declares; one built with `try_new` has no model and"]
66        #[doc = "reads against IFC4, as in 0.2.0."]
67        #[derive(Debug, Clone, Copy)]
68        pub struct $name<'m> {
69            id: ifc_model::EntityId,
70            entity: &'m ifc_model::Entity,
71            release: crate::release::Release<'m>,
72        }
73
74        impl<'m> $name<'m> {
75            /// Wraps `entity` as this view after checking it is an instance
76            /// of the expected IFC entity type; fails with
77            /// [`crate::MaterialError::WrongEntityType`] otherwise.
78            ///
79            /// Without a model there is no header to bind, so the projection
80            /// reads against IFC4. Use [`Self::try_from_view`] to read the
81            /// release a model declares.
82            pub fn try_new(
83                id: ifc_model::EntityId,
84                entity: &'m ifc_model::Entity,
85            ) -> crate::MaterialResult<Self> {
86                Self::check(id, entity, crate::release::Release::LEGACY)
87            }
88
89            /// Looks up `id` in the view's model and projects it against the
90            /// release the model declares.
91            ///
92            /// # Errors
93            ///
94            /// [`crate::MaterialError::UnknownEntity`] when `id` is not in
95            /// the model, and [`crate::MaterialError::WrongEntityType`] when
96            /// it is another entity type.
97            pub fn try_from_view(
98                view: crate::MaterialView<'m>,
99                id: ifc_model::EntityId,
100            ) -> crate::MaterialResult<Self> {
101                let entity = view
102                    .model()
103                    .get(id)
104                    .ok_or(crate::MaterialError::UnknownEntity { id })?;
105                Self::check(id, entity, view.release())
106            }
107
108            fn check(
109                id: ifc_model::EntityId,
110                entity: &'m ifc_model::Entity,
111                release: crate::release::Release<'m>,
112            ) -> crate::MaterialResult<Self> {
113                if !entity.is_type($ifc_name) {
114                    return Err(crate::MaterialError::WrongEntityType {
115                        expected: $ifc_name,
116                        actual: entity.type_name.to_string(),
117                    });
118                }
119                Ok(Self {
120                    id,
121                    entity,
122                    release,
123                })
124            }
125
126            pub(crate) fn from_known(
127                id: ifc_model::EntityId,
128                entity: &'m ifc_model::Entity,
129                release: crate::release::Release<'m>,
130            ) -> Self {
131                Self {
132                    id,
133                    entity,
134                    release,
135                }
136            }
137
138            /// The entity id of the wrapped record.
139            pub fn id(self) -> ifc_model::EntityId {
140                self.id
141            }
142
143            /// The raw, untyped entity record backing this view.
144            pub fn entity(self) -> &'m ifc_model::Entity {
145                self.entity
146            }
147
148            /// The release this projection reads against.
149            ///
150            /// # Errors
151            ///
152            /// [`crate::MaterialError::MultipleSchemas`] or
153            /// [`crate::MaterialError::UnsupportedSchema`] when the model's
154            /// header binds none.
155            pub fn schema(self) -> crate::MaterialResult<crate::SchemaVersion> {
156                self.release.bound().map(|(version, _)| version)
157            }
158
159            /// The release binding of this projection.
160            #[allow(dead_code)]
161            pub(crate) fn release(self) -> crate::release::Release<'m> {
162                self.release
163            }
164
165            /// Slot of `attribute` (IFC4 name) on this record in the bound
166            /// release; `NotInSchema` or `EntityNotInSchema` otherwise.
167            #[allow(dead_code)]
168            pub(crate) fn slot(self, attribute: &'static str) -> crate::MaterialResult<usize> {
169                self.release.slot($ifc_name, self.id, attribute)
170            }
171        }
172    };
173}
174pub(crate) use borrowed_entity;
175
176fn kind(value: &Value) -> &'static str {
177    match value {
178        Value::Null => "null",
179        Value::Derived => "derived marker",
180        Value::Bool(_) => "boolean",
181        Value::LogicalUnknown => "logical unknown",
182        Value::Integer(_) => "integer",
183        Value::Real(_) => "real",
184        Value::Text(_) => "text",
185        Value::Binary(_) => "binary",
186        Value::Enum(_) => "enumeration",
187        Value::Ref(_) => "reference",
188        Value::List(_) => "aggregate",
189        Value::Typed { .. } => "typed value",
190    }
191}
192
193fn invalid(
194    entity_type: &'static str,
195    id: EntityId,
196    attribute: &'static str,
197    expected: &str,
198    actual: &Value,
199) -> MaterialError {
200    MaterialError::InvalidValue {
201        entity: entity_type,
202        id,
203        attribute,
204        value: format!("expected {expected}, found {}", kind(actual)),
205    }
206}
207
208fn optional_raw<'a>(
209    entity_type: &'static str,
210    id: EntityId,
211    entity: &'a Entity,
212    slot: usize,
213    attribute: &'static str,
214) -> MaterialResult<Option<&'a Value>> {
215    match entity.attribute(slot) {
216        None | Some(Value::Null) => Ok(None),
217        Some(Value::Derived) => Err(invalid(
218            entity_type,
219            id,
220            attribute,
221            "an explicit value or $",
222            &Value::Derived,
223        )),
224        Some(value) => Ok(Some(value)),
225    }
226}
227
228fn required_raw<'a>(
229    entity_type: &'static str,
230    id: EntityId,
231    entity: &'a Entity,
232    slot: usize,
233    attribute: &'static str,
234) -> MaterialResult<&'a Value> {
235    optional_raw(entity_type, id, entity, slot, attribute)?.ok_or(MaterialError::MissingAttribute {
236        entity: entity_type,
237        id,
238        attribute,
239    })
240}
241
242fn unwrap_scalar<'a>(
243    entity_type: &'static str,
244    id: EntityId,
245    attribute: &'static str,
246    value: &'a Value,
247) -> MaterialResult<&'a Value> {
248    let mut current = value;
249    for _ in 0..MAX_TYPED_WRAPPERS {
250        match current {
251            Value::Typed { value, .. } => current = value,
252            _ => return Ok(current),
253        }
254    }
255    Err(MaterialError::InvalidValue {
256        entity: entity_type,
257        id,
258        attribute,
259        value: format!("typed-wrapper nesting exceeds {MAX_TYPED_WRAPPERS}"),
260    })
261}
262
263pub(crate) fn required_text<'a>(
264    entity_type: &'static str,
265    id: EntityId,
266    entity: &'a Entity,
267    slot: usize,
268    attribute: &'static str,
269) -> MaterialResult<&'a str> {
270    let raw = required_raw(entity_type, id, entity, slot, attribute)?;
271    let value = unwrap_scalar(entity_type, id, attribute, raw)?;
272    match value {
273        Value::Text(value) => Ok(value),
274        _ => Err(invalid(entity_type, id, attribute, "text", value)),
275    }
276}
277
278pub(crate) fn optional_text<'a>(
279    entity_type: &'static str,
280    id: EntityId,
281    entity: &'a Entity,
282    slot: usize,
283    attribute: &'static str,
284) -> MaterialResult<Option<&'a str>> {
285    let Some(raw) = optional_raw(entity_type, id, entity, slot, attribute)? else {
286        return Ok(None);
287    };
288    let value = unwrap_scalar(entity_type, id, attribute, raw)?;
289    match value {
290        Value::Text(value) => Ok(Some(value)),
291        _ => Err(invalid(entity_type, id, attribute, "text or $", value)),
292    }
293}
294
295pub(crate) fn required_ref(
296    entity_type: &'static str,
297    id: EntityId,
298    entity: &Entity,
299    slot: usize,
300    attribute: &'static str,
301) -> MaterialResult<EntityId> {
302    let value = required_raw(entity_type, id, entity, slot, attribute)?;
303    match value {
304        Value::Ref(target) => Ok(*target),
305        _ => Err(invalid(entity_type, id, attribute, "reference", value)),
306    }
307}
308
309pub(crate) fn optional_ref(
310    entity_type: &'static str,
311    id: EntityId,
312    entity: &Entity,
313    slot: usize,
314    attribute: &'static str,
315) -> MaterialResult<Option<EntityId>> {
316    let Some(value) = optional_raw(entity_type, id, entity, slot, attribute)? else {
317        return Ok(None);
318    };
319    match value {
320        Value::Ref(target) => Ok(Some(*target)),
321        _ => Err(invalid(entity_type, id, attribute, "reference or $", value)),
322    }
323}
324
325fn refs_from_aggregate(
326    entity_type: &'static str,
327    id: EntityId,
328    attribute: &'static str,
329    value: &Value,
330    minimum: usize,
331) -> MaterialResult<Vec<EntityId>> {
332    let Value::List(items) = value else {
333        return Err(invalid(
334            entity_type,
335            id,
336            attribute,
337            "an immediate aggregate",
338            value,
339        ));
340    };
341    if items.len() < minimum {
342        return Err(MaterialError::InvalidValue {
343            entity: entity_type,
344            id,
345            attribute,
346            value: format!("expected at least {minimum} item(s), found {}", items.len()),
347        });
348    }
349    items
350        .iter()
351        .map(|item| match item {
352            Value::Ref(target) => Ok(*target),
353            _ => Err(invalid(
354                entity_type,
355                id,
356                attribute,
357                "an aggregate of direct references",
358                item,
359            )),
360        })
361        .collect()
362}
363
364pub(crate) fn required_refs(
365    entity_type: &'static str,
366    id: EntityId,
367    entity: &Entity,
368    slot: usize,
369    attribute: &'static str,
370    minimum: usize,
371) -> MaterialResult<Vec<EntityId>> {
372    refs_from_aggregate(
373        entity_type,
374        id,
375        attribute,
376        required_raw(entity_type, id, entity, slot, attribute)?,
377        minimum,
378    )
379}
380
381pub(crate) fn optional_refs(
382    entity_type: &'static str,
383    id: EntityId,
384    entity: &Entity,
385    slot: usize,
386    attribute: &'static str,
387    minimum: usize,
388) -> MaterialResult<Option<Vec<EntityId>>> {
389    let Some(value) = optional_raw(entity_type, id, entity, slot, attribute)? else {
390        return Ok(None);
391    };
392    refs_from_aggregate(entity_type, id, attribute, value, minimum).map(Some)
393}
394
395fn number_value(
396    entity_type: &'static str,
397    id: EntityId,
398    attribute: &'static str,
399    raw: &Value,
400) -> MaterialResult<f64> {
401    let value = unwrap_scalar(entity_type, id, attribute, raw)?;
402    let number = match value {
403        Value::Real(value) => *value,
404        Value::Integer(value) => *value as f64,
405        _ => return Err(invalid(entity_type, id, attribute, "a number", value)),
406    };
407    if number.is_finite() {
408        Ok(number)
409    } else {
410        Err(MaterialError::InvalidValue {
411            entity: entity_type,
412            id,
413            attribute,
414            value: "number must be finite".to_owned(),
415        })
416    }
417}
418
419pub(crate) fn required_number(
420    entity_type: &'static str,
421    id: EntityId,
422    entity: &Entity,
423    slot: usize,
424    attribute: &'static str,
425) -> MaterialResult<f64> {
426    number_value(
427        entity_type,
428        id,
429        attribute,
430        required_raw(entity_type, id, entity, slot, attribute)?,
431    )
432}
433
434pub(crate) fn optional_number(
435    entity_type: &'static str,
436    id: EntityId,
437    entity: &Entity,
438    slot: usize,
439    attribute: &'static str,
440) -> MaterialResult<Option<f64>> {
441    let Some(value) = optional_raw(entity_type, id, entity, slot, attribute)? else {
442        return Ok(None);
443    };
444    number_value(entity_type, id, attribute, value).map(Some)
445}
446
447fn integer_value(
448    entity_type: &'static str,
449    id: EntityId,
450    attribute: &'static str,
451    raw: &Value,
452) -> MaterialResult<i64> {
453    let value = unwrap_scalar(entity_type, id, attribute, raw)?;
454    match value {
455        Value::Integer(value) => Ok(*value),
456        _ => Err(invalid(entity_type, id, attribute, "an integer", value)),
457    }
458}
459
460pub(crate) fn optional_integer(
461    entity_type: &'static str,
462    id: EntityId,
463    entity: &Entity,
464    slot: usize,
465    attribute: &'static str,
466) -> MaterialResult<Option<i64>> {
467    let Some(value) = optional_raw(entity_type, id, entity, slot, attribute)? else {
468        return Ok(None);
469    };
470    integer_value(entity_type, id, attribute, value).map(Some)
471}
472
473pub(crate) fn required_enum<'a>(
474    entity_type: &'static str,
475    id: EntityId,
476    entity: &'a Entity,
477    slot: usize,
478    attribute: &'static str,
479) -> MaterialResult<&'a str> {
480    let raw = required_raw(entity_type, id, entity, slot, attribute)?;
481    let value = unwrap_scalar(entity_type, id, attribute, raw)?;
482    match value {
483        Value::Enum(token) => Ok(token),
484        _ => Err(invalid(entity_type, id, attribute, "an enumeration", value)),
485    }
486}
487
488pub(crate) fn optional_logical(
489    entity_type: &'static str,
490    id: EntityId,
491    entity: &Entity,
492    slot: usize,
493    attribute: &'static str,
494) -> MaterialResult<Option<LogicalValue>> {
495    let Some(raw) = optional_raw(entity_type, id, entity, slot, attribute)? else {
496        return Ok(None);
497    };
498    let value = unwrap_scalar(entity_type, id, attribute, raw)?;
499    match value {
500        Value::Bool(false) => Ok(Some(LogicalValue::False)),
501        Value::Bool(true) => Ok(Some(LogicalValue::True)),
502        Value::LogicalUnknown => Ok(Some(LogicalValue::Unknown)),
503        _ => Err(invalid(
504            entity_type,
505            id,
506            attribute,
507            "a logical value or $",
508            value,
509        )),
510    }
511}
512
513pub(crate) fn required_number_array_2(
514    entity_type: &'static str,
515    id: EntityId,
516    entity: &Entity,
517    slot: usize,
518    attribute: &'static str,
519) -> MaterialResult<[f64; 2]> {
520    let value = required_raw(entity_type, id, entity, slot, attribute)?;
521    let Value::List(items) = value else {
522        return Err(invalid(
523            entity_type,
524            id,
525            attribute,
526            "an ARRAY [1:2] of two numbers",
527            value,
528        ));
529    };
530    if items.len() != 2 {
531        return Err(MaterialError::InvalidValue {
532            entity: entity_type,
533            id,
534            attribute,
535            value: format!("expected 2 values, found {}", items.len()),
536        });
537    }
538    Ok([
539        number_value(entity_type, id, attribute, &items[0])?,
540        number_value(entity_type, id, attribute, &items[1])?,
541    ])
542}