1use std::collections::HashSet;
18
19use ifc_model::{Entity, EntityId, Model, Value};
20use ifc_schema::{ifc4, ifc4x3, Schema, SchemaVersion, TypeKind};
21
22use crate::error::{ResourceError, ResourceResult};
23use crate::{ConstructionResource, ResourceTime};
24
25#[derive(Debug, Clone, Copy)]
26pub struct ResourceView<'m, 's> {
30 pub(crate) model: &'m Model,
31 pub(crate) schema: &'s Schema,
32}
33
34impl<'m, 's> ResourceView<'m, 's> {
35 pub fn new(model: &'m Model, schema: &'s Schema) -> ResourceResult<Self> {
39 let Some(version) = schema.version() else {
40 return Err(ResourceError::UnsupportedSchema {
41 token: schema.name().to_owned(),
42 });
43 };
44 if !matches!(version, SchemaVersion::Ifc4 | SchemaVersion::Ifc4x3) {
45 return Err(ResourceError::UnsupportedSchema {
46 token: schema.name().to_owned(),
47 });
48 }
49 match model.header().schema.as_slice() {
50 [] => {}
51 [token] if SchemaVersion::from_header_token(token) == Some(version) => {}
52 [token] => {
53 return Err(ResourceError::UnsupportedSchema {
54 token: token.clone(),
55 });
56 }
57 tokens => {
58 return Err(ResourceError::AmbiguousSchema {
59 tokens: tokens.to_vec(),
60 });
61 }
62 }
63 Ok(Self { model, schema })
64 }
65
66 #[must_use]
68 pub fn schema(&self) -> &'s Schema {
69 self.schema
70 }
71
72 pub fn resource(&self, id: EntityId) -> ResourceResult<ConstructionResource<'m, 's>> {
75 ConstructionResource::from_record(self.record(id, "IfcConstructionResource")?)
76 }
77
78 pub fn resource_time(&self, id: EntityId) -> ResourceResult<ResourceTime<'m, 's>> {
80 Ok(ResourceTime::from_record(
81 self.record(id, "IfcResourceTime")?,
82 ))
83 }
84
85 pub(crate) fn record(
86 &self,
87 id: EntityId,
88 expected: &'static str,
89 ) -> ResourceResult<Record<'m, 's>> {
90 Record::new(self.model, self.schema, id, expected)
91 }
92
93 pub(crate) fn ids_of_ancestor(&self, ancestor: &str) -> Vec<EntityId> {
94 self.model
95 .iter()
96 .filter_map(|(id, entity)| self.schema.is_a(&entity.type_name, ancestor).then_some(id))
97 .collect()
98 }
99}
100
101impl<'m> ResourceView<'m, 'static> {
102 pub fn for_model(model: &'m Model) -> ResourceResult<Self> {
106 let token = match model.header().schema.as_slice() {
107 [] => return Err(ResourceError::MissingSchema),
108 [token] => token,
109 tokens => {
110 return Err(ResourceError::AmbiguousSchema {
111 tokens: tokens.to_vec(),
112 });
113 }
114 };
115 let version = SchemaVersion::from_header_token(token).ok_or_else(|| {
116 ResourceError::UnsupportedSchema {
117 token: token.clone(),
118 }
119 })?;
120 let schema = match version {
121 SchemaVersion::Ifc4 => ifc4(),
122 SchemaVersion::Ifc4x3 => ifc4x3(),
123 _ => {
126 return Err(ResourceError::UnsupportedSchema {
127 token: token.clone(),
128 });
129 }
130 #[allow(unreachable_patterns)]
133 _ => {
134 return Err(ResourceError::UnsupportedSchema {
135 token: token.clone(),
136 });
137 }
138 };
139 Self::new(model, schema)
140 }
141}
142
143pub(crate) fn validate_object_assignment(
144 model: &Model,
145 schema: &Schema,
146 relation: Option<EntityId>,
147 related_objects_type: Option<&str>,
148 related_objects: &[EntityId],
149) -> ResourceResult<()> {
150 let Some(category) = related_objects_type else {
151 return Ok(());
152 };
153 let expected = if category.eq_ignore_ascii_case("NOTDEFINED") {
154 return Ok(());
155 } else if category.eq_ignore_ascii_case("PRODUCT") {
156 "IfcProduct"
157 } else if category.eq_ignore_ascii_case("PROCESS") {
158 "IfcProcess"
159 } else if category.eq_ignore_ascii_case("CONTROL") {
160 "IfcControl"
161 } else if category.eq_ignore_ascii_case("RESOURCE") {
162 "IfcResource"
163 } else if category.eq_ignore_ascii_case("ACTOR") {
164 "IfcActor"
165 } else if category.eq_ignore_ascii_case("GROUP") {
166 "IfcGroup"
167 } else if category.eq_ignore_ascii_case("PROJECT") {
168 "IfcProject"
169 } else {
170 return Err(ResourceError::InvalidEnumeration {
171 entity: relation,
172 attribute: "RelatedObjectsType",
173 value: category.to_owned(),
174 });
175 };
176 if related_objects.iter().any(|target| {
177 model
178 .get(*target)
179 .is_none_or(|entity| !schema.is_a(&entity.type_name, expected))
180 }) {
181 return Err(ResourceError::SemanticViolation {
182 entity: relation,
183 rule: "IfcRelAssigns.WR1_IfcCorrectObjectAssignment",
184 });
185 }
186 Ok(())
187}
188
189#[derive(Debug, Clone, Copy)]
190pub(crate) struct Record<'m, 's> {
191 pub(crate) model: &'m Model,
192 pub(crate) schema: &'s Schema,
193 pub(crate) id: EntityId,
194 pub(crate) entity: &'m Entity,
195}
196
197impl<'m, 's> Record<'m, 's> {
198 pub(crate) fn new(
199 model: &'m Model,
200 schema: &'s Schema,
201 id: EntityId,
202 expected: &'static str,
203 ) -> ResourceResult<Self> {
204 let entity = model.get(id).ok_or(ResourceError::EntityNotFound { id })?;
205 if !schema.is_a(&entity.type_name, expected) {
206 return Err(ResourceError::WrongType {
207 id,
208 expected,
209 actual: entity.type_name.to_string(),
210 });
211 }
212 Ok(Self {
213 model,
214 schema,
215 id,
216 entity,
217 })
218 }
219
220 fn slot(&self, attribute: &'static str) -> ResourceResult<usize> {
221 self.schema
222 .attribute_names(&self.entity.type_name)
223 .iter()
224 .position(|name| name.eq_ignore_ascii_case(attribute))
225 .ok_or(ResourceError::MissingAttribute {
226 entity: self.id,
227 attribute,
228 })
229 }
230
231 pub(crate) fn value(&self, attribute: &'static str) -> ResourceResult<&'m Value> {
232 self.entity
233 .attributes
234 .get(self.slot(attribute)?)
235 .ok_or(ResourceError::MissingAttribute {
236 entity: self.id,
237 attribute,
238 })
239 }
240
241 pub(crate) fn required_text(&self, attribute: &'static str) -> ResourceResult<&'m str> {
242 match self.value(attribute)?.unwrap_typed() {
243 Value::Text(value) => Ok(value),
244 _ => Err(self.invalid(attribute, "text")),
245 }
246 }
247
248 pub(crate) fn optional_text_list(
249 &self,
250 attribute: &'static str,
251 minimum: usize,
252 ) -> ResourceResult<Vec<&'m str>> {
253 let values = match self.value(attribute)?.unwrap_typed() {
254 Value::Null | Value::Derived => return Ok(Vec::new()),
255 Value::List(values) => values,
256 _ => return Err(self.invalid(attribute, "aggregate of text or null")),
257 };
258 if values.len() < minimum {
259 return Err(ResourceError::InvalidCardinality {
260 entity: self.id,
261 attribute,
262 minimum,
263 actual: values.len(),
264 });
265 }
266 values
267 .iter()
268 .map(|value| match value.unwrap_typed() {
269 Value::Text(value) => Ok(value.as_ref()),
270 _ => Err(self.invalid(attribute, "aggregate of text or null")),
271 })
272 .collect()
273 }
274
275 pub(crate) fn optional_text(&self, attribute: &'static str) -> ResourceResult<Option<&'m str>> {
276 match self.value(attribute)?.unwrap_typed() {
277 Value::Null | Value::Derived => Ok(None),
278 Value::Text(value) => Ok(Some(value)),
279 _ => Err(self.invalid(attribute, "text or null")),
280 }
281 }
282
283 pub(crate) fn optional_bool(&self, attribute: &'static str) -> ResourceResult<Option<bool>> {
284 match self.value(attribute)?.unwrap_typed() {
285 Value::Null | Value::Derived => Ok(None),
286 Value::Bool(value) => Ok(Some(*value)),
287 _ => Err(self.invalid(attribute, "boolean or null")),
288 }
289 }
290
291 pub(crate) fn optional_positive_number(
292 &self,
293 attribute: &'static str,
294 ) -> ResourceResult<Option<f64>> {
295 let value = match self.value(attribute)?.unwrap_typed() {
296 Value::Null | Value::Derived => return Ok(None),
297 Value::Integer(value) => *value as f64,
298 Value::Real(value) => *value,
299 _ => return Err(self.invalid(attribute, "finite positive number or null")),
300 };
301 if !value.is_finite() || value <= 0.0 {
302 return Err(self.invalid(attribute, "finite positive number or null"));
303 }
304 Ok(Some(value))
305 }
306
307 pub(crate) fn required_non_negative_number(
308 &self,
309 attribute: &'static str,
310 ) -> ResourceResult<f64> {
311 let value = match self.value(attribute)?.unwrap_typed() {
312 Value::Integer(value) => *value as f64,
313 Value::Real(value) => *value,
314 _ => return Err(self.invalid(attribute, "finite non-negative number")),
315 };
316 if !value.is_finite() || value < 0.0 {
317 return Err(self.invalid(attribute, "finite non-negative number"));
318 }
319 Ok(value)
320 }
321
322 pub(crate) fn required_enum(&self, attribute: &'static str) -> ResourceResult<&'m str> {
323 let value = match self.value(attribute)?.unwrap_typed() {
324 Value::Enum(value) => value,
325 _ => return Err(self.invalid(attribute, "declared enumeration")),
326 };
327 if !self.declares_enum_member(attribute, value) {
328 return Err(ResourceError::InvalidEnumeration {
329 entity: Some(self.id),
330 attribute,
331 value: value.to_string(),
332 });
333 }
334 Ok(value)
335 }
336
337 pub(crate) fn optional_enum(&self, attribute: &'static str) -> ResourceResult<Option<&'m str>> {
338 let value = match self.value(attribute)?.unwrap_typed() {
339 Value::Null | Value::Derived => return Ok(None),
340 Value::Enum(value) => value,
341 _ => return Err(self.invalid(attribute, "declared enumeration or null")),
342 };
343 if !self.declares_enum_member(attribute, value) {
344 return Err(ResourceError::InvalidEnumeration {
345 entity: Some(self.id),
346 attribute,
347 value: value.to_string(),
348 });
349 }
350 Ok(Some(value))
351 }
352
353 fn declares_enum_member(&self, attribute: &str, value: &str) -> bool {
354 let declarations = self.schema.attributes(&self.entity.type_name);
355 let Some(declaration) = declarations
356 .iter()
357 .find(|candidate| candidate.name.eq_ignore_ascii_case(attribute))
358 else {
359 return false;
360 };
361 let mut type_name = declaration.type_name.as_str();
362 for _ in 0..16 {
363 let Some(definition) = self.schema.type_def(type_name) else {
364 return false;
365 };
366 match &definition.kind {
367 TypeKind::Enumeration(members) => {
368 return members
369 .iter()
370 .any(|member| member.eq_ignore_ascii_case(value));
371 }
372 TypeKind::Defined(alias) => type_name = alias,
373 TypeKind::Select(_) => return false,
374 _ => return false,
375 }
376 }
377 false
378 }
379
380 pub(crate) fn optional_ref(
381 &self,
382 attribute: &'static str,
383 expected: &'static str,
384 ) -> ResourceResult<Option<EntityId>> {
385 let target = match self.value(attribute)?.unwrap_typed() {
386 Value::Null | Value::Derived => return Ok(None),
387 Value::Ref(target) => *target,
388 _ => return Err(self.invalid(attribute, "entity reference or null")),
389 };
390 self.check_reference(attribute, target, &[expected], expected)?;
391 Ok(Some(target))
392 }
393
394 pub(crate) fn required_ref(
395 &self,
396 attribute: &'static str,
397 expected: &'static str,
398 ) -> ResourceResult<EntityId> {
399 let Value::Ref(target) = self.value(attribute)?.unwrap_typed() else {
400 return Err(self.invalid(attribute, "entity reference"));
401 };
402 self.check_reference(attribute, *target, &[expected], expected)?;
403 Ok(*target)
404 }
405
406 pub(crate) fn optional_ref_select(
407 &self,
408 attribute: &'static str,
409 expected: &'static str,
410 members: &[&str],
411 ) -> ResourceResult<Option<EntityId>> {
412 let target = match self.value(attribute)?.unwrap_typed() {
413 Value::Null | Value::Derived => return Ok(None),
414 Value::Ref(target) => *target,
415 _ => return Err(self.invalid(attribute, "entity reference or null")),
416 };
417 self.check_reference(attribute, target, members, expected)?;
418 Ok(Some(target))
419 }
420
421 pub(crate) fn required_ref_select(
422 &self,
423 attribute: &'static str,
424 expected: &'static str,
425 members: &[&str],
426 ) -> ResourceResult<EntityId> {
427 let Value::Ref(target) = self.value(attribute)?.unwrap_typed() else {
428 return Err(self.invalid(attribute, "entity reference"));
429 };
430 self.check_reference(attribute, *target, members, expected)?;
431 Ok(*target)
432 }
433
434 pub(crate) fn refs(
435 &self,
436 attribute: &'static str,
437 expected: &'static str,
438 minimum: usize,
439 optional: bool,
440 unique: bool,
441 ) -> ResourceResult<Vec<EntityId>> {
442 let values = match self.value(attribute)?.unwrap_typed() {
443 Value::Null | Value::Derived if optional => return Ok(Vec::new()),
444 Value::List(values) => values,
445 _ => return Err(self.invalid(attribute, "aggregate of entity references")),
446 };
447 if values.len() < minimum {
448 return Err(ResourceError::InvalidCardinality {
449 entity: self.id,
450 attribute,
451 minimum,
452 actual: values.len(),
453 });
454 }
455 let mut targets = Vec::with_capacity(values.len());
456 let mut seen = HashSet::with_capacity(values.len());
457 for value in values {
458 let Value::Ref(target) = value.unwrap_typed() else {
459 return Err(self.invalid(attribute, "aggregate of entity references"));
460 };
461 if unique && !seen.insert(*target) {
462 return Err(ResourceError::DuplicateReference {
463 entity: self.id,
464 attribute,
465 target: *target,
466 });
467 }
468 self.check_reference(attribute, *target, &[expected], expected)?;
469 targets.push(*target);
470 }
471 Ok(targets)
472 }
473
474 pub(crate) fn check_reference(
475 &self,
476 attribute: &'static str,
477 target: EntityId,
478 members: &[&str],
479 expected: &'static str,
480 ) -> ResourceResult<()> {
481 let entity = self
482 .model
483 .get(target)
484 .ok_or(ResourceError::DanglingReference {
485 entity: self.id,
486 attribute,
487 target,
488 })?;
489 if !members
490 .iter()
491 .any(|member| self.schema.is_a(&entity.type_name, member))
492 {
493 return Err(ResourceError::WrongReferenceType {
494 entity: self.id,
495 attribute,
496 target,
497 expected,
498 actual: entity.type_name.to_string(),
499 });
500 }
501 Ok(())
502 }
503
504 pub(crate) fn require_object_type_if(
505 &self,
506 condition: bool,
507 rule: &'static str,
508 ) -> ResourceResult<()> {
509 if condition
510 && self
511 .optional_text("ObjectType")?
512 .is_none_or(|value| value.trim().is_empty())
513 {
514 return Err(ResourceError::SemanticViolation {
515 entity: Some(self.id),
516 rule,
517 });
518 }
519 Ok(())
520 }
521
522 fn invalid(&self, attribute: &'static str, expected: &'static str) -> ResourceError {
523 ResourceError::InvalidValue {
524 entity: self.id,
525 attribute,
526 expected,
527 }
528 }
529}
530
531#[cfg(test)]
532mod intermediate_release_tests {
533 use super::*;
534
535 #[test]
538 fn ifc4x1_and_ifc4x2_are_refused_not_aliased() {
539 for token in ["IFC4X1", "IFC4X2"] {
540 let mut model = Model::new();
541 model.header_mut().schema = vec![token.to_owned()];
542 assert!(
543 matches!(
544 ResourceView::for_model(&model),
545 Err(ResourceError::UnsupportedSchema { token: found }) if found == token
546 ),
547 "{token} must be refused"
548 );
549 }
550 }
551}