ifc_control/assignment.rs
1//! Staging `IfcRelAssignsToControl` for the controls this crate owns.
2//!
3//! # Who writes the assignment
4//!
5//! The crate that owns the relating control writes the assignment.
6//! `ifc-cost` binds items to its cost schedules, `ifc-schedule` binds
7//! tasks to its work controls, and this module binds work to a permit,
8//! a project order, an action request or a performance history. A
9//! relating control of any other type is refused here, even though the
10//! schema admits every `IfcControl`: the owning crate knows which
11//! objects make sense under it, this one does not.
12//!
13//! `IfcPerformanceHistory` is included because it is an `IfcControl`
14//! subtype in IFC4 ADD2 TC1 and IFC4X3 ADD2, so `RelatingControl :
15//! IfcControl` admits it; the schema draws no distinction among the four.
16//!
17//! # What the schema requires
18//!
19//! `IfcRelAssigns.RelatedObjects` is `SET [1:?] OF IfcObjectDefinition`:
20//! at least one member, no duplicates, each an object definition.
21//! `IfcRelAssignsToControl.NoSelfReference` forbids the relating control
22//! among its own related objects. All four are refused before staging.
23//!
24//! # RelatedObjectsType
25//!
26//! Always left unset. IFC4 ADD2 TC1 declares it `OPTIONAL
27//! IfcObjectTypeEnum` under `WR1 : IfcCorrectObjectAssignment`, which
28//! returns TRUE when it is absent; IFC4X3 ADD2 redeclares it `OPTIONAL
29//! IfcStrippedOptional`, a BOOLEAN kept only so older files parse. Null
30//! is the one value valid in both, and naming the related objects' type
31//! would add a constraint the caller never asked for.
32
33use std::collections::HashSet;
34
35use ifc_model::guid::Guid;
36use ifc_model::{Entity, EntityId, Model, Transaction, Value};
37use ifc_schema::Schema;
38
39use crate::authoring::ControlKind;
40use crate::error::{ControlError, ControlResult};
41use crate::release::{bind, projected_type, Release};
42
43const RELATION: &str = "IFCRELASSIGNSTOCONTROL";
44
45/// One `IfcRelAssignsToControl` to stage.
46#[derive(Debug, Clone, Copy)]
47#[non_exhaustive]
48pub struct ControlAssignmentDraft<'a> {
49 /// `GlobalId`: a 22-character compressed IFC GUID.
50 pub global_id: &'a str,
51 /// `Name`.
52 pub name: Option<&'a str>,
53 /// `Description`.
54 pub description: Option<&'a str>,
55 /// `RelatingControl`: a permit, project order, action request or
56 /// performance history, in the model or staged earlier in the
57 /// transaction.
58 pub control: EntityId,
59 /// `RelatedObjects`: the work the control governs.
60 pub related_objects: &'a [EntityId],
61}
62
63impl<'a> ControlAssignmentDraft<'a> {
64 /// Starts a draft with its required fields; the rest are unset.
65 #[must_use]
66 pub fn new(global_id: &'a str, control: EntityId, related_objects: &'a [EntityId]) -> Self {
67 Self {
68 global_id,
69 name: None,
70 description: None,
71 control,
72 related_objects,
73 }
74 }
75
76 /// Sets [`Self::name`]: `Name`.
77 #[must_use]
78 pub fn name(mut self, value: &'a str) -> Self {
79 self.name = Some(value);
80 self
81 }
82
83 /// Sets [`Self::description`]: `Description`.
84 #[must_use]
85 pub fn description(mut self, value: &'a str) -> Self {
86 self.description = Some(value);
87 self
88 }
89}
90
91/// Stage one `IfcRelAssignsToControl` relating work to an owned control.
92///
93/// References may target the model or entities staged earlier in `tx`.
94/// The record is laid out by attribute name in `schema`, so a schema that
95/// moves a slot produces a correct record rather than a shifted one.
96/// `OwnerHistory` is left unset, which IFC4 and IFC4X3 allow; an IFC2X3
97/// `schema` requires it and is refused with
98/// [`ControlError::AuthoringRequired`] (use
99/// [`assign_to_control_with_owner_history`]).
100///
101/// # Errors
102///
103/// Refuses a malformed GlobalId; a relating control or related object
104/// that exists neither in `model` nor in `tx`; a relating control that
105/// is not one of the four [`ControlKind`]s; an empty or duplicated
106/// `RelatedObjects`; the control among its own related objects; a
107/// related object that is not an `IfcObjectDefinition`; a schema that
108/// does not declare the relationship; and an IFC2X3 schema. Nothing is
109/// staged on an error.
110pub fn assign_to_control(
111 tx: &mut Transaction,
112 model: &Model,
113 schema: &Schema,
114 draft: ControlAssignmentDraft<'_>,
115) -> ControlResult<EntityId> {
116 let record = assignment_record(tx, model, Release::of_schema(schema), draft, Value::Null)?;
117 Ok(tx.create(record))
118}
119
120/// [`assign_to_control`] in `model`'s declared release, with a
121/// caller-supplied `IfcOwnerHistory`, which IFC2X3 requires (#202).
122///
123/// The release is bound from `FILE_SCHEMA` (none binds IFC4); in IFC4 and
124/// IFC4X3 the record is that of [`assign_to_control`] with the reference in
125/// the optional slot. The owner history is never invented.
126///
127/// # Errors
128///
129/// Those of [`assign_to_control`] except the IFC2X3 refusal, and the
130/// release and owner-history refusals of
131/// [`create_control_with_owner_history`](crate::create_control_with_owner_history).
132/// Nothing is staged on an error.
133pub fn assign_to_control_with_owner_history(
134 tx: &mut Transaction,
135 model: &Model,
136 draft: ControlAssignmentDraft<'_>,
137 owner_history: EntityId,
138) -> ControlResult<EntityId> {
139 let release = bind(model)?;
140 let record = assignment_record(tx, model, release, draft, Value::Ref(owner_history))?;
141 release.require_owner_history(tx, model, RELATION, owner_history)?;
142 Ok(tx.create(record))
143}
144
145fn assignment_record(
146 tx: &Transaction,
147 model: &Model,
148 release: Release<'_>,
149 draft: ControlAssignmentDraft<'_>,
150 owner_history: Value,
151) -> ControlResult<Entity> {
152 let schema = release.schema();
153 if schema.attributes(RELATION).is_empty() {
154 return Err(ControlError::UnsupportedEntity {
155 schema: schema.name().to_owned(),
156 entity: RELATION,
157 });
158 }
159
160 if Guid::parse(draft.global_id).is_none() {
161 return Err(invalid("GlobalId", draft.global_id));
162 }
163
164 let control_type = projected_type(tx, model, draft.control)
165 .ok_or(ControlError::UnknownEntity { id: draft.control })?;
166 if !ControlKind::ALL
167 .iter()
168 .any(|kind| control_type.eq_ignore_ascii_case(kind.type_name()))
169 {
170 return Err(ControlError::ForeignControl {
171 id: draft.control,
172 actual: control_type,
173 });
174 }
175
176 if draft.related_objects.is_empty() {
177 return Err(invalid("RelatedObjects", "expected at least one member"));
178 }
179 let mut seen = HashSet::new();
180 for &object in draft.related_objects {
181 if !seen.insert(object) {
182 return Err(invalid(
183 "RelatedObjects",
184 format!("{object} is listed twice"),
185 ));
186 }
187 if object == draft.control {
188 // `NoSelfReference`.
189 return Err(invalid(
190 "RelatedObjects",
191 format!("{object} is the relating control"),
192 ));
193 }
194 let actual =
195 projected_type(tx, model, object).ok_or(ControlError::UnknownEntity { id: object })?;
196 if !schema.is_a(&actual, "IFCOBJECTDEFINITION") {
197 return Err(invalid(
198 "RelatedObjects",
199 format!("{object} is {actual}, not an IfcObjectDefinition"),
200 ));
201 }
202 }
203
204 release.record(
205 RELATION,
206 vec![
207 ("GlobalId", Value::Text(draft.global_id.into())),
208 ("OwnerHistory", owner_history),
209 ("Name", text(draft.name)),
210 ("Description", text(draft.description)),
211 (
212 "RelatedObjects",
213 Value::List(
214 draft
215 .related_objects
216 .iter()
217 .copied()
218 .map(Value::Ref)
219 .collect(),
220 ),
221 ),
222 // RelatedObjectsType: always unset, see the module docs.
223 ("RelatedObjectsType", Value::Null),
224 ("RelatingControl", Value::Ref(draft.control)),
225 ],
226 )
227}
228
229fn invalid(attribute: &'static str, value: impl Into<String>) -> ControlError {
230 ControlError::AuthoringInvalid {
231 entity: RELATION,
232 attribute,
233 value: value.into(),
234 }
235}
236
237fn text(value: Option<&str>) -> Value {
238 value.map_or(Value::Null, |v| Value::Text(v.into()))
239}