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//! 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 kind whose STEP type name is `type_name`, compared without
125    /// regard to case; `None` for any other entity, including the
126    /// `IfcControl` subtypes owned by `ifc-cost` and `ifc-schedule`.
127    #[must_use]
128    pub fn from_type_name(type_name: &str) -> Option<Self> {
129        Self::ALL
130            .into_iter()
131            .find(|kind| kind.type_name().eq_ignore_ascii_case(type_name))
132    }
133
134    /// The tokens this entity's own `PredefinedType` enum declares.
135    #[must_use]
136    pub const fn members(self) -> &'static [&'static str] {
137        match self {
138            Self::Permit => PERMIT,
139            Self::ProjectOrder => PROJECT_ORDER,
140            Self::ActionRequest => ACTION_REQUEST,
141            Self::PerformanceHistory => PERFORMANCE_HISTORY,
142        }
143    }
144}
145
146/// Attributes shared by every control.
147#[derive(Debug, Clone, Copy, Default)]
148#[non_exhaustive]
149pub struct ControlDraft<'a> {
150    /// `Name`.
151    pub name: Option<&'a str>,
152    /// `Description`.
153    pub description: Option<&'a str>,
154    /// `ObjectType`, slot 4. Required when the predefined type is
155    /// `USERDEFINED`.
156    pub object_type: Option<&'a str>,
157    /// `Identification`, slot 5: the permit or order number.
158    pub identification: Option<&'a str>,
159    /// `Status`, slot 7. Not declared by `IfcPerformanceHistory`.
160    pub status: Option<&'a str>,
161    /// `LongDescription`, slot 8. Not declared by
162    /// `IfcPerformanceHistory`.
163    pub long_description: Option<&'a str>,
164    /// `LifeCyclePhase`, slot 6. Required by
165    /// `IfcPerformanceHistory` and declared by no other control.
166    pub life_cycle_phase: Option<&'a str>,
167}
168
169impl<'a> ControlDraft<'a> {
170    /// Starts a draft with every field unset.
171    #[must_use]
172    pub fn new() -> Self {
173        Self {
174            name: None,
175            description: None,
176            object_type: None,
177            identification: None,
178            status: None,
179            long_description: None,
180            life_cycle_phase: None,
181        }
182    }
183
184    /// Sets [`Self::name`]: `Name`.
185    #[must_use]
186    pub fn name(mut self, value: &'a str) -> Self {
187        self.name = Some(value);
188        self
189    }
190
191    /// Sets [`Self::description`]: `Description`.
192    #[must_use]
193    pub fn description(mut self, value: &'a str) -> Self {
194        self.description = Some(value);
195        self
196    }
197
198    /// Sets [`Self::object_type`]: `ObjectType`, slot 4. Required when the predefined type is `USERDEFINED`.
199    #[must_use]
200    pub fn object_type(mut self, value: &'a str) -> Self {
201        self.object_type = Some(value);
202        self
203    }
204
205    /// Sets [`Self::identification`]: `Identification`, slot 5: the permit or order number.
206    #[must_use]
207    pub fn identification(mut self, value: &'a str) -> Self {
208        self.identification = Some(value);
209        self
210    }
211
212    /// Sets [`Self::status`]: `Status`, slot 7. Not declared by `IfcPerformanceHistory`.
213    #[must_use]
214    pub fn status(mut self, value: &'a str) -> Self {
215        self.status = Some(value);
216        self
217    }
218
219    /// Sets [`Self::long_description`]: `LongDescription`, slot 8. Not declared by `IfcPerformanceHistory`.
220    #[must_use]
221    pub fn long_description(mut self, value: &'a str) -> Self {
222        self.long_description = Some(value);
223        self
224    }
225
226    /// Sets [`Self::life_cycle_phase`]: `LifeCyclePhase`, slot 6. Required by `IfcPerformanceHistory` and declared by no other control.
227    #[must_use]
228    pub fn life_cycle_phase(mut self, value: &'a str) -> Self {
229        self.life_cycle_phase = Some(value);
230        self
231    }
232}
233
234fn invalid(
235    entity: &'static str,
236    attribute: &'static str,
237    value: impl Into<String>,
238) -> ControlError {
239    ControlError::AuthoringInvalid {
240        entity,
241        attribute,
242        value: value.into(),
243    }
244}
245
246fn text(value: Option<&str>) -> Value {
247    value.map_or(Value::Null, |v| Value::Text(v.into()))
248}
249
250fn blank(value: Option<&str>) -> bool {
251    value.is_none_or(|v| v.trim().is_empty())
252}
253
254/// Stage one control record, laid out by attribute name in `schema`.
255///
256/// `OwnerHistory` is left unset, which IFC4 and IFC4X3 allow. IFC2X3
257/// requires it, so an IFC2X3 `schema` is refused with
258/// [`ControlError::AuthoringRequired`] instead of written as `$`; use
259/// [`create_control_with_owner_history`] there. Before #198 an IFC2X3
260/// `IfcPermit`, `IfcActionRequest` or `IfcPerformanceHistory` panicked
261/// here, indexing past their six declared attributes.
262///
263/// # Errors
264///
265/// Refuses a malformed GlobalId; a blank name; a token outside the
266/// entity's own enum; `USERDEFINED` without `object_type`; a
267/// `life_cycle_phase` on an entity that does not declare it and a
268/// missing one on `IfcPerformanceHistory`; `status` or
269/// `long_description` on `IfcPerformanceHistory`; and an entity the
270/// schema does not declare. Against the release's own table: a value
271/// the entity does not declare there (`AuthoringNotInSchema`, such as an
272/// IFC2X3 `PredefinedType` on a permit), one it cannot hold
273/// (`AuthoringValueType`), and a required one left unset
274/// (`AuthoringRequired`, such as the IFC2X3 `OwnerHistory` or
275/// `PermitID`). Nothing is staged on an error.
276pub fn create_control(
277    tx: &mut Transaction,
278    schema: &Schema,
279    kind: ControlKind,
280    global_id: &str,
281    predefined_type: Option<&str>,
282    draft: ControlDraft<'_>,
283) -> ControlResult<EntityId> {
284    let release = Release::of_schema(schema);
285    let record = control_record(
286        release,
287        kind,
288        global_id,
289        predefined_type,
290        draft,
291        Value::Null,
292    )?;
293    Ok(tx.create(record))
294}
295
296/// [`create_control`] in `model`'s declared release, with a caller-supplied
297/// `IfcOwnerHistory`, which IFC2X3 requires (#198, #202).
298///
299/// The release is bound from `FILE_SCHEMA` (none binds IFC4). In IFC2X3
300/// `Identification` is written as the entity's own identifier
301/// (`PermitID`, `RequestID`, `ID`), which IFC2X3 requires; IFC2X3 declares
302/// no `PredefinedType` for a permit, an action request or a performance
303/// history, no `Status` for a permit or an action request, and no
304/// `LongDescription` at all. In IFC4 and IFC4X3 the record is that of
305/// [`create_control`] with the reference in the optional slot. The
306/// owner history is never invented: build it with `ifc-author`.
307///
308/// # Errors
309///
310/// Those of [`create_control`] except the IFC2X3 refusal, and:
311/// [`ControlError::MultipleSchemas`] or [`ControlError::UnsupportedSchema`]
312/// if the model binds no single known release;
313/// [`ControlError::UnknownEntity`] if `owner_history` is neither in the
314/// model nor staged; [`ControlError::AuthoringInvalid`] if it is not an
315/// `IfcOwnerHistory`. Nothing is staged on an error.
316pub fn create_control_with_owner_history(
317    tx: &mut Transaction,
318    model: &Model,
319    kind: ControlKind,
320    global_id: &str,
321    predefined_type: Option<&str>,
322    draft: ControlDraft<'_>,
323    owner_history: EntityId,
324) -> ControlResult<EntityId> {
325    let release = bind(model)?;
326    let record = control_record(
327        release,
328        kind,
329        global_id,
330        predefined_type,
331        draft,
332        Value::Ref(owner_history),
333    )?;
334    release.require_owner_history(tx, model, kind.type_name(), owner_history)?;
335    Ok(tx.create(record))
336}
337
338/// Validate a draft and lay its record out in `release`.
339fn control_record(
340    release: Release<'_>,
341    kind: ControlKind,
342    global_id: &str,
343    predefined_type: Option<&str>,
344    draft: ControlDraft<'_>,
345    owner_history: Value,
346) -> ControlResult<Entity> {
347    let entity = kind.type_name();
348    if release.schema().attributes(entity).is_empty() {
349        return Err(ControlError::UnsupportedEntity {
350            schema: release.schema().name().to_owned(),
351            entity,
352        });
353    }
354
355    if Guid::parse(global_id).is_none() {
356        return Err(invalid(entity, "GlobalId", global_id));
357    }
358    // `IfcRoot.Name` is optional in the slot table, but a control
359    // nobody can name is a record nobody can cite in correspondence.
360    if blank(draft.name) {
361        return Err(invalid(entity, "Name", "required"));
362    }
363
364    if let Some(token) = predefined_type {
365        if !kind.members().contains(&token) {
366            return Err(invalid(entity, "PredefinedType", token));
367        }
368        if token == "USERDEFINED" && blank(draft.object_type) {
369            return Err(invalid(entity, "ObjectType", "required by USERDEFINED"));
370        }
371    }
372
373    let history = kind == ControlKind::PerformanceHistory;
374    // Attributes the entity does not declare are refused, not
375    // dropped: silently discarding a Status writes a file missing
376    // data the caller believes they supplied.
377    if history {
378        if blank(draft.life_cycle_phase) {
379            return Err(invalid(entity, "LifeCyclePhase", "required"));
380        }
381        if draft.status.is_some() {
382            return Err(invalid(entity, "Status", "not declared"));
383        }
384        if draft.long_description.is_some() {
385            return Err(invalid(entity, "LongDescription", "not declared"));
386        }
387    } else if draft.life_cycle_phase.is_some() {
388        return Err(invalid(entity, "LifeCyclePhase", "not declared"));
389    }
390
391    // Placed by name from the release's own table: IFC2X3 declares six
392    // attributes for a permit, where IFC4 declares nine. Indexing by the
393    // IFC4 positions is what panicked (#198).
394    let mut values = vec![
395        ("GlobalId", Value::Text(global_id.into())),
396        ("OwnerHistory", owner_history),
397        ("Name", text(draft.name)),
398        ("Description", text(draft.description)),
399        ("ObjectType", text(draft.object_type)),
400        ("Identification", text(draft.identification)),
401        (
402            "PredefinedType",
403            predefined_type.map_or(Value::Null, |t| Value::Enum(t.into())),
404        ),
405    ];
406    if history {
407        values.push(("LifeCyclePhase", text(draft.life_cycle_phase)));
408    } else {
409        values.push(("Status", text(draft.status)));
410        values.push(("LongDescription", text(draft.long_description)));
411    }
412    release.record(entity, values)
413}