ifc_control/authoring.rs
1//! Staging the four `IfcControl` subtypes this crate owns.
2//!
3//! # What a control is
4//!
5//! A control is a record that governs work rather than describing
6//! physical form: a permit that authorises it, an order that
7//! commissions it, a request that asks for it, a performance history
8//! that records how it behaved. They carry no geometry.
9//!
10//! # The shape they share
11//!
12//! All four are `IfcControl` subtypes, so slots 0-5 are fixed:
13//! the four `IfcRoot` slots, `ObjectType`, and `Identification`.
14//! Three of them then take `PredefinedType`, `Status` and
15//! `LongDescription`. `IfcPerformanceHistory` does not: it takes a
16//! required `LifeCyclePhase` at slot 6 and its predefined type at
17//! slot 7, so a writer that assumes the common tail would file the
18//! phase as a status.
19//!
20//! The positions above are IFC4's. IFC2X3 declares an `IfcControl`
21//! without `Identification`, and each control its own tail (#198):
22//!
23//! ```text
24//! IFC2X3_TC1 IfcPermit ... ObjectType, PermitID
25//! IFC2X3_TC1 IfcActionRequest ... ObjectType, RequestID
26//! IFC2X3_TC1 IfcProjectOrder ... ObjectType, ID, PredefinedType, Status
27//! IFC2X3_TC1 IfcPerformanceHistory ... ObjectType, LifeCyclePhase
28//! ```
29//!
30//! So records are laid out by attribute name from the release's own table
31//! (`release.rs`), never by these positions.
32//!
33//! # USERDEFINED
34//!
35//! These entities declare no WHERE rules. The `USERDEFINED` token
36//! still asserts that a name is given elsewhere, and `ObjectType` is
37//! where `IfcObject` puts it. Writing `USERDEFINED` without it
38//! produces a record that claims a specific kind and withholds which,
39//! so it is refused here even though the schema does not say so.
40//!
41//! # Work orders
42//!
43//! `IfcWorkOrder` does not exist in any IFC release: a work order is
44//! an `IfcProjectOrder` whose `PredefinedType` is `WORKORDER`. There is
45//! deliberately no separate entity or `ControlKind` for it.
46
47use ifc_model::guid::Guid;
48use ifc_model::{Entity, EntityId, Model, Transaction, Value};
49use ifc_schema::Schema;
50
51use crate::error::{ControlError, ControlResult};
52use crate::release::{bind, Release};
53
54/// `IfcPermitTypeEnum`.
55const PERMIT: &[&str] = &["ACCESS", "BUILDING", "WORK", "USERDEFINED", "NOTDEFINED"];
56
57/// `IfcProjectOrderTypeEnum`.
58const PROJECT_ORDER: &[&str] = &[
59 "CHANGEORDER",
60 "MAINTENANCEWORKORDER",
61 "MOVEORDER",
62 "PURCHASEORDER",
63 "WORKORDER",
64 "USERDEFINED",
65 "NOTDEFINED",
66];
67
68/// `IfcActionRequestTypeEnum`.
69const ACTION_REQUEST: &[&str] = &[
70 "EMAIL",
71 "FAX",
72 "PHONE",
73 "POST",
74 "VERBAL",
75 "USERDEFINED",
76 "NOTDEFINED",
77];
78
79/// `IfcPerformanceHistoryTypeEnum`.
80const PERFORMANCE_HISTORY: &[&str] = &["USERDEFINED", "NOTDEFINED"];
81
82/// Which control is being staged.
83///
84/// A single enum rather than four writers: the four differ only in
85/// type name, predefined-type enum, and whether they carry the
86/// `Status`/`LongDescription` tail. Four near-identical functions
87/// would drift apart on the next schema revision.
88#[derive(Debug, Clone, Copy, PartialEq, Eq)]
89#[non_exhaustive]
90pub enum ControlKind {
91 /// `IfcPermit`: authorisation to proceed.
92 Permit,
93 /// `IfcProjectOrder`: an instruction to carry work out.
94 ProjectOrder,
95 /// `IfcActionRequest`: a request that work be done.
96 ActionRequest,
97 /// `IfcPerformanceHistory`: recorded in-use behaviour.
98 PerformanceHistory,
99}
100
101impl ControlKind {
102 /// Every control this crate owns, in declaration order.
103 pub const ALL: [Self; 4] = [
104 Self::Permit,
105 Self::ProjectOrder,
106 Self::ActionRequest,
107 Self::PerformanceHistory,
108 ];
109
110 /// STEP type name, upper-case as the catalogue stores it.
111 ///
112 /// Upper-case because `Entity::new` does not normalise and the
113 /// model indexes by the stored string.
114 #[must_use]
115 pub const fn type_name(self) -> &'static str {
116 match self {
117 Self::Permit => "IFCPERMIT",
118 Self::ProjectOrder => "IFCPROJECTORDER",
119 Self::ActionRequest => "IFCACTIONREQUEST",
120 Self::PerformanceHistory => "IFCPERFORMANCEHISTORY",
121 }
122 }
123
124 /// The tokens this entity's own `PredefinedType` enum declares.
125 #[must_use]
126 pub const fn members(self) -> &'static [&'static str] {
127 match self {
128 Self::Permit => PERMIT,
129 Self::ProjectOrder => PROJECT_ORDER,
130 Self::ActionRequest => ACTION_REQUEST,
131 Self::PerformanceHistory => PERFORMANCE_HISTORY,
132 }
133 }
134}
135
136/// Attributes shared by every control.
137#[derive(Debug, Clone, Copy, Default)]
138#[non_exhaustive]
139pub struct ControlDraft<'a> {
140 /// `Name`.
141 pub name: Option<&'a str>,
142 /// `Description`.
143 pub description: Option<&'a str>,
144 /// `ObjectType`, slot 4. Required when the predefined type is
145 /// `USERDEFINED`.
146 pub object_type: Option<&'a str>,
147 /// `Identification`, slot 5: the permit or order number.
148 pub identification: Option<&'a str>,
149 /// `Status`, slot 7. Not declared by `IfcPerformanceHistory`.
150 pub status: Option<&'a str>,
151 /// `LongDescription`, slot 8. Not declared by
152 /// `IfcPerformanceHistory`.
153 pub long_description: Option<&'a str>,
154 /// `LifeCyclePhase`, slot 6. Required by
155 /// `IfcPerformanceHistory` and declared by no other control.
156 pub life_cycle_phase: Option<&'a str>,
157}
158
159impl<'a> ControlDraft<'a> {
160 /// Starts a draft with every field unset.
161 #[must_use]
162 pub fn new() -> Self {
163 Self {
164 name: None,
165 description: None,
166 object_type: None,
167 identification: None,
168 status: None,
169 long_description: None,
170 life_cycle_phase: None,
171 }
172 }
173
174 /// Sets [`Self::name`]: `Name`.
175 #[must_use]
176 pub fn name(mut self, value: &'a str) -> Self {
177 self.name = Some(value);
178 self
179 }
180
181 /// Sets [`Self::description`]: `Description`.
182 #[must_use]
183 pub fn description(mut self, value: &'a str) -> Self {
184 self.description = Some(value);
185 self
186 }
187
188 /// Sets [`Self::object_type`]: `ObjectType`, slot 4. Required when the predefined type is `USERDEFINED`.
189 #[must_use]
190 pub fn object_type(mut self, value: &'a str) -> Self {
191 self.object_type = Some(value);
192 self
193 }
194
195 /// Sets [`Self::identification`]: `Identification`, slot 5: the permit or order number.
196 #[must_use]
197 pub fn identification(mut self, value: &'a str) -> Self {
198 self.identification = Some(value);
199 self
200 }
201
202 /// Sets [`Self::status`]: `Status`, slot 7. Not declared by `IfcPerformanceHistory`.
203 #[must_use]
204 pub fn status(mut self, value: &'a str) -> Self {
205 self.status = Some(value);
206 self
207 }
208
209 /// Sets [`Self::long_description`]: `LongDescription`, slot 8. Not declared by `IfcPerformanceHistory`.
210 #[must_use]
211 pub fn long_description(mut self, value: &'a str) -> Self {
212 self.long_description = Some(value);
213 self
214 }
215
216 /// Sets [`Self::life_cycle_phase`]: `LifeCyclePhase`, slot 6. Required by `IfcPerformanceHistory` and declared by no other control.
217 #[must_use]
218 pub fn life_cycle_phase(mut self, value: &'a str) -> Self {
219 self.life_cycle_phase = Some(value);
220 self
221 }
222}
223
224fn invalid(
225 entity: &'static str,
226 attribute: &'static str,
227 value: impl Into<String>,
228) -> ControlError {
229 ControlError::AuthoringInvalid {
230 entity,
231 attribute,
232 value: value.into(),
233 }
234}
235
236fn text(value: Option<&str>) -> Value {
237 value.map_or(Value::Null, |v| Value::Text(v.into()))
238}
239
240fn blank(value: Option<&str>) -> bool {
241 value.is_none_or(|v| v.trim().is_empty())
242}
243
244/// Stage one control record, laid out by attribute name in `schema`.
245///
246/// `OwnerHistory` is left unset, which IFC4 and IFC4X3 allow. IFC2X3
247/// requires it, so an IFC2X3 `schema` is refused with
248/// [`ControlError::AuthoringRequired`] instead of written as `$`; use
249/// [`create_control_with_owner_history`] there. Before #198 an IFC2X3
250/// `IfcPermit`, `IfcActionRequest` or `IfcPerformanceHistory` panicked
251/// here, indexing past their six declared attributes.
252///
253/// # Errors
254///
255/// Refuses a malformed GlobalId; a blank name; a token outside the
256/// entity's own enum; `USERDEFINED` without `object_type`; a
257/// `life_cycle_phase` on an entity that does not declare it and a
258/// missing one on `IfcPerformanceHistory`; `status` or
259/// `long_description` on `IfcPerformanceHistory`; and an entity the
260/// schema does not declare. Against the release's own table: a value
261/// the entity does not declare there (`AuthoringNotInSchema`, such as an
262/// IFC2X3 `PredefinedType` on a permit), one it cannot hold
263/// (`AuthoringValueType`), and a required one left unset
264/// (`AuthoringRequired`, such as the IFC2X3 `OwnerHistory` or
265/// `PermitID`). Nothing is staged on an error.
266pub fn create_control(
267 tx: &mut Transaction,
268 schema: &Schema,
269 kind: ControlKind,
270 global_id: &str,
271 predefined_type: Option<&str>,
272 draft: ControlDraft<'_>,
273) -> ControlResult<EntityId> {
274 let release = Release::of_schema(schema);
275 let record = control_record(
276 release,
277 kind,
278 global_id,
279 predefined_type,
280 draft,
281 Value::Null,
282 )?;
283 Ok(tx.create(record))
284}
285
286/// [`create_control`] in `model`'s declared release, with a caller-supplied
287/// `IfcOwnerHistory`, which IFC2X3 requires (#198, #202).
288///
289/// The release is bound from `FILE_SCHEMA` (none binds IFC4). In IFC2X3
290/// `Identification` is written as the entity's own identifier
291/// (`PermitID`, `RequestID`, `ID`), which IFC2X3 requires; IFC2X3 declares
292/// no `PredefinedType` for a permit, an action request or a performance
293/// history, no `Status` for a permit or an action request, and no
294/// `LongDescription` at all. In IFC4 and IFC4X3 the record is that of
295/// [`create_control`] with the reference in the optional slot. The
296/// owner history is never invented: build it with `ifc-author`.
297///
298/// # Errors
299///
300/// Those of [`create_control`] except the IFC2X3 refusal, and:
301/// [`ControlError::MultipleSchemas`] or [`ControlError::UnsupportedSchema`]
302/// if the model binds no single known release;
303/// [`ControlError::UnknownEntity`] if `owner_history` is neither in the
304/// model nor staged; [`ControlError::AuthoringInvalid`] if it is not an
305/// `IfcOwnerHistory`. Nothing is staged on an error.
306pub fn create_control_with_owner_history(
307 tx: &mut Transaction,
308 model: &Model,
309 kind: ControlKind,
310 global_id: &str,
311 predefined_type: Option<&str>,
312 draft: ControlDraft<'_>,
313 owner_history: EntityId,
314) -> ControlResult<EntityId> {
315 let release = bind(model)?;
316 let record = control_record(
317 release,
318 kind,
319 global_id,
320 predefined_type,
321 draft,
322 Value::Ref(owner_history),
323 )?;
324 release.require_owner_history(tx, model, kind.type_name(), owner_history)?;
325 Ok(tx.create(record))
326}
327
328/// Validate a draft and lay its record out in `release`.
329fn control_record(
330 release: Release<'_>,
331 kind: ControlKind,
332 global_id: &str,
333 predefined_type: Option<&str>,
334 draft: ControlDraft<'_>,
335 owner_history: Value,
336) -> ControlResult<Entity> {
337 let entity = kind.type_name();
338 if release.schema().attributes(entity).is_empty() {
339 return Err(ControlError::UnsupportedEntity {
340 schema: release.schema().name().to_owned(),
341 entity,
342 });
343 }
344
345 if Guid::parse(global_id).is_none() {
346 return Err(invalid(entity, "GlobalId", global_id));
347 }
348 // `IfcRoot.Name` is optional in the slot table, but a control
349 // nobody can name is a record nobody can cite in correspondence.
350 if blank(draft.name) {
351 return Err(invalid(entity, "Name", "required"));
352 }
353
354 if let Some(token) = predefined_type {
355 if !kind.members().contains(&token) {
356 return Err(invalid(entity, "PredefinedType", token));
357 }
358 if token == "USERDEFINED" && blank(draft.object_type) {
359 return Err(invalid(entity, "ObjectType", "required by USERDEFINED"));
360 }
361 }
362
363 let history = kind == ControlKind::PerformanceHistory;
364 // Attributes the entity does not declare are refused, not
365 // dropped: silently discarding a Status writes a file missing
366 // data the caller believes they supplied.
367 if history {
368 if blank(draft.life_cycle_phase) {
369 return Err(invalid(entity, "LifeCyclePhase", "required"));
370 }
371 if draft.status.is_some() {
372 return Err(invalid(entity, "Status", "not declared"));
373 }
374 if draft.long_description.is_some() {
375 return Err(invalid(entity, "LongDescription", "not declared"));
376 }
377 } else if draft.life_cycle_phase.is_some() {
378 return Err(invalid(entity, "LifeCyclePhase", "not declared"));
379 }
380
381 // Placed by name from the release's own table: IFC2X3 declares six
382 // attributes for a permit, where IFC4 declares nine. Indexing by the
383 // IFC4 positions is what panicked (#198).
384 let mut values = vec![
385 ("GlobalId", Value::Text(global_id.into())),
386 ("OwnerHistory", owner_history),
387 ("Name", text(draft.name)),
388 ("Description", text(draft.description)),
389 ("ObjectType", text(draft.object_type)),
390 ("Identification", text(draft.identification)),
391 (
392 "PredefinedType",
393 predefined_type.map_or(Value::Null, |t| Value::Enum(t.into())),
394 ),
395 ];
396 if history {
397 values.push(("LifeCyclePhase", text(draft.life_cycle_phase)));
398 } else {
399 values.push(("Status", text(draft.status)));
400 values.push(("LongDescription", text(draft.long_description)));
401 }
402 release.record(entity, values)
403}