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