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)]
21pub struct PersonDraft<'a> {
22    /// `IfcPerson.Identification`.
23    pub identification: Option<&'a str>,
24    /// `IfcPerson.FamilyName`.
25    pub family_name: Option<&'a str>,
26    /// `IfcPerson.GivenName`.
27    pub given_name: Option<&'a str>,
28}
29
30/// Authored fields for `IfcOrganization`.
31#[derive(Debug, Clone, Copy, Default)]
32pub struct OrganizationDraft<'a> {
33    /// `IfcOrganization.Identification`.
34    pub identification: Option<&'a str>,
35    /// `IfcOrganization.Name`. Required by the schema.
36    pub name: &'a str,
37    /// `IfcOrganization.Description`.
38    pub description: Option<&'a str>,
39}
40
41/// Authored fields for `IfcApplication`.
42#[derive(Debug, Clone, Copy)]
43pub struct ApplicationDraft<'a> {
44    /// `IfcApplication.ApplicationDeveloper`, an `IfcOrganization`.
45    pub developer: EntityId,
46    /// `IfcApplication.Version`.
47    pub version: &'a str,
48    /// `IfcApplication.ApplicationFullName`.
49    pub full_name: &'a str,
50    /// `IfcApplication.ApplicationIdentifier`.
51    pub identifier: &'a str,
52}
53
54/// Stage an `IfcPerson`.
55///
56/// IFC4 requires that a person carry at least one of Identification,
57/// FamilyName or GivenName -- an entirely empty person identifies nobody and
58/// defeats the purpose of recording ownership.
59pub fn add_person(tx: &mut Transaction, draft: PersonDraft<'_>) -> AuthorResult<EntityId> {
60    if draft.identification.is_none() && draft.family_name.is_none() && draft.given_name.is_none() {
61        return Err(AuthorError::MissingRequired {
62            entity: "IFCPERSON".to_owned(),
63            attribute: "Identification|FamilyName|GivenName".to_owned(),
64        });
65    }
66    Ok(tx.create(Entity::new(
67        "IFCPERSON",
68        vec![
69            opt_text(draft.identification),
70            opt_text(draft.family_name),
71            opt_text(draft.given_name),
72            Value::Null,
73            Value::Null,
74            Value::Null,
75            Value::Null,
76            Value::Null,
77        ],
78    )))
79}
80
81/// Stage an `IfcOrganization`. `Name` is required by the schema.
82pub fn add_organization(
83    tx: &mut Transaction,
84    draft: OrganizationDraft<'_>,
85) -> AuthorResult<EntityId> {
86    if draft.name.trim().is_empty() {
87        return Err(AuthorError::MissingRequired {
88            entity: "IFCORGANIZATION".to_owned(),
89            attribute: "Name".to_owned(),
90        });
91    }
92    Ok(tx.create(Entity::new(
93        "IFCORGANIZATION",
94        vec![
95            opt_text(draft.identification),
96            Value::Text(draft.name.into()),
97            opt_text(draft.description),
98            Value::Null,
99            Value::Null,
100        ],
101    )))
102}
103
104/// Stage an `IfcPersonAndOrganization`, the `IfcActorSelect` used by
105/// `IfcOwnerHistory.OwningUser`.
106pub fn add_person_and_organization(
107    tx: &mut Transaction,
108    person: EntityId,
109    organization: EntityId,
110) -> EntityId {
111    tx.create(Entity::new(
112        "IFCPERSONANDORGANIZATION",
113        vec![Value::Ref(person), Value::Ref(organization), Value::Null],
114    ))
115}
116
117/// Stage an `IfcApplication`, the authoring tool recorded in ownership.
118pub fn add_application(
119    tx: &mut Transaction,
120    draft: ApplicationDraft<'_>,
121) -> AuthorResult<EntityId> {
122    for (attribute, value) in [
123        ("Version", draft.version),
124        ("ApplicationFullName", draft.full_name),
125        ("ApplicationIdentifier", draft.identifier),
126    ] {
127        if value.trim().is_empty() {
128            return Err(AuthorError::MissingRequired {
129                entity: "IFCAPPLICATION".to_owned(),
130                attribute: attribute.to_owned(),
131            });
132        }
133    }
134    Ok(tx.create(Entity::new(
135        "IFCAPPLICATION",
136        vec![
137            Value::Ref(draft.developer),
138            Value::Text(draft.version.into()),
139            Value::Text(draft.full_name.into()),
140            Value::Text(draft.identifier.into()),
141        ],
142    )))
143}
144
145/// Authored fields for `IfcOwnerHistory`.
146#[derive(Debug, Clone, Copy)]
147pub struct OwnerHistoryDraft<'a> {
148    /// `IfcOwnerHistory.OwningUser`, an `IfcPersonAndOrganization`.
149    pub owning_user: EntityId,
150    /// `IfcOwnerHistory.OwningApplication`, an `IfcApplication`.
151    pub owning_application: EntityId,
152    /// `IfcOwnerHistory.ChangeAction`, an `IfcChangeActionEnum` constant.
153    pub change_action: Option<&'a str>,
154    /// `IfcOwnerHistory.CreationDate`, an IFC timestamp (seconds since the
155    /// 1970 epoch).
156    pub creation_date: i64,
157    /// `IfcOwnerHistory.LastModifiedDate`, if the record was edited.
158    pub last_modified_date: Option<i64>,
159}
160
161/// Valid `IfcChangeActionEnum` constants in IFC4 ADD2 TC1.
162const CHANGE_ACTIONS: &[&str] = &["NOCHANGE", "MODIFIED", "ADDED", "DELETED", "NOTDEFINED"];
163
164/// Stage an `IfcOwnerHistory`.
165///
166/// Two invariants are enforced here rather than left to a validator. A
167/// `ChangeAction` outside `IfcChangeActionEnum` would serialize as an
168/// unparseable enumeration token. And a `LastModifiedDate` earlier than
169/// `CreationDate` describes a record edited before it existed: it parses,
170/// validates, and quietly corrupts any audit trail built on it.
171pub fn add_owner_history(
172    tx: &mut Transaction,
173    draft: OwnerHistoryDraft<'_>,
174) -> AuthorResult<EntityId> {
175    if let Some(action) = draft.change_action {
176        if !CHANGE_ACTIONS
177            .iter()
178            .any(|known| known.eq_ignore_ascii_case(action))
179        {
180            return Err(AuthorError::TypeMismatch {
181                entity: "IFCOWNERHISTORY".to_owned(),
182                attribute: "ChangeAction".to_owned(),
183                expected: "an IfcChangeActionEnum constant".to_owned(),
184                found: action.to_owned(),
185            });
186        }
187    }
188    if let Some(modified) = draft.last_modified_date {
189        if modified < draft.creation_date {
190            return Err(AuthorError::TypeMismatch {
191                entity: "IFCOWNERHISTORY".to_owned(),
192                attribute: "LastModifiedDate".to_owned(),
193                expected: "a timestamp at or after CreationDate".to_owned(),
194                found: modified.to_string(),
195            });
196        }
197    }
198    Ok(tx.create(Entity::new(
199        "IFCOWNERHISTORY",
200        vec![
201            Value::Ref(draft.owning_user),
202            Value::Ref(draft.owning_application),
203            Value::Null,
204            draft
205                .change_action
206                .map_or(Value::Null, |a| Value::Enum(a.into())),
207            draft.last_modified_date.map_or(Value::Null, Value::Integer),
208            Value::Null,
209            Value::Null,
210            Value::Integer(draft.creation_date),
211        ],
212    )))
213}
214
215fn opt_text(value: Option<&str>) -> Value {
216    value.map_or(Value::Null, |v| Value::Text(v.into()))
217}