1use std::collections::HashSet;
10use std::sync::Arc;
11
12use ifc_model::{Edit, Entity, EntityId, Model, Transaction, Value};
13use ifc_schema::{Schema, TypeKind};
14
15use crate::datetime::DateTimeInput;
16use crate::release::{bind, Layout};
17
18use crate::types::{
19 Benchmark, ConstraintGrade, LogicalOperator, MetricValueDraft, ObjectiveQualifier,
20};
21use crate::{ConstraintError, ConstraintResult};
22
23const METRIC: &str = "IFCMETRIC";
24const OBJECTIVE: &str = "IFCOBJECTIVE";
25const RESOURCE_REL: &str = "IFCRESOURCECONSTRAINTRELATIONSHIP";
26
27#[derive(Debug, Clone, Copy)]
29#[non_exhaustive]
30pub struct ConstraintBaseDraft<'a> {
31 pub name: &'a str,
33 pub description: Option<&'a str>,
35 pub grade: ConstraintGrade,
37 pub source: Option<&'a str>,
39 pub creating_actor: Option<EntityId>,
41 pub creation_time: Option<DateTimeInput<'a>>,
44 pub user_defined_grade: Option<&'a str>,
46}
47
48impl<'a> ConstraintBaseDraft<'a> {
49 #[must_use]
51 pub fn new(name: &'a str, grade: ConstraintGrade) -> Self {
52 Self {
53 name,
54 description: None,
55 grade,
56 source: None,
57 creating_actor: None,
58 creation_time: None,
59 user_defined_grade: None,
60 }
61 }
62
63 #[must_use]
65 pub fn description(mut self, value: &'a str) -> Self {
66 self.description = Some(value);
67 self
68 }
69
70 #[must_use]
72 pub fn source(mut self, value: &'a str) -> Self {
73 self.source = Some(value);
74 self
75 }
76
77 #[must_use]
80 pub fn creating_actor(mut self, value: EntityId) -> Self {
81 self.creating_actor = Some(value);
82 self
83 }
84
85 #[must_use]
87 pub fn creation_time(mut self, value: impl Into<DateTimeInput<'a>>) -> Self {
88 self.creation_time = Some(value.into());
89 self
90 }
91
92 #[must_use]
94 pub fn user_defined_grade(mut self, value: &'a str) -> Self {
95 self.user_defined_grade = Some(value);
96 self
97 }
98}
99
100#[derive(Debug, Clone, Copy)]
102#[non_exhaustive]
103pub struct MetricDraft<'a> {
104 pub base: ConstraintBaseDraft<'a>,
106 pub benchmark: Benchmark,
108 pub value_source: Option<&'a str>,
110 pub data_value: Option<MetricValueDraft<'a>>,
112 pub reference_path: Option<EntityId>,
114}
115
116impl<'a> MetricDraft<'a> {
117 #[must_use]
119 pub fn new(base: ConstraintBaseDraft<'a>, benchmark: Benchmark) -> Self {
120 Self {
121 base,
122 benchmark,
123 value_source: None,
124 data_value: None,
125 reference_path: None,
126 }
127 }
128
129 #[must_use]
131 pub fn value_source(mut self, value: &'a str) -> Self {
132 self.value_source = Some(value);
133 self
134 }
135
136 #[must_use]
138 pub fn data_value(mut self, value: MetricValueDraft<'a>) -> Self {
139 self.data_value = Some(value);
140 self
141 }
142
143 #[must_use]
146 pub fn reference_path(mut self, value: EntityId) -> Self {
147 self.reference_path = Some(value);
148 self
149 }
150}
151
152#[derive(Debug, Clone, Copy)]
154#[non_exhaustive]
155pub struct ObjectiveDraft<'a> {
156 pub base: ConstraintBaseDraft<'a>,
158 pub benchmark_values: Option<&'a [EntityId]>,
160 pub logical_aggregator: Option<LogicalOperator>,
162 pub qualifier: ObjectiveQualifier,
164 pub user_defined_qualifier: Option<&'a str>,
166}
167
168impl<'a> ObjectiveDraft<'a> {
169 #[must_use]
171 pub fn new(base: ConstraintBaseDraft<'a>, qualifier: ObjectiveQualifier) -> Self {
172 Self {
173 base,
174 benchmark_values: None,
175 logical_aggregator: None,
176 qualifier,
177 user_defined_qualifier: None,
178 }
179 }
180
181 #[must_use]
183 pub fn benchmark_values(mut self, value: &'a [EntityId]) -> Self {
184 self.benchmark_values = Some(value);
185 self
186 }
187
188 #[must_use]
190 pub fn logical_aggregator(mut self, value: LogicalOperator) -> Self {
191 self.logical_aggregator = Some(value);
192 self
193 }
194
195 #[must_use]
198 pub fn user_defined_qualifier(mut self, value: &'a str) -> Self {
199 self.user_defined_qualifier = Some(value);
200 self
201 }
202}
203
204#[derive(Debug, Clone, Copy)]
206#[non_exhaustive]
207pub struct ResourceConstraintDraft<'a> {
208 pub name: Option<&'a str>,
210 pub description: Option<&'a str>,
212 pub relating_constraint: EntityId,
214 pub related_resources: &'a [EntityId],
216}
217
218impl<'a> ResourceConstraintDraft<'a> {
219 #[must_use]
221 pub fn new(relating_constraint: EntityId, related_resources: &'a [EntityId]) -> Self {
222 Self {
223 name: None,
224 description: None,
225 relating_constraint,
226 related_resources,
227 }
228 }
229
230 #[must_use]
232 pub fn name(mut self, value: &'a str) -> Self {
233 self.name = Some(value);
234 self
235 }
236
237 #[must_use]
239 pub fn description(mut self, value: &'a str) -> Self {
240 self.description = Some(value);
241 self
242 }
243}
244
245pub fn create_metric(
265 tx: &mut Transaction,
266 model: &Model,
267 draft: MetricDraft<'_>,
268) -> ConstraintResult<EntityId> {
269 let layout = bind(model)?;
270 layout.require_entity(METRIC)?;
271 let base = base_values(layout, tx, model, METRIC, draft.base)?;
272 if let Some(path) = draft.reference_path {
273 if layout.declared(METRIC, "ReferencePath").is_some() {
274 validate_target_in(layout.schema(), tx, model, path, "IfcReference")?;
275 }
276 }
277 let data_value = match draft.data_value {
278 None => Value::Null,
279 Some(MetricValueDraft::Entity(target)) => {
280 validate_target_in(layout.schema(), tx, model, target, "IfcMetricValueSelect")?;
281 Value::Ref(target)
282 }
283 Some(MetricValueDraft::Typed { type_name, value }) => {
284 if !layout
285 .schema()
286 .accepts_type("IfcMetricValueSelect", type_name)
287 {
288 return Err(ConstraintError::AuthoringInvalid {
289 entity: METRIC,
290 attribute: "DataValue",
291 value: format!(
292 "type {type_name} is outside {:?} IfcMetricValueSelect",
293 layout.version()
294 ),
295 });
296 }
297 Value::Typed {
298 type_name: Arc::from(type_name.to_ascii_uppercase()),
299 value: Box::new(value.clone()),
300 }
301 }
302 };
303 let mut values = base;
304 values.extend([
305 (
306 "Benchmark",
307 enumerator(layout, METRIC, "Benchmark", draft.benchmark.token())?,
308 ),
309 ("ValueSource", optional_text(draft.value_source)),
310 ("DataValue", data_value),
311 ("ReferencePath", optional_ref(draft.reference_path)),
312 ]);
313 let record = layout.named_record(METRIC, values)?;
314 Ok(tx.create(record))
315}
316
317pub fn create_objective(
336 tx: &mut Transaction,
337 model: &Model,
338 draft: ObjectiveDraft<'_>,
339) -> ConstraintResult<EntityId> {
340 let layout = bind(model)?;
341 layout.require_entity(OBJECTIVE)?;
342 let base = base_values(layout, tx, model, OBJECTIVE, draft.base)?;
343 if draft.qualifier == ObjectiveQualifier::UserDefined && draft.user_defined_qualifier.is_none()
344 {
345 return Err(ConstraintError::AuthoringInvalid {
346 entity: OBJECTIVE,
347 attribute: "WR21",
348 value: "USERDEFINED qualifier requires UserDefinedQualifier".into(),
349 });
350 }
351 let benchmarks = match draft.benchmark_values {
352 None => Value::Null,
353 Some([]) => {
354 return Err(ConstraintError::AuthoringInvalid {
355 entity: OBJECTIVE,
356 attribute: "BenchmarkValues",
357 value: "empty LIST [1:?]".into(),
358 });
359 }
360 Some(values) => {
361 let (aggregate, expected) = layout
362 .declared(OBJECTIVE, "BenchmarkValues")
363 .map_or((true, "IfcConstraint"), |(_, declared)| {
364 (declared.aggregate, declared.type_name.as_str())
365 });
366 for &target in values {
367 validate_target_in(layout.schema(), tx, model, target, expected)?;
368 }
369 match (aggregate, values) {
370 (true, values) => refs(values),
371 (false, [one]) => Value::Ref(*one),
372 (false, _) => {
373 return Err(ConstraintError::AuthoringValueType {
374 entity: OBJECTIVE,
375 attribute: "BenchmarkValues",
376 declared: expected,
377 schema: layout.version(),
378 })
379 }
380 }
381 }
382 };
383 let aggregator = match draft.logical_aggregator {
384 None => Value::Null,
385 Some(value) => enumerator(layout, OBJECTIVE, "LogicalAggregator", value.token())?,
386 };
387 let mut values = base;
388 values.extend([
389 ("BenchmarkValues", benchmarks),
390 ("LogicalAggregator", aggregator),
391 (
392 "ObjectiveQualifier",
393 enumerator(
394 layout,
395 OBJECTIVE,
396 "ObjectiveQualifier",
397 draft.qualifier.token(),
398 )?,
399 ),
400 (
401 "UserDefinedQualifier",
402 optional_text(draft.user_defined_qualifier),
403 ),
404 ]);
405 let record = layout.named_record(OBJECTIVE, values)?;
406 Ok(tx.create(record))
407}
408
409pub fn relate_resource_constraint(
420 tx: &mut Transaction,
421 model: &Model,
422 draft: ResourceConstraintDraft<'_>,
423) -> ConstraintResult<EntityId> {
424 let layout = bind(model)?;
425 layout.require_entity(RESOURCE_REL)?;
426 validate_target_in(
427 layout.schema(),
428 tx,
429 model,
430 draft.relating_constraint,
431 "IfcConstraint",
432 )?;
433 validate_set(
434 layout.schema(),
435 tx,
436 model,
437 RESOURCE_REL,
438 "RelatedResourceObjects",
439 draft.related_resources,
440 "IfcResourceObjectSelect",
441 )?;
442 let record = layout.named_record(
443 RESOURCE_REL,
444 vec![
445 ("Name", optional_text(draft.name)),
446 ("Description", optional_text(draft.description)),
447 ("RelatingConstraint", Value::Ref(draft.relating_constraint)),
448 ("RelatedResourceObjects", refs(draft.related_resources)),
449 ],
450 )?;
451 Ok(tx.create(record))
452}
453
454fn base_values(
457 layout: Layout,
458 tx: &Transaction,
459 model: &Model,
460 kind: &'static str,
461 draft: ConstraintBaseDraft<'_>,
462) -> ConstraintResult<Vec<(&'static str, Value)>> {
463 if draft.grade == ConstraintGrade::UserDefined && draft.user_defined_grade.is_none() {
464 return Err(ConstraintError::AuthoringInvalid {
465 entity: kind,
466 attribute: "WR11",
467 value: "USERDEFINED grade requires UserDefinedGrade".into(),
468 });
469 }
470 if let Some(actor) = draft.creating_actor {
471 validate_target_in(layout.schema(), tx, model, actor, "IfcActorSelect")?;
472 }
473 let creation_time = match draft.creation_time {
474 None => Value::Null,
475 Some(DateTimeInput::Text(text)) => Value::Text(Arc::from(text)),
476 Some(DateTimeInput::Record(target)) => {
477 if let Some((_, declared)) = layout.declared(kind, "CreationTime") {
478 if layout.admits_entity(&declared.type_name, 8) {
479 validate_target_in(
480 layout.schema(),
481 tx,
482 model,
483 target,
484 declared.type_name.as_str(),
485 )?;
486 }
487 }
488 Value::Ref(target)
489 }
490 };
491 Ok(vec![
492 ("Name", text(draft.name)),
493 ("Description", optional_text(draft.description)),
494 (
495 "ConstraintGrade",
496 enumerator(layout, kind, "ConstraintGrade", draft.grade.token())?,
497 ),
498 ("ConstraintSource", optional_text(draft.source)),
499 ("CreatingActor", optional_ref(draft.creating_actor)),
500 ("CreationTime", creation_time),
501 ("UserDefinedGrade", optional_text(draft.user_defined_grade)),
502 ])
503}
504
505fn enumerator(
509 layout: Layout,
510 kind: &'static str,
511 attribute: &'static str,
512 token: &str,
513) -> ConstraintResult<Value> {
514 let Some((_, declared)) = layout.declared(kind, attribute) else {
515 return Err(ConstraintError::AuthoringNotInSchema {
516 entity: kind,
517 attribute,
518 schema: layout.version(),
519 });
520 };
521 match layout
522 .schema()
523 .type_def(&declared.type_name)
524 .map(|t| &t.kind)
525 {
526 Some(TypeKind::Enumeration(members))
527 if members
528 .iter()
529 .any(|member| member.eq_ignore_ascii_case(token)) =>
530 {
531 Ok(enumeration(token))
532 }
533 _ => Err(ConstraintError::AuthoringValueType {
534 entity: kind,
535 attribute,
536 declared: declared.type_name.as_str(),
537 schema: layout.version(),
538 }),
539 }
540}
541
542fn validate_set(
543 schema: &Schema,
544 tx: &Transaction,
545 model: &Model,
546 kind: &'static str,
547 attribute: &'static str,
548 targets: &[EntityId],
549 expected: &'static str,
550) -> ConstraintResult<()> {
551 if targets.is_empty() {
552 return Err(ConstraintError::AuthoringInvalid {
553 entity: kind,
554 attribute,
555 value: "empty SET [1:?]".into(),
556 });
557 }
558 let mut seen = HashSet::new();
559 for &target in targets {
560 if !seen.insert(target) {
561 return Err(ConstraintError::AuthoringInvalid {
562 entity: kind,
563 attribute,
564 value: format!("duplicate {target}"),
565 });
566 }
567 validate_target_in(schema, tx, model, target, expected)?;
568 }
569 Ok(())
570}
571
572#[derive(Debug, Clone, Copy, Default)]
574#[non_exhaustive]
575pub struct ReferenceDraft<'a> {
576 pub type_identifier: Option<&'a str>,
578 pub attribute_identifier: Option<&'a str>,
580 pub instance_name: Option<&'a str>,
582 pub list_positions: &'a [i64],
584 pub inner_reference: Option<EntityId>,
586}
587
588impl<'a> ReferenceDraft<'a> {
589 #[must_use]
591 pub fn new() -> Self {
592 Self {
593 type_identifier: None,
594 attribute_identifier: None,
595 instance_name: None,
596 list_positions: &[],
597 inner_reference: None,
598 }
599 }
600
601 #[must_use]
604 pub fn type_identifier(mut self, value: &'a str) -> Self {
605 self.type_identifier = Some(value);
606 self
607 }
608
609 #[must_use]
612 pub fn attribute_identifier(mut self, value: &'a str) -> Self {
613 self.attribute_identifier = Some(value);
614 self
615 }
616
617 #[must_use]
619 pub fn instance_name(mut self, value: &'a str) -> Self {
620 self.instance_name = Some(value);
621 self
622 }
623
624 #[must_use]
627 pub fn list_positions(mut self, value: &'a [i64]) -> Self {
628 self.list_positions = value;
629 self
630 }
631
632 #[must_use]
634 pub fn inner_reference(mut self, value: EntityId) -> Self {
635 self.inner_reference = Some(value);
636 self
637 }
638}
639
640pub fn create_reference(
659 tx: &mut Transaction,
660 model: &Model,
661 draft: ReferenceDraft<'_>,
662) -> ConstraintResult<EntityId> {
663 const ENTITY: &str = "IfcReference";
664 let layout = bind(model)?;
665 layout.require_entity("IFCREFERENCE")?;
666 let empty = draft.type_identifier.is_none()
667 && draft.attribute_identifier.is_none()
668 && draft.instance_name.is_none()
669 && draft.list_positions.is_empty()
670 && draft.inner_reference.is_none();
671 if empty {
672 return Err(ConstraintError::AuthoringInvalid {
673 entity: ENTITY,
674 attribute: "TypeIdentifier",
675 value: "a reference with every slot unset addresses nothing".to_owned(),
676 });
677 }
678 for position in draft.list_positions {
679 if *position < 1 {
680 return Err(ConstraintError::AuthoringInvalid {
681 entity: ENTITY,
682 attribute: "ListPositions",
683 value: format!("{position} is not a 1-based list index"),
684 });
685 }
686 }
687 if let Some(inner) = draft.inner_reference {
688 validate_target_in(layout.schema(), tx, model, inner, ENTITY)?;
689 }
690
691 let positions = if draft.list_positions.is_empty() {
692 Value::Null
695 } else {
696 Value::List(
697 draft
698 .list_positions
699 .iter()
700 .copied()
701 .map(Value::Integer)
702 .collect(),
703 )
704 };
705 Ok(tx.create(Entity::new(
706 "IFCREFERENCE",
707 vec![
708 optional_text(draft.type_identifier),
709 optional_text(draft.attribute_identifier),
710 optional_text(draft.instance_name),
711 positions,
712 draft.inner_reference.map_or(Value::Null, Value::Ref),
713 ],
714 )))
715}
716
717pub(crate) fn validate_target_in(
720 schema: &Schema,
721 tx: &Transaction,
722 model: &Model,
723 target: EntityId,
724 expected: &'static str,
725) -> ConstraintResult<()> {
726 let actual =
727 final_type(tx, model, target).ok_or(ConstraintError::UnknownEntity { id: target })?;
728 if schema.accepts_type(expected, actual) {
729 Ok(())
730 } else {
731 Err(ConstraintError::AuthoringReferenceType {
732 target,
733 expected,
734 actual: actual.into(),
735 })
736 }
737}
738
739pub(crate) fn final_type<'a>(
740 tx: &'a Transaction,
741 model: &'a Model,
742 id: EntityId,
743) -> Option<&'a str> {
744 for edit in tx.edits().iter().rev() {
745 match edit {
746 Edit::Create {
747 id: edit_id,
748 entity,
749 } if *edit_id == id => return Some(&entity.type_name),
750 Edit::Remove { id: edit_id } if *edit_id == id => return None,
751 Edit::Retype {
752 id: edit_id,
753 type_name,
754 } if *edit_id == id => return Some(type_name),
755 _ => {}
756 }
757 }
758 model.get(id).map(|entity| entity.type_name.as_ref())
759}
760
761pub(crate) fn text(value: &str) -> Value {
762 Value::Text(Arc::from(value))
763}
764pub(crate) fn optional_text(value: Option<&str>) -> Value {
765 value.map_or(Value::Null, text)
766}
767pub(crate) fn optional_ref(value: Option<EntityId>) -> Value {
768 value.map_or(Value::Null, Value::Ref)
769}
770fn enumeration(value: &str) -> Value {
771 Value::Enum(Arc::from(value))
772}
773pub(crate) fn refs(values: &[EntityId]) -> Value {
774 Value::List(values.iter().copied().map(Value::Ref).collect())
775}