Skip to main content

ifc_author/
owner.rs

1//! Authoring for `IfcOwnerHistory` and the actor records it points at.
2//!
3//! Every `IfcRoot` subtype carries `OwnerHistory` in slot 1. Domain crates in
4//! this workspace currently write `Value::Null` there, which is schema-legal in
5//! IFC4 but discards provenance: who authored a record, with which
6//! application, and when. This module is the shared write side so each domain
7//! crate can stop inventing its own.
8//!
9//! Ownership is deliberately NOT auto-attached. A caller that wants provenance
10//! states it; one that does not keeps the existing null. Silently stamping an
11//! invented actor onto every authored entity would be worse than an honest
12//! omission, because downstream readers cannot tell a real author from a
13//! placeholder.
14
15use ifc_model::{Entity, EntityId, Transaction, Value};
16
17use crate::{AuthorError, AuthorResult};
18
19/// Authored fields for `IfcPerson`.
20#[derive(Debug, Clone, Copy, Default)]
21#[non_exhaustive]
22pub struct PersonDraft<'a> {
23    /// `IfcPerson.Identification`.
24    pub identification: Option<&'a str>,
25    /// `IfcPerson.FamilyName`.
26    pub family_name: Option<&'a str>,
27    /// `IfcPerson.GivenName`.
28    pub given_name: Option<&'a str>,
29}
30
31impl<'a> PersonDraft<'a> {
32    /// Starts a draft with every field unset.
33    #[must_use]
34    pub fn new() -> Self {
35        Self {
36            identification: None,
37            family_name: None,
38            given_name: None,
39        }
40    }
41
42    /// Sets [`Self::identification`]: `IfcPerson.Identification`.
43    #[must_use]
44    pub fn identification(mut self, value: &'a str) -> Self {
45        self.identification = Some(value);
46        self
47    }
48
49    /// Sets [`Self::family_name`]: `IfcPerson.FamilyName`.
50    #[must_use]
51    pub fn family_name(mut self, value: &'a str) -> Self {
52        self.family_name = Some(value);
53        self
54    }
55
56    /// Sets [`Self::given_name`]: `IfcPerson.GivenName`.
57    #[must_use]
58    pub fn given_name(mut self, value: &'a str) -> Self {
59        self.given_name = Some(value);
60        self
61    }
62}
63
64/// Authored fields for `IfcOrganization`.
65#[derive(Debug, Clone, Copy, Default)]
66#[non_exhaustive]
67pub struct OrganizationDraft<'a> {
68    /// `IfcOrganization.Identification`.
69    pub identification: Option<&'a str>,
70    /// `IfcOrganization.Name`. Required by the schema.
71    pub name: &'a str,
72    /// `IfcOrganization.Description`.
73    pub description: Option<&'a str>,
74}
75
76impl<'a> OrganizationDraft<'a> {
77    /// Starts a draft with its required fields; the rest are unset.
78    #[must_use]
79    pub fn new(name: &'a str) -> Self {
80        Self {
81            identification: None,
82            name,
83            description: None,
84        }
85    }
86
87    /// Sets [`Self::identification`]: `IfcOrganization.Identification`.
88    #[must_use]
89    pub fn identification(mut self, value: &'a str) -> Self {
90        self.identification = Some(value);
91        self
92    }
93
94    /// Sets [`Self::description`]: `IfcOrganization.Description`.
95    #[must_use]
96    pub fn description(mut self, value: &'a str) -> Self {
97        self.description = Some(value);
98        self
99    }
100}
101
102/// Authored fields for `IfcApplication`.
103#[derive(Debug, Clone, Copy)]
104#[non_exhaustive]
105pub struct ApplicationDraft<'a> {
106    /// `IfcApplication.ApplicationDeveloper`, an `IfcOrganization`.
107    pub developer: EntityId,
108    /// `IfcApplication.Version`.
109    pub version: &'a str,
110    /// `IfcApplication.ApplicationFullName`.
111    pub full_name: &'a str,
112    /// `IfcApplication.ApplicationIdentifier`.
113    pub identifier: &'a str,
114}
115
116impl<'a> ApplicationDraft<'a> {
117    /// Starts a draft with its required fields; the rest are unset.
118    #[must_use]
119    pub fn new(
120        developer: EntityId,
121        version: &'a str,
122        full_name: &'a str,
123        identifier: &'a str,
124    ) -> Self {
125        Self {
126            developer,
127            version,
128            full_name,
129            identifier,
130        }
131    }
132}
133
134/// Stage an `IfcPerson`.
135///
136/// IFC4 requires that a person carry at least one of Identification,
137/// FamilyName or GivenName -- an entirely empty person identifies nobody and
138/// defeats the purpose of recording ownership.
139pub fn add_person(tx: &mut Transaction, draft: PersonDraft<'_>) -> AuthorResult<EntityId> {
140    if draft.identification.is_none() && draft.family_name.is_none() && draft.given_name.is_none() {
141        return Err(AuthorError::MissingRequired {
142            entity: "IFCPERSON".to_owned(),
143            attribute: "Identification|FamilyName|GivenName".to_owned(),
144        });
145    }
146    Ok(tx.create(Entity::new(
147        "IFCPERSON",
148        vec![
149            opt_text(draft.identification),
150            opt_text(draft.family_name),
151            opt_text(draft.given_name),
152            Value::Null,
153            Value::Null,
154            Value::Null,
155            Value::Null,
156            Value::Null,
157        ],
158    )))
159}
160
161/// Stage an `IfcOrganization`. `Name` is required by the schema.
162pub fn add_organization(
163    tx: &mut Transaction,
164    draft: OrganizationDraft<'_>,
165) -> AuthorResult<EntityId> {
166    if draft.name.trim().is_empty() {
167        return Err(AuthorError::MissingRequired {
168            entity: "IFCORGANIZATION".to_owned(),
169            attribute: "Name".to_owned(),
170        });
171    }
172    Ok(tx.create(Entity::new(
173        "IFCORGANIZATION",
174        vec![
175            opt_text(draft.identification),
176            Value::Text(draft.name.into()),
177            opt_text(draft.description),
178            Value::Null,
179            Value::Null,
180        ],
181    )))
182}
183
184/// Stage an `IfcPersonAndOrganization`, the `IfcActorSelect` used by
185/// `IfcOwnerHistory.OwningUser`.
186pub fn add_person_and_organization(
187    tx: &mut Transaction,
188    person: EntityId,
189    organization: EntityId,
190) -> EntityId {
191    tx.create(Entity::new(
192        "IFCPERSONANDORGANIZATION",
193        vec![Value::Ref(person), Value::Ref(organization), Value::Null],
194    ))
195}
196
197/// Stage an `IfcApplication`, the authoring tool recorded in ownership.
198pub fn add_application(
199    tx: &mut Transaction,
200    draft: ApplicationDraft<'_>,
201) -> AuthorResult<EntityId> {
202    for (attribute, value) in [
203        ("Version", draft.version),
204        ("ApplicationFullName", draft.full_name),
205        ("ApplicationIdentifier", draft.identifier),
206    ] {
207        if value.trim().is_empty() {
208            return Err(AuthorError::MissingRequired {
209                entity: "IFCAPPLICATION".to_owned(),
210                attribute: attribute.to_owned(),
211            });
212        }
213    }
214    Ok(tx.create(Entity::new(
215        "IFCAPPLICATION",
216        vec![
217            Value::Ref(draft.developer),
218            Value::Text(draft.version.into()),
219            Value::Text(draft.full_name.into()),
220            Value::Text(draft.identifier.into()),
221        ],
222    )))
223}
224
225/// Authored fields for `IfcOwnerHistory`.
226#[derive(Debug, Clone, Copy)]
227#[non_exhaustive]
228pub struct OwnerHistoryDraft<'a> {
229    /// `IfcOwnerHistory.OwningUser`, an `IfcPersonAndOrganization`.
230    pub owning_user: EntityId,
231    /// `IfcOwnerHistory.OwningApplication`, an `IfcApplication`.
232    pub owning_application: EntityId,
233    /// `IfcOwnerHistory.ChangeAction`, an `IfcChangeActionEnum` constant.
234    pub change_action: Option<&'a str>,
235    /// `IfcOwnerHistory.CreationDate`, an IFC timestamp (seconds since the
236    /// 1970 epoch).
237    pub creation_date: i64,
238    /// `IfcOwnerHistory.LastModifiedDate`, if the record was edited.
239    pub last_modified_date: Option<i64>,
240}
241
242impl<'a> OwnerHistoryDraft<'a> {
243    /// Starts a draft with its required fields; the rest are unset.
244    #[must_use]
245    pub fn new(owning_user: EntityId, owning_application: EntityId, creation_date: i64) -> Self {
246        Self {
247            owning_user,
248            owning_application,
249            change_action: None,
250            creation_date,
251            last_modified_date: None,
252        }
253    }
254
255    /// Sets [`Self::change_action`]: `IfcOwnerHistory.ChangeAction`, an `IfcChangeActionEnum` constant.
256    #[must_use]
257    pub fn change_action(mut self, value: &'a str) -> Self {
258        self.change_action = Some(value);
259        self
260    }
261
262    /// Sets [`Self::last_modified_date`]: `IfcOwnerHistory.LastModifiedDate`, if the record was edited.
263    #[must_use]
264    pub fn last_modified_date(mut self, value: i64) -> Self {
265        self.last_modified_date = Some(value);
266        self
267    }
268}
269
270/// Valid `IfcChangeActionEnum` constants in IFC4 ADD2 TC1.
271const CHANGE_ACTIONS: &[&str] = &["NOCHANGE", "MODIFIED", "ADDED", "DELETED", "NOTDEFINED"];
272
273/// Stage an `IfcOwnerHistory`.
274///
275/// Two invariants are enforced here rather than left to a validator. A
276/// `ChangeAction` outside `IfcChangeActionEnum` would serialize as an
277/// unparseable enumeration token. And a `LastModifiedDate` earlier than
278/// `CreationDate` describes a record edited before it existed: it parses,
279/// validates, and quietly corrupts any audit trail built on it.
280pub fn add_owner_history(
281    tx: &mut Transaction,
282    draft: OwnerHistoryDraft<'_>,
283) -> AuthorResult<EntityId> {
284    if let Some(action) = draft.change_action {
285        if !CHANGE_ACTIONS
286            .iter()
287            .any(|known| known.eq_ignore_ascii_case(action))
288        {
289            return Err(AuthorError::TypeMismatch {
290                entity: "IFCOWNERHISTORY".to_owned(),
291                attribute: "ChangeAction".to_owned(),
292                expected: "an IfcChangeActionEnum constant".to_owned(),
293                found: action.to_owned(),
294            });
295        }
296    }
297    if let Some(modified) = draft.last_modified_date {
298        if modified < draft.creation_date {
299            return Err(AuthorError::TypeMismatch {
300                entity: "IFCOWNERHISTORY".to_owned(),
301                attribute: "LastModifiedDate".to_owned(),
302                expected: "a timestamp at or after CreationDate".to_owned(),
303                found: modified.to_string(),
304            });
305        }
306    }
307    Ok(tx.create(Entity::new(
308        "IFCOWNERHISTORY",
309        vec![
310            Value::Ref(draft.owning_user),
311            Value::Ref(draft.owning_application),
312            Value::Null,
313            draft
314                .change_action
315                .map_or(Value::Null, |a| Value::Enum(a.into())),
316            draft.last_modified_date.map_or(Value::Null, Value::Integer),
317            Value::Null,
318            Value::Null,
319            Value::Integer(draft.creation_date),
320        ],
321    )))
322}
323
324fn opt_text(value: Option<&str>) -> Value {
325    value.map_or(Value::Null, |v| Value::Text(v.into()))
326}