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 crate::view::borrowed_entity!($name, $ifc_name, []);
63 };
64 ($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 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 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 pub fn id(self) -> ifc_model::EntityId {
145 self.id
146 }
147
148 pub fn entity(self) -> &'m ifc_model::Entity {
150 self.entity
151 }
152
153 pub fn schema(self) -> crate::MaterialResult<crate::SchemaVersion> {
161 self.release.bound().map(|(version, _)| version)
162 }
163
164 #[allow(dead_code)]
166 pub(crate) fn release(self) -> crate::release::Release<'m> {
167 self.release
168 }
169
170 #[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}