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