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)]
47pub struct ControlAssignmentDraft<'a> {
48 /// `GlobalId`: a 22-character compressed IFC GUID.
49 pub global_id: &'a str,
50 /// `Name`.
51 pub name: Option<&'a str>,
52 /// `Description`.
53 pub description: Option<&'a str>,
54 /// `RelatingControl`: a permit, project order, action request or
55 /// performance history, in the model or staged earlier in the
56 /// transaction.
57 pub control: EntityId,
58 /// `RelatedObjects`: the work the control governs.
59 pub related_objects: &'a [EntityId],
60}
61
62/// Stage one `IfcRelAssignsToControl` relating work to an owned control.
63///
64/// References may target the model or entities staged earlier in `tx`.
65/// The record is laid out by attribute name in `schema`, so a schema that
66/// moves a slot produces a correct record rather than a shifted one.
67/// `OwnerHistory` is left unset, which IFC4 and IFC4X3 allow; an IFC2X3
68/// `schema` requires it and is refused with
69/// [`ControlError::AuthoringRequired`] (use
70/// [`assign_to_control_with_owner_history`]).
71///
72/// # Errors
73///
74/// Refuses a malformed GlobalId; a relating control or related object
75/// that exists neither in `model` nor in `tx`; a relating control that
76/// is not one of the four [`ControlKind`]s; an empty or duplicated
77/// `RelatedObjects`; the control among its own related objects; a
78/// related object that is not an `IfcObjectDefinition`; a schema that
79/// does not declare the relationship; and an IFC2X3 schema. Nothing is
80/// staged on an error.
81pub fn assign_to_control(
82 tx: &mut Transaction,
83 model: &Model,
84 schema: &Schema,
85 draft: ControlAssignmentDraft<'_>,
86) -> ControlResult<EntityId> {
87 let record = assignment_record(tx, model, Release::of_schema(schema), draft, Value::Null)?;
88 Ok(tx.create(record))
89}
90
91/// [`assign_to_control`] in `model`'s declared release, with a
92/// caller-supplied `IfcOwnerHistory`, which IFC2X3 requires (#202).
93///
94/// The release is bound from `FILE_SCHEMA` (none binds IFC4); in IFC4 and
95/// IFC4X3 the record is that of [`assign_to_control`] with the reference in
96/// the optional slot. The owner history is never invented.
97///
98/// # Errors
99///
100/// Those of [`assign_to_control`] except the IFC2X3 refusal, and the
101/// release and owner-history refusals of
102/// [`create_control_with_owner_history`](crate::create_control_with_owner_history).
103/// Nothing is staged on an error.
104pub fn assign_to_control_with_owner_history(
105 tx: &mut Transaction,
106 model: &Model,
107 draft: ControlAssignmentDraft<'_>,
108 owner_history: EntityId,
109) -> ControlResult<EntityId> {
110 let release = bind(model)?;
111 let record = assignment_record(tx, model, release, draft, Value::Ref(owner_history))?;
112 release.require_owner_history(tx, model, RELATION, owner_history)?;
113 Ok(tx.create(record))
114}
115
116fn assignment_record(
117 tx: &Transaction,
118 model: &Model,
119 release: Release<'_>,
120 draft: ControlAssignmentDraft<'_>,
121 owner_history: Value,
122) -> ControlResult<Entity> {
123 let schema = release.schema();
124 if schema.attributes(RELATION).is_empty() {
125 return Err(ControlError::UnsupportedEntity {
126 schema: schema.name().to_owned(),
127 entity: RELATION,
128 });
129 }
130
131 if Guid::parse(draft.global_id).is_none() {
132 return Err(invalid("GlobalId", draft.global_id));
133 }
134
135 let control_type = projected_type(tx, model, draft.control)
136 .ok_or(ControlError::UnknownEntity { id: draft.control })?;
137 if !ControlKind::ALL
138 .iter()
139 .any(|kind| control_type.eq_ignore_ascii_case(kind.type_name()))
140 {
141 return Err(ControlError::ForeignControl {
142 id: draft.control,
143 actual: control_type,
144 });
145 }
146
147 if draft.related_objects.is_empty() {
148 return Err(invalid("RelatedObjects", "expected at least one member"));
149 }
150 let mut seen = HashSet::new();
151 for &object in draft.related_objects {
152 if !seen.insert(object) {
153 return Err(invalid(
154 "RelatedObjects",
155 format!("{object} is listed twice"),
156 ));
157 }
158 if object == draft.control {
159 // `NoSelfReference`.
160 return Err(invalid(
161 "RelatedObjects",
162 format!("{object} is the relating control"),
163 ));
164 }
165 let actual =
166 projected_type(tx, model, object).ok_or(ControlError::UnknownEntity { id: object })?;
167 if !schema.is_a(&actual, "IFCOBJECTDEFINITION") {
168 return Err(invalid(
169 "RelatedObjects",
170 format!("{object} is {actual}, not an IfcObjectDefinition"),
171 ));
172 }
173 }
174
175 release.record(
176 RELATION,
177 vec![
178 ("GlobalId", Value::Text(draft.global_id.into())),
179 ("OwnerHistory", owner_history),
180 ("Name", text(draft.name)),
181 ("Description", text(draft.description)),
182 (
183 "RelatedObjects",
184 Value::List(
185 draft
186 .related_objects
187 .iter()
188 .copied()
189 .map(Value::Ref)
190 .collect(),
191 ),
192 ),
193 // RelatedObjectsType: always unset, see the module docs.
194 ("RelatedObjectsType", Value::Null),
195 ("RelatingControl", Value::Ref(draft.control)),
196 ],
197 )
198}
199
200fn invalid(attribute: &'static str, value: impl Into<String>) -> ControlError {
201 ControlError::AuthoringInvalid {
202 entity: RELATION,
203 attribute,
204 value: value.into(),
205 }
206}
207
208fn text(value: Option<&str>) -> Value {
209 value.map_or(Value::Null, |v| Value::Text(v.into()))
210}