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//! # USERDEFINED
21//!
22//! These entities declare no WHERE rules. The `USERDEFINED` token
23//! still asserts that a name is given elsewhere, and `ObjectType` is
24//! where `IfcObject` puts it. Writing `USERDEFINED` without it
25//! produces a record that claims a specific kind and withholds which,
26//! so it is refused here even though the schema does not say so.
27
28use ifc_model::guid::Guid;
29use ifc_model::{Entity, EntityId, Transaction, Value};
30use ifc_schema::Schema;
31
32use crate::error::{ControlError, ControlResult};
33
34/// `IfcPermitTypeEnum`.
35const PERMIT: &[&str] = &["ACCESS", "BUILDING", "WORK", "USERDEFINED", "NOTDEFINED"];
36
37/// `IfcProjectOrderTypeEnum`.
38const PROJECT_ORDER: &[&str] = &[
39 "CHANGEORDER",
40 "MAINTENANCEWORKORDER",
41 "MOVEORDER",
42 "PURCHASEORDER",
43 "WORKORDER",
44 "USERDEFINED",
45 "NOTDEFINED",
46];
47
48/// `IfcActionRequestTypeEnum`.
49const ACTION_REQUEST: &[&str] = &[
50 "EMAIL",
51 "FAX",
52 "PHONE",
53 "POST",
54 "VERBAL",
55 "USERDEFINED",
56 "NOTDEFINED",
57];
58
59/// `IfcPerformanceHistoryTypeEnum`.
60const PERFORMANCE_HISTORY: &[&str] = &["USERDEFINED", "NOTDEFINED"];
61
62/// Which control is being staged.
63///
64/// A single enum rather than four writers: the four differ only in
65/// type name, predefined-type enum, and whether they carry the
66/// `Status`/`LongDescription` tail. Four near-identical functions
67/// would drift apart on the next schema revision.
68#[derive(Debug, Clone, Copy, PartialEq, Eq)]
69#[non_exhaustive]
70pub enum ControlKind {
71 /// `IfcPermit`: authorisation to proceed.
72 Permit,
73 /// `IfcProjectOrder`: an instruction to carry work out.
74 ProjectOrder,
75 /// `IfcActionRequest`: a request that work be done.
76 ActionRequest,
77 /// `IfcPerformanceHistory`: recorded in-use behaviour.
78 PerformanceHistory,
79}
80
81impl ControlKind {
82 /// STEP type name, upper-case as the catalogue stores it.
83 ///
84 /// Upper-case because `Entity::new` does not normalise and the
85 /// model indexes by the stored string.
86 #[must_use]
87 pub const fn type_name(self) -> &'static str {
88 match self {
89 Self::Permit => "IFCPERMIT",
90 Self::ProjectOrder => "IFCPROJECTORDER",
91 Self::ActionRequest => "IFCACTIONREQUEST",
92 Self::PerformanceHistory => "IFCPERFORMANCEHISTORY",
93 }
94 }
95
96 /// The tokens this entity's own `PredefinedType` enum declares.
97 #[must_use]
98 pub const fn members(self) -> &'static [&'static str] {
99 match self {
100 Self::Permit => PERMIT,
101 Self::ProjectOrder => PROJECT_ORDER,
102 Self::ActionRequest => ACTION_REQUEST,
103 Self::PerformanceHistory => PERFORMANCE_HISTORY,
104 }
105 }
106
107 /// Slot holding `PredefinedType`.
108 ///
109 /// `IfcPerformanceHistory` puts it at 7, after the required
110 /// `LifeCyclePhase`; the others at 6.
111 const fn predefined_slot(self) -> usize {
112 match self {
113 Self::PerformanceHistory => 7,
114 _ => 6,
115 }
116 }
117}
118
119/// Attributes shared by every control.
120#[derive(Debug, Clone, Copy, Default)]
121pub struct ControlDraft<'a> {
122 /// `Name`.
123 pub name: Option<&'a str>,
124 /// `Description`.
125 pub description: Option<&'a str>,
126 /// `ObjectType`, slot 4. Required when the predefined type is
127 /// `USERDEFINED`.
128 pub object_type: Option<&'a str>,
129 /// `Identification`, slot 5: the permit or order number.
130 pub identification: Option<&'a str>,
131 /// `Status`, slot 7. Not declared by `IfcPerformanceHistory`.
132 pub status: Option<&'a str>,
133 /// `LongDescription`, slot 8. Not declared by
134 /// `IfcPerformanceHistory`.
135 pub long_description: Option<&'a str>,
136 /// `LifeCyclePhase`, slot 6. Required by
137 /// `IfcPerformanceHistory` and declared by no other control.
138 pub life_cycle_phase: Option<&'a str>,
139}
140
141fn invalid(
142 entity: &'static str,
143 attribute: &'static str,
144 value: impl Into<String>,
145) -> ControlError {
146 ControlError::AuthoringInvalid {
147 entity,
148 attribute,
149 value: value.into(),
150 }
151}
152
153fn text(value: Option<&str>) -> Value {
154 value.map_or(Value::Null, |v| Value::Text(v.into()))
155}
156
157fn blank(value: Option<&str>) -> bool {
158 value.is_none_or(|v| v.trim().is_empty())
159}
160
161/// Stage one control record.
162///
163/// The arity is taken from `schema`, not hardcoded, so a schema that
164/// declares a different tail produces a correctly-sized record
165/// instead of a silently truncated one.
166///
167/// # Errors
168///
169/// Refuses a malformed GlobalId; a blank name; a token outside the
170/// entity's own enum; `USERDEFINED` without `object_type`; a
171/// `life_cycle_phase` on an entity that does not declare it and a
172/// missing one on `IfcPerformanceHistory`; `status` or
173/// `long_description` on `IfcPerformanceHistory`; and an entity the
174/// schema does not declare.
175pub fn create_control(
176 tx: &mut Transaction,
177 schema: &Schema,
178 kind: ControlKind,
179 global_id: &str,
180 predefined_type: Option<&str>,
181 draft: ControlDraft<'_>,
182) -> ControlResult<EntityId> {
183 let entity = kind.type_name();
184 let declared = schema.attributes(entity);
185 if declared.is_empty() {
186 return Err(ControlError::UnsupportedEntity {
187 schema: schema.name().to_owned(),
188 entity,
189 });
190 }
191
192 if Guid::parse(global_id).is_none() {
193 return Err(invalid(entity, "GlobalId", global_id));
194 }
195 // `IfcRoot.Name` is optional in the slot table, but a control
196 // nobody can name is a record nobody can cite in correspondence.
197 if blank(draft.name) {
198 return Err(invalid(entity, "Name", "required"));
199 }
200
201 if let Some(token) = predefined_type {
202 if !kind.members().contains(&token) {
203 return Err(invalid(entity, "PredefinedType", token));
204 }
205 if token == "USERDEFINED" && blank(draft.object_type) {
206 return Err(invalid(entity, "ObjectType", "required by USERDEFINED"));
207 }
208 }
209
210 let history = kind == ControlKind::PerformanceHistory;
211 // Attributes the entity does not declare are refused, not
212 // dropped: silently discarding a Status writes a file missing
213 // data the caller believes they supplied.
214 if history {
215 if blank(draft.life_cycle_phase) {
216 return Err(invalid(entity, "LifeCyclePhase", "required"));
217 }
218 if draft.status.is_some() {
219 return Err(invalid(entity, "Status", "not declared"));
220 }
221 if draft.long_description.is_some() {
222 return Err(invalid(entity, "LongDescription", "not declared"));
223 }
224 } else if draft.life_cycle_phase.is_some() {
225 return Err(invalid(entity, "LifeCyclePhase", "not declared"));
226 }
227
228 let mut attrs = vec![Value::Null; declared.len()];
229 attrs[0] = Value::Text(global_id.into());
230 attrs[2] = text(draft.name);
231 attrs[3] = text(draft.description);
232 attrs[4] = text(draft.object_type);
233 attrs[5] = text(draft.identification);
234 if history {
235 attrs[6] = text(draft.life_cycle_phase);
236 } else {
237 // Indexing is bounded by the schema's own count rather than the
238 // 9 slots IFC4 happens to declare: a panic here would turn a
239 // schema difference into a crash instead of a refusal.
240 let tail = [
241 (7, "Status", draft.status),
242 (8, "LongDescription", draft.long_description),
243 ];
244 for (slot, attribute, value) in tail {
245 if let Some(cell) = attrs.get_mut(slot) {
246 *cell = text(value);
247 } else if value.is_some() {
248 return Err(invalid(entity, attribute, "not declared"));
249 }
250 }
251 }
252 attrs[kind.predefined_slot()] = predefined_type.map_or(Value::Null, |t| Value::Enum(t.into()));
253
254 Ok(tx.create(Entity::new(entity, attrs)))
255}