1use ifc_model::{Entity, EntityId, Model, Value};
4
5use crate::{LogicalValue, MaterialError, MaterialResult};
6
7const MAX_TYPED_WRAPPERS: usize = 8;
8
9#[derive(Debug, Clone, Copy)]
11pub struct MaterialView<'m> {
12 model: &'m Model,
13}
14
15impl<'m> MaterialView<'m> {
16 pub fn new(model: &'m Model) -> Self {
18 Self { model }
19 }
20
21 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 #[doc = $ifc_name]
40 #[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 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 pub fn id(self) -> ifc_model::EntityId {
73 self.id
74 }
75
76 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}