1use std::collections::HashSet;
4
5use ifc_model::{Budget, Entity, EntityId, Model, Transaction, Value};
6use ifc_schema::{ifc4, Schema, TypeKind};
7
8use crate::author::draft::{AllocationDraft, NestingDraft, ResourceDraft, ResourceTimeDraft};
9use crate::error::{ResourceError, ResourceResult};
10use crate::view::validate_object_assignment;
11use crate::{ResourceKind, ResourceView};
12
13pub struct ResourceEditor<'m> {
19 model: &'m mut Model,
20 pub(crate) schema: &'static Schema,
21}
22
23impl<'m> ResourceEditor<'m> {
24 pub fn for_model(model: &'m mut Model) -> ResourceResult<Self> {
27 ResourceView::for_model(model)?;
28 Ok(Self {
29 model,
30 schema: ifc4(),
31 })
32 }
33
34 pub(crate) fn validate_new_global_id(&self, value: &str) -> ResourceResult<()> {
35 validate_global_id(value)?;
36 for (id, entity) in self.model.iter() {
37 let Some(slot) = self
38 .schema
39 .attribute_names(&entity.type_name)
40 .iter()
41 .position(|name| name.eq_ignore_ascii_case("GlobalId"))
42 else {
43 continue;
44 };
45 if entity
46 .attributes
47 .get(slot)
48 .is_some_and(|stored| matches!(stored.unwrap_typed(), Value::Text(stored) if stored.as_ref() == value))
49 {
50 return Err(ResourceError::SemanticViolation {
51 entity: Some(id),
52 rule: "IfcRoot.GlobalId must be unique in the model",
53 });
54 }
55 }
56 Ok(())
57 }
58
59 pub fn create_resource(&mut self, draft: ResourceDraft<'_>) -> ResourceResult<EntityId> {
64 self.validate_new_global_id(draft.global_id)?;
65 let entity_type = resource_entity_type(draft.kind);
66 if let Some(value) = draft.predefined_type {
67 validate_enum(self.schema, entity_type, "PredefinedType", value)?;
68 if value == "USERDEFINED"
69 && draft
70 .object_type
71 .is_none_or(|value| value.trim().is_empty())
72 {
73 return Err(ResourceError::SemanticViolation {
74 entity: None,
75 rule: "USERDEFINED_REQUIRES_OBJECT_TYPE",
76 });
77 }
78 }
79 if let Some(usage) = draft.usage {
80 self.check_reference(usage, "Usage", "IfcResourceTime", usage)?;
81 }
82 for cost in &draft.base_costs {
83 self.check_reference(*cost, "BaseCosts", "IfcAppliedValue", *cost)?;
84 }
85 if let Some(quantity) = draft.base_quantity {
86 self.check_reference(quantity, "BaseQuantity", "IfcPhysicalQuantity", quantity)?;
87 }
88
89 let base_costs = (!draft.base_costs.is_empty()).then(|| refs(&draft.base_costs));
90 let entity = build_entity(
91 self.schema,
92 entity_type,
93 &[
94 ("GlobalId", Some(text(draft.global_id))),
95 ("Name", draft.name.map(text)),
96 ("ObjectType", draft.object_type.map(text)),
97 ("Identification", draft.identification.map(text)),
98 ("LongDescription", draft.long_description.map(text)),
99 ("Usage", draft.usage.map(Value::Ref)),
100 ("BaseCosts", base_costs),
101 ("BaseQuantity", draft.base_quantity.map(Value::Ref)),
102 (
103 "PredefinedType",
104 draft.predefined_type.map(|value| Value::Enum(value.into())),
105 ),
106 ],
107 )?;
108 self.commit_create(entity)
109 }
110
111 pub fn create_resource_type(&mut self, draft: ResourceDraft<'_>) -> ResourceResult<EntityId> {
129 self.validate_new_global_id(draft.global_id)?;
130 let entity_type = resource_type_entity_type(draft.kind);
131 let Some(predefined_type) = draft.predefined_type else {
132 return Err(ResourceError::SemanticViolation {
133 entity: None,
134 rule: "RESOURCE_TYPE_REQUIRES_PREDEFINED_TYPE",
135 });
136 };
137 validate_enum(self.schema, entity_type, "PredefinedType", predefined_type)?;
138 if predefined_type == "USERDEFINED"
139 && draft
140 .object_type
141 .is_none_or(|value| value.trim().is_empty())
142 {
143 return Err(ResourceError::SemanticViolation {
144 entity: None,
145 rule: "USERDEFINED_REQUIRES_OBJECT_TYPE",
146 });
147 }
148 if draft.usage.is_some() {
149 return Err(ResourceError::SemanticViolation {
150 entity: None,
151 rule: "RESOURCE_TYPE_HAS_NO_USAGE",
152 });
153 }
154 for cost in &draft.base_costs {
155 self.check_reference(*cost, "BaseCosts", "IfcAppliedValue", *cost)?;
156 }
157 if let Some(quantity) = draft.base_quantity {
158 self.check_reference(quantity, "BaseQuantity", "IfcPhysicalQuantity", quantity)?;
159 }
160
161 let base_costs = (!draft.base_costs.is_empty()).then(|| refs(&draft.base_costs));
162 let entity = build_entity(
163 self.schema,
164 entity_type,
165 &[
166 ("GlobalId", Some(text(draft.global_id))),
167 ("Name", draft.name.map(text)),
168 ("ApplicableOccurrence", draft.object_type.map(text)),
169 ("Identification", draft.identification.map(text)),
170 ("LongDescription", draft.long_description.map(text)),
171 ("BaseCosts", base_costs),
172 ("BaseQuantity", draft.base_quantity.map(Value::Ref)),
173 ("PredefinedType", Some(Value::Enum(predefined_type.into()))),
174 ],
175 )?;
176 self.commit_create(entity)
177 }
178
179 pub fn create_time(&mut self, draft: ResourceTimeDraft<'_>) -> ResourceResult<EntityId> {
182 for (attribute, value) in [
183 ("ScheduleUsage", draft.schedule_usage),
184 ("ActualUsage", draft.actual_usage),
185 ("RemainingUsage", draft.remaining_usage),
186 ("Completion", draft.completion),
187 ] {
188 if value.is_some_and(|value| !value.is_finite() || value <= 0.0) {
189 return Err(ResourceError::InvalidDraft {
190 entity_type: "IfcResourceTime",
191 attribute,
192 expected: "finite positive ratio",
193 });
194 }
195 }
196 let entity = build_entity(
197 self.schema,
198 "IfcResourceTime",
199 &[
200 ("Name", draft.name.map(text)),
201 ("ScheduleWork", draft.schedule_work.map(text)),
202 ("ScheduleUsage", draft.schedule_usage.map(Value::Real)),
203 ("ScheduleStart", draft.schedule_start.map(text)),
204 ("ScheduleFinish", draft.schedule_finish.map(text)),
205 ("IsOverAllocated", draft.is_over_allocated.map(Value::Bool)),
206 ("StatusTime", draft.status_time.map(text)),
207 ("ActualWork", draft.actual_work.map(text)),
208 ("ActualUsage", draft.actual_usage.map(Value::Real)),
209 ("ActualStart", draft.actual_start.map(text)),
210 ("ActualFinish", draft.actual_finish.map(text)),
211 ("RemainingWork", draft.remaining_work.map(text)),
212 ("RemainingUsage", draft.remaining_usage.map(Value::Real)),
213 ("Completion", draft.completion.map(Value::Real)),
214 ],
215 )?;
216 self.commit_create(entity)
217 }
218
219 pub fn create_allocation(&mut self, draft: AllocationDraft<'_>) -> ResourceResult<EntityId> {
224 self.validate_new_global_id(draft.global_id)?;
225 self.check_reference_select(
226 draft.resource,
227 "RelatingResource",
228 "IfcResourceSelect",
229 &["IfcResource", "IfcTypeResource"],
230 draft.resource,
231 )?;
232 validate_required_unique(draft.resource, "RelatedObjects", &draft.related_objects)?;
233 if let Some(category) = draft.related_objects_type {
234 validate_enum(
235 self.schema,
236 "IfcRelAssignsToResource",
237 "RelatedObjectsType",
238 category,
239 )?;
240 }
241 for target in &draft.related_objects {
242 self.check_reference(
243 draft.resource,
244 "RelatedObjects",
245 "IfcObjectDefinition",
246 *target,
247 )?;
248 if *target == draft.resource {
249 return Err(ResourceError::SemanticViolation {
250 entity: None,
251 rule: "RESOURCE_ASSIGNMENT_NO_SELF_REFERENCE",
252 });
253 }
254 }
255 validate_object_assignment(
256 self.model,
257 self.schema,
258 None,
259 draft.related_objects_type,
260 &draft.related_objects,
261 )?;
262 let entity = build_entity(
263 self.schema,
264 "IfcRelAssignsToResource",
265 &[
266 ("GlobalId", Some(text(draft.global_id))),
267 ("Name", draft.name.map(text)),
268 ("Description", draft.description.map(text)),
269 ("RelatedObjects", Some(refs(&draft.related_objects))),
270 (
271 "RelatedObjectsType",
272 draft
273 .related_objects_type
274 .map(|value| Value::Enum(value.into())),
275 ),
276 ("RelatingResource", Some(Value::Ref(draft.resource))),
277 ],
278 )?;
279 self.commit_create(entity)
280 }
281
282 pub fn create_nesting(&mut self, draft: NestingDraft<'_>) -> ResourceResult<EntityId> {
287 self.validate_new_global_id(draft.global_id)?;
288 self.check_reference(
289 draft.parent,
290 "RelatingObject",
291 "IfcConstructionResource",
292 draft.parent,
293 )?;
294 validate_required_unique(draft.parent, "RelatedObjects", &draft.children)?;
295 for child in &draft.children {
296 self.check_reference(
297 draft.parent,
298 "RelatedObjects",
299 "IfcConstructionResource",
300 *child,
301 )?;
302 if *child == draft.parent {
303 return Err(ResourceError::SemanticViolation {
304 entity: None,
305 rule: "RESOURCE_NESTING_NO_SELF_REFERENCE",
306 });
307 }
308 }
309 {
310 let view = ResourceView::new(self.model, self.schema)?;
311 for child in &draft.children {
312 if view.parent_resource(*child)?.is_some() {
313 return Err(ResourceError::SemanticViolation {
314 entity: Some(*child),
315 rule: "IfcObject.Nests permits at most one resource parent",
316 });
317 }
318 if view
319 .descendants(*child, Budget::DEFAULT)?
320 .contains(&draft.parent)
321 {
322 return Err(ResourceError::SemanticViolation {
323 entity: Some(*child),
324 rule: "resource nesting must remain acyclic",
325 });
326 }
327 }
328 }
329 let entity = build_entity(
330 self.schema,
331 "IfcRelNests",
332 &[
333 ("GlobalId", Some(text(draft.global_id))),
334 ("Name", draft.name.map(text)),
335 ("Description", draft.description.map(text)),
336 ("RelatingObject", Some(Value::Ref(draft.parent))),
337 ("RelatedObjects", Some(refs(&draft.children))),
338 ],
339 )?;
340 self.commit_create(entity)
341 }
342
343 pub(crate) fn check_reference(
344 &self,
345 owner: EntityId,
346 attribute: &'static str,
347 expected: &'static str,
348 target: EntityId,
349 ) -> ResourceResult<()> {
350 self.check_reference_select(owner, attribute, expected, &[expected], target)
351 }
352
353 pub(crate) fn check_reference_select(
354 &self,
355 owner: EntityId,
356 attribute: &'static str,
357 expected: &'static str,
358 members: &[&str],
359 target: EntityId,
360 ) -> ResourceResult<()> {
361 let entity = self
362 .model
363 .get(target)
364 .ok_or(ResourceError::DanglingReference {
365 entity: owner,
366 attribute,
367 target,
368 })?;
369 if !members
370 .iter()
371 .any(|member| self.schema.is_a(&entity.type_name, member))
372 {
373 return Err(ResourceError::WrongReferenceType {
374 entity: owner,
375 attribute,
376 target,
377 expected,
378 actual: entity.type_name.to_string(),
379 });
380 }
381 Ok(())
382 }
383
384 pub(crate) fn commit_create(&mut self, entity: Entity) -> ResourceResult<EntityId> {
385 let mut transaction = Transaction::new(self.model);
386 let expected = transaction.revision();
387 let id = transaction.create(entity);
388 transaction
389 .commit(self.model)
390 .map_err(|_| ResourceError::TransactionConflict {
391 expected,
392 actual: self.model.revision(),
393 })?;
394 Ok(id)
395 }
396}
397
398fn resource_entity_type(kind: ResourceKind) -> &'static str {
399 match kind {
400 ResourceKind::Labor => "IfcLaborResource",
401 ResourceKind::Equipment => "IfcConstructionEquipmentResource",
402 ResourceKind::Crew => "IfcCrewResource",
403 ResourceKind::Material => "IfcConstructionMaterialResource",
404 ResourceKind::Product => "IfcConstructionProductResource",
405 ResourceKind::Subcontract => "IfcSubContractResource",
406 }
407}
408
409fn resource_type_entity_type(kind: ResourceKind) -> &'static str {
415 match kind {
416 ResourceKind::Labor => "IfcLaborResourceType",
417 ResourceKind::Equipment => "IfcConstructionEquipmentResourceType",
418 ResourceKind::Crew => "IfcCrewResourceType",
419 ResourceKind::Material => "IfcConstructionMaterialResourceType",
420 ResourceKind::Product => "IfcConstructionProductResourceType",
421 ResourceKind::Subcontract => "IfcSubContractResourceType",
422 }
423}
424
425pub(crate) fn build_entity(
426 schema: &Schema,
427 entity_type: &'static str,
428 values: &[(&'static str, Option<Value>)],
429) -> ResourceResult<Entity> {
430 if schema.entity(entity_type).is_none() {
431 return Err(ResourceError::InvalidDraft {
432 entity_type,
433 attribute: "<entity>",
434 expected: "entity declared by IFC4",
435 });
436 }
437 let attribute_names = schema.attribute_names(entity_type);
438 let mut attributes = vec![Value::Null; attribute_names.len()];
439 for (name, value) in values {
440 if let Some(value) = value {
441 let slot = attribute_names
442 .iter()
443 .position(|candidate| candidate.eq_ignore_ascii_case(name))
444 .ok_or(ResourceError::InvalidDraft {
445 entity_type,
446 attribute: name,
447 expected: "attribute declared by IFC4",
448 })?;
449 attributes[slot] = value.clone();
450 }
451 }
452 Ok(Entity::new(entity_type, attributes))
453}
454
455pub(crate) fn validate_enum(
456 schema: &Schema,
457 entity_type: &'static str,
458 attribute: &'static str,
459 value: &str,
460) -> ResourceResult<()> {
461 let declared = schema
462 .attributes(entity_type)
463 .iter()
464 .find(|candidate| candidate.name.eq_ignore_ascii_case(attribute))
465 .and_then(|attribute| schema.type_def(&attribute.type_name))
466 .is_some_and(|definition| {
467 matches!(&definition.kind, TypeKind::Enumeration(values) if values.iter().any(|member| member == value))
468 });
469 if !declared {
470 return Err(ResourceError::InvalidEnumeration {
471 entity: None,
472 attribute,
473 value: value.to_owned(),
474 });
475 }
476 Ok(())
477}
478
479fn validate_required_unique(
480 owner: EntityId,
481 attribute: &'static str,
482 values: &[EntityId],
483) -> ResourceResult<()> {
484 if values.is_empty() {
485 return Err(ResourceError::InvalidCardinality {
486 entity: owner,
487 attribute,
488 minimum: 1,
489 actual: 0,
490 });
491 }
492 let mut unique = HashSet::with_capacity(values.len());
493 for value in values {
494 if !unique.insert(*value) {
495 return Err(ResourceError::DuplicateReference {
496 entity: owner,
497 attribute,
498 target: *value,
499 });
500 }
501 }
502 Ok(())
503}
504
505fn validate_global_id(value: &str) -> ResourceResult<()> {
506 if value.len() != 22
507 || !matches!(value.as_bytes().first(), Some(b'0'..=b'3'))
508 || !value
509 .bytes()
510 .all(|byte| byte.is_ascii_alphanumeric() || byte == b'_' || byte == b'$')
511 {
512 return Err(ResourceError::InvalidGlobalId);
513 }
514 Ok(())
515}
516
517pub(crate) fn text(value: &str) -> Value {
518 Value::Text(value.into())
519}
520
521pub(crate) fn refs(values: &[EntityId]) -> Value {
522 Value::List(values.iter().copied().map(Value::Ref).collect())
523}
524
525#[derive(Debug, Clone, Copy, Default)]
527pub struct AppliedValueDraft<'a> {
528 pub name: Option<&'a str>,
530 pub description: Option<&'a str>,
532 pub applied_value: Option<EntityId>,
534 pub unit_basis: Option<EntityId>,
536 pub applicable_date: Option<&'a str>,
538 pub fixed_until_date: Option<&'a str>,
540 pub category: Option<&'a str>,
542 pub condition: Option<&'a str>,
544 pub arithmetic_operator: Option<&'a str>,
546}
547
548impl ResourceEditor<'_> {
549 pub fn create_applied_value(
571 &mut self,
572 draft: AppliedValueDraft<'_>,
573 components: &[EntityId],
574 ) -> ResourceResult<EntityId> {
575 const ENTITY: &str = "IfcAppliedValue";
576 if let Some(operator) = draft.arithmetic_operator {
577 validate_enum(self.schema, ENTITY, "ArithmeticOperator", operator)?;
578 }
579 if draft.arithmetic_operator.is_some() == components.is_empty() {
580 return Err(ResourceError::SemanticViolation {
581 entity: None,
582 rule: "ArithmeticOperator and Components are stated together",
583 });
584 }
585 if let Some(value) = draft.applied_value {
586 self.check_reference_select(
587 value,
588 "AppliedValue",
589 "IfcAppliedValueSelect",
590 &[
591 "IfcMeasureWithUnit",
592 "IfcMonetaryMeasure",
593 "IfcRatioMeasure",
594 "IfcReference",
595 ],
596 value,
597 )?;
598 }
599 if let Some(basis) = draft.unit_basis {
600 self.check_reference(basis, "UnitBasis", "IfcMeasureWithUnit", basis)?;
601 }
602 for component in components {
603 self.check_reference(*component, "Components", ENTITY, *component)?;
604 }
605
606 let entity = build_entity(
607 self.schema,
608 ENTITY,
609 &[
610 ("Name", draft.name.map(text)),
611 ("Description", draft.description.map(text)),
612 ("AppliedValue", draft.applied_value.map(Value::Ref)),
613 ("UnitBasis", draft.unit_basis.map(Value::Ref)),
614 ("ApplicableDate", draft.applicable_date.map(text)),
615 ("FixedUntilDate", draft.fixed_until_date.map(text)),
616 ("Category", draft.category.map(text)),
617 ("Condition", draft.condition.map(text)),
618 ("ArithmeticOperator", draft.arithmetic_operator.map(enum_of)),
619 (
620 "Components",
621 (!components.is_empty()).then(|| refs(components)),
622 ),
623 ],
624 )?;
625 self.commit_create(entity)
626 }
627}
628
629fn enum_of(token: &str) -> Value {
631 Value::Enum(std::sync::Arc::from(token.to_ascii_uppercase()))
632}