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