Skip to main content

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}