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;