Skip to main content

ifc_classification/
authoring.rs

1//! Transactional classification/document/library authoring, bound to the
2//! model's release.
3//!
4//! These helpers only stage records on a caller-owned [`Transaction`];
5//! [`Transaction::commit`] owns atomic application.
6//!
7//! Every function that takes the [`Model`] lays its record out by attribute
8//! name from the bundled table of the release the header declares (see
9//! [`crate::classification_schema`]), never from IFC4 by assumption, and
10//! checks every reference against that release's declared type. A record
11//! type the release lacks (an IFC2X3 `IfcExternalReferenceRelationship`) is
12//! refused with [`ClassificationError::EntityNotInSchema`]; a draft value
13//! for an attribute it lacks (an IFC2X3 classification `Description`) with
14//! [`ClassificationError::AuthoringNotInSchema`]; text for an attribute it
15//! types as a record (an IFC2X3 `IfcCalendarDate`) with
16//! [`ClassificationError::AuthoringValueType`]; and an attribute it
17//! requires that the call leaves unset (the IFC2X3 `IfcRoot.OwnerHistory`,
18//! or IFC2X3 `IfcClassification.Source`) with
19//! [`ClassificationError::AuthoringRequired`]. A header binding no single
20//! known release is refused with `MultipleSchemas` or `UnsupportedSchema`.
21//! Nothing is staged on refusal.
22//!
23//! [`create_classification`] takes no model and cannot see the release: it
24//! writes the IFC4 layout, which IFC4X3 shares. Use
25//! [`create_classification_in`] for any other release.
26
27mod association;
28mod records;
29
30pub use association::{
31    associate_classification, associate_classification_with_owner_history, associate_document,
32    associate_document_with_owner_history, associate_library, associate_library_with_owner_history,
33    relate_documents,
34};
35pub use records::{
36    create_classification, create_classification_in, create_classification_reference,
37    create_document, create_document_reference, create_library, create_library_reference,
38};
39
40use std::sync::Arc;
41
42use ifc_model::{Edit, EntityId, Model, Transaction, Value};
43
44use crate::release::Release;
45use crate::{ClassificationError, ClassificationResult};
46
47/// Draft for one `IfcClassification`, fields named as IFC4 names them.
48#[derive(Debug, Clone, Copy)]
49pub struct ClassificationDraft<'a> {
50    /// `Source` publishing organization, when stated. Required by IFC2X3.
51    pub source: Option<&'a str>,
52    /// `Edition` identifier of the classification, when stated. Required by
53    /// IFC2X3.
54    pub edition: Option<&'a str>,
55    /// `EditionDate` (IFC date string), when stated. IFC2X3 types it as an
56    /// `IfcCalendarDate` record, so text is refused there.
57    pub edition_date: Option<&'a str>,
58    /// Required `Name` of the classification system.
59    pub name: &'a str,
60    /// `Description` of the classification, when stated. IFC4 onwards.
61    pub description: Option<&'a str>,
62    /// `Location` (URI) of the classification, when stated: written to
63    /// `Specification` in IFC4X3. IFC4 onwards.
64    pub location: Option<&'a str>,
65    /// `ReferenceTokens` delimiter set; must be non-empty when given. IFC4
66    /// onwards.
67    pub reference_tokens: Option<&'a [&'a str]>,
68}
69
70/// Draft for one `IfcClassificationReference`.
71#[derive(Debug, Clone, Copy)]
72pub struct ClassificationReferenceDraft<'a> {
73    /// `Location` (URI) of the reference; at least one of location/identification/name must be given.
74    pub location: Option<&'a str>,
75    /// `Identification` code within the classification, when stated
76    /// (`ItemReference` in IFC2X3).
77    pub identification: Option<&'a str>,
78    /// `Name` of the referenced item, when stated.
79    pub name: Option<&'a str>,
80    /// `ReferencedSource`: the parent `IfcClassification` or
81    /// `IfcClassificationReference` (only an `IfcClassification` in
82    /// IFC2X3), when stated.
83    pub referenced_source: Option<EntityId>,
84    /// `Description` of the reference, when stated. IFC4 onwards.
85    pub description: Option<&'a str>,
86    /// `Sort` order token, when stated. IFC4 onwards.
87    pub sort: Option<&'a str>,
88}
89
90/// Draft for one `IfcDocumentInformation`.
91#[derive(Debug, Clone, Copy)]
92pub struct DocumentDraft<'a> {
93    /// Required `Identification` code of the document (`DocumentId` in IFC2X3).
94    pub identification: &'a str,
95    /// Required `Name` of the document.
96    pub name: &'a str,
97    /// `Description` of the document, when stated.
98    pub description: Option<&'a str>,
99    /// `Location` (URI) of the document, when stated. IFC4 onwards.
100    pub location: Option<&'a str>,
101    /// `Purpose` of the document, when stated.
102    pub purpose: Option<&'a str>,
103    /// `IntendedUse` of the document, when stated.
104    pub intended_use: Option<&'a str>,
105    /// `Scope` of the document, when stated.
106    pub scope: Option<&'a str>,
107    /// `Revision` identifier, when stated.
108    pub revision: Option<&'a str>,
109    /// `DocumentOwner`: an `IfcActorSelect`, when stated.
110    pub document_owner: Option<EntityId>,
111    /// `Editors`: non-empty unique set of `IfcActorSelect` ids, when stated.
112    pub editors: Option<&'a [EntityId]>,
113    /// `CreationTime` (IFC date-time string), when stated. IFC2X3 types it
114    /// as an `IfcDateAndTime` record, so text is refused there.
115    pub creation_time: Option<&'a str>,
116    /// `LastRevisionTime` (IFC date-time string), when stated. A record in
117    /// IFC2X3, as `creation_time`.
118    pub last_revision_time: Option<&'a str>,
119    /// `ElectronicFormat`, when stated. IFC2X3 types it as an
120    /// `IfcDocumentElectronicFormat` record, so text is refused there.
121    pub electronic_format: Option<&'a str>,
122    /// `ValidFrom` (IFC date string), when stated. An `IfcCalendarDate`
123    /// record in IFC2X3.
124    pub valid_from: Option<&'a str>,
125    /// `ValidUntil` (IFC date string), when stated. An `IfcCalendarDate`
126    /// record in IFC2X3.
127    pub valid_until: Option<&'a str>,
128    /// `Confidentiality` enumerator; must be one of the release's
129    /// `IfcDocumentConfidentialityEnum` values.
130    pub confidentiality: Option<&'a str>,
131    /// `Status` enumerator; must be one of the release's
132    /// `IfcDocumentStatusEnum` values.
133    pub status: Option<&'a str>,
134}
135
136/// Draft for one `IfcDocumentReference`.
137#[derive(Debug, Clone, Copy)]
138pub struct DocumentReferenceDraft<'a> {
139    /// `Location` (URI) of the reference; at least one of location/identification/name must be given.
140    pub location: Option<&'a str>,
141    /// `Identification` code, when stated (`ItemReference` in IFC2X3).
142    pub identification: Option<&'a str>,
143    /// `Name`; exactly one of `name` and `referenced_document` must be given.
144    pub name: Option<&'a str>,
145    /// `Description` of the reference, when stated. IFC4 onwards.
146    pub description: Option<&'a str>,
147    /// `ReferencedDocument`: an `IfcDocumentInformation`; exactly one of
148    /// `name` and this must be given. IFC4 onwards: IFC2X3 links the other
149    /// way, so there `name` is required.
150    pub referenced_document: Option<EntityId>,
151}
152
153/// Draft for one `IfcLibraryInformation`.
154#[derive(Debug, Clone, Copy)]
155pub struct LibraryDraft<'a> {
156    /// Required `Name` of the library.
157    pub name: &'a str,
158    /// `Version` identifier, when stated.
159    pub version: Option<&'a str>,
160    /// `Publisher`: an `IfcActorSelect` (only an `IfcOrganization` in
161    /// IFC2X3), when stated.
162    pub publisher: Option<EntityId>,
163    /// `VersionDate` (IFC date-time string), when stated. An
164    /// `IfcCalendarDate` record in IFC2X3.
165    pub version_date: Option<&'a str>,
166    /// `Location` (URI) of the library, when stated. IFC4 onwards.
167    pub location: Option<&'a str>,
168    /// `Description` of the library, when stated. IFC4 onwards.
169    pub description: Option<&'a str>,
170}
171
172/// Draft for one `IfcLibraryReference`.
173#[derive(Debug, Clone, Copy)]
174pub struct LibraryReferenceDraft<'a> {
175    /// `Location` (URI) of the reference; at least one of location/identification/name must be given.
176    pub location: Option<&'a str>,
177    /// `Identification` code within the library, when stated
178    /// (`ItemReference` in IFC2X3).
179    pub identification: Option<&'a str>,
180    /// `Name` of the referenced item, when stated.
181    pub name: Option<&'a str>,
182    /// `Description` of the reference, when stated. IFC4 onwards.
183    pub description: Option<&'a str>,
184    /// `Language` of the referenced content, when stated. IFC4 onwards.
185    pub language: Option<&'a str>,
186    /// `ReferencedLibrary`: an `IfcLibraryInformation`, when stated. IFC4
187    /// onwards.
188    pub referenced_library: Option<EntityId>,
189}
190
191/// Draft shared by `IfcRelAssociatesClassification`/`Document`/`Library`.
192#[derive(Debug, Clone, Copy)]
193pub struct AssociationDraft<'a> {
194    /// Required `GlobalId`; must parse as a valid IFC GUID.
195    pub global_id: &'a str,
196    /// `Name` of the relationship, when stated.
197    pub name: Option<&'a str>,
198    /// `Description` of the relationship, when stated.
199    pub description: Option<&'a str>,
200    /// `RelatedObjects`: non-empty unique set of object or property
201    /// definitions (`IfcDefinitionSelect`; `IfcRoot` restricted by WR21 in
202    /// IFC2X3).
203    pub related_objects: &'a [EntityId],
204}
205
206pub(crate) fn text(value: &str) -> Value {
207    Value::Text(Arc::from(value))
208}
209fn optional_text(value: Option<&str>) -> Value {
210    value.map_or(Value::Null, text)
211}
212fn optional_ref(value: Option<EntityId>) -> Value {
213    value.map_or(Value::Null, Value::Ref)
214}
215fn refs(values: &[EntityId]) -> Value {
216    Value::List(values.iter().copied().map(Value::Ref).collect())
217}
218
219/// The type `id` will have once `tx` commits: its last staged create or
220/// retype, else the model's record. `None` when removed or absent.
221pub(crate) fn final_type<'a>(
222    tx: &'a Transaction,
223    model: &'a Model,
224    id: EntityId,
225) -> Option<&'a str> {
226    for edit in tx.edits().iter().rev() {
227        match edit {
228            Edit::Create {
229                id: edit_id,
230                entity,
231            } if *edit_id == id => return Some(&entity.type_name),
232            Edit::Retype {
233                id: edit_id,
234                type_name,
235            } if *edit_id == id => return Some(type_name),
236            Edit::Remove { id: edit_id } if *edit_id == id => return None,
237            _ => {}
238        }
239    }
240    model.get(id).map(|entity| entity.type_name.as_ref())
241}
242
243/// Check that `target` (committed or staged) is a legal value of
244/// `attribute` on `entity` in `release`, as the release declares it.
245///
246/// An attribute the release lacks is `AuthoringNotInSchema`; a target of
247/// another type is `AuthoringReferenceType`, labelled with the release's
248/// declared type (for example `IfcClassificationReferenceSelect`).
249pub(crate) fn require_accepts(
250    tx: &Transaction,
251    model: &Model,
252    release: Release<'_>,
253    entity: &'static str,
254    attribute: &'static str,
255    target: EntityId,
256) -> ClassificationResult<()> {
257    let declared = release.declared(entity, attribute)?;
258    let actual =
259        final_type(tx, model, target).ok_or(ClassificationError::UnknownEntity { id: target })?;
260    let (_, schema) = release.bound()?;
261    if schema.accepts_type(&declared.type_name, actual) {
262        Ok(())
263    } else {
264        Err(ClassificationError::AuthoringReferenceType {
265            target,
266            expected: declared.type_name.as_str(),
267            actual: actual.to_owned(),
268        })
269    }
270}
271
272/// Check `values` as a non-empty set of unique references accepted by
273/// `attribute` on `entity` in `release`.
274fn require_set(
275    tx: &Transaction,
276    model: &Model,
277    release: Release<'_>,
278    entity: &'static str,
279    attribute: &'static str,
280    values: &[EntityId],
281) -> ClassificationResult<()> {
282    if values.is_empty() {
283        return Err(ClassificationError::AuthoringInvalid {
284            entity,
285            attribute,
286            value: "empty SET [1:?]".into(),
287        });
288    }
289    let mut seen = std::collections::HashSet::new();
290    for &value in values {
291        if !seen.insert(value) {
292            return Err(ClassificationError::AuthoringInvalid {
293                entity,
294                attribute,
295                value: format!("duplicate {value}"),
296            });
297        }
298        require_accepts(tx, model, release, entity, attribute, value)?;
299    }
300    Ok(())
301}
302
303fn require_external_identity(
304    entity: &'static str,
305    location: Option<&str>,
306    identification: Option<&str>,
307    name: Option<&str>,
308) -> ClassificationResult<()> {
309    if location.is_some() || identification.is_some() || name.is_some() {
310        Ok(())
311    } else {
312        Err(ClassificationError::AuthoringInvalid {
313            entity,
314            attribute: "WR1",
315            value: "Location, Identification, and Name are all unstated".into(),
316        })
317    }
318}
319
320/// Check an enumerator against the release's declared enumeration.
321fn require_enum(
322    release: Release<'_>,
323    entity: &'static str,
324    attribute: &'static str,
325    value: Option<&str>,
326) -> ClassificationResult<()> {
327    let Some(value) = value else {
328        return Ok(());
329    };
330    let declared = release.declared(entity, attribute)?;
331    let (_, schema) = release.bound()?;
332    if crate::release::enumeration(schema, &declared.type_name)
333        .iter()
334        .any(|candidate| candidate.eq_ignore_ascii_case(value))
335    {
336        Ok(())
337    } else {
338        Err(ClassificationError::AuthoringInvalid {
339            entity,
340            attribute,
341            value: value.into(),
342        })
343    }
344}