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