1use 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#[derive(Debug, Clone, Copy)]
16pub struct MaterialView<'m> {
17 model: &'m Model,
18 release: Release<'m>,
19}
20
21impl<'m> MaterialView<'m> {
22 pub fn new(model: &'m Model) -> Self {
25 Self {
26 model,
27 release: Release::of(model),
28 }
29 }
30
31 pub fn model(self) -> &'m Model {
33 self.model
34 }
35
36 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 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 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 pub fn id(self) -> ifc_model::EntityId {
140 self.id
141 }
142
143 pub fn entity(self) -> &'m ifc_model::Entity {
145 self.entity
146 }
147
148 pub fn schema(self) -> crate::MaterialResult<crate::SchemaVersion> {
156 self.release.bound().map(|(version, _)| version)
157 }
158
159 #[allow(dead_code)]
161 pub(crate) fn release(self) -> crate::release::Release<'m> {
162 self.release
163 }
164
165 #[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}