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}