Skip to main content

ifc_classification/authoring/
association.rs

1//! `IfcRelAssociates*` and document relationships, laid out by name in the
2//! bound release.
3//!
4//! `IfcRoot.OwnerHistory` is required in IFC2X3 TC1 and `OPTIONAL` from
5//! IFC4 on:
6//!
7//! ```text
8//! IFC2X3_TC1   OwnerHistory : IfcOwnerHistory;
9//! IFC4         OwnerHistory : OPTIONAL IfcOwnerHistory;
10//! IFC4X3_ADD2  OwnerHistory : OPTIONAL IfcOwnerHistory;
11//! ```
12//!
13//! The `associate_*` writers leave it unset, so they refuse an IFC2X3 model
14//! with [`ClassificationError::AuthoringRequired`] instead of writing `$`.
15//! The `*_with_owner_history` variants take a caller-supplied
16//! `IfcOwnerHistory` (in the model or staged on the transaction); one is
17//! never invented here. This follows `ifc-material` (#77) and
18//! `ifc-properties` (#191).
19
20use ifc_model::guid::Guid;
21use ifc_model::{EntityId, Model, Transaction, Value};
22
23use super::{optional_text, refs, require_accepts, text, AssociationDraft};
24use crate::release::Release;
25use crate::{ClassificationError, ClassificationResult};
26
27/// Check `object` as a `RelatedObjects` member: the release's declared type
28/// (`IfcDefinitionSelect`, or `IfcRoot` in IFC2X3) restricted to object and
29/// property definitions (IFC2X3 `IfcRelAssociates.WR21`).
30fn require_definition(
31    tx: &Transaction,
32    model: &Model,
33    release: Release<'_>,
34    relation: &'static str,
35    object: EntityId,
36) -> ClassificationResult<()> {
37    let declared = release.declared(relation, "RelatedObjects")?;
38    let actual = super::final_type(tx, model, object)
39        .ok_or(ClassificationError::UnknownEntity { id: object })?;
40    let (_, schema) = release.bound()?;
41    let definition =
42        schema.is_a(actual, "IFCOBJECTDEFINITION") || schema.is_a(actual, "IFCPROPERTYDEFINITION");
43    if definition && schema.accepts_type(&declared.type_name, actual) {
44        Ok(())
45    } else {
46        Err(ClassificationError::AuthoringReferenceType {
47            target: object,
48            expected: declared.type_name.as_str(),
49            actual: actual.to_owned(),
50        })
51    }
52}
53
54fn associate(
55    tx: &mut Transaction,
56    model: &Model,
57    draft: AssociationDraft<'_>,
58    target: EntityId,
59    kind: &'static str,
60    target_attribute: &'static str,
61    owner_history: Option<EntityId>,
62) -> ClassificationResult<EntityId> {
63    let release = Release::of(model);
64    release.require_entity(kind)?;
65    if Guid::parse(draft.global_id).is_none() {
66        return Err(ClassificationError::AuthoringInvalid {
67            entity: kind,
68            attribute: "GlobalId",
69            value: draft.global_id.into(),
70        });
71    }
72    if draft.related_objects.is_empty() {
73        return Err(ClassificationError::AuthoringInvalid {
74            entity: kind,
75            attribute: "RelatedObjects",
76            value: "empty SET [1:?]".into(),
77        });
78    }
79    let mut seen = std::collections::HashSet::new();
80    for &object in draft.related_objects {
81        if !seen.insert(object) {
82            return Err(ClassificationError::AuthoringInvalid {
83                entity: kind,
84                attribute: "RelatedObjects",
85                value: format!("duplicate {object}"),
86            });
87        }
88        require_definition(tx, model, release, kind, object)?;
89    }
90    require_accepts(tx, model, release, kind, target_attribute, target)?;
91    if let Some(owner_history) = owner_history {
92        require_accepts(tx, model, release, kind, "OwnerHistory", owner_history)?;
93    }
94    let record = release.record(
95        kind,
96        vec![
97            ("GlobalId", text(draft.global_id)),
98            (
99                "OwnerHistory",
100                owner_history.map_or(Value::Null, Value::Ref),
101            ),
102            ("Name", optional_text(draft.name)),
103            ("Description", optional_text(draft.description)),
104            ("RelatedObjects", refs(draft.related_objects)),
105            (target_attribute, Value::Ref(target)),
106        ],
107    )?;
108    Ok(tx.create(record))
109}
110
111const CLASSIFICATION: (&str, &str) = ("IFCRELASSOCIATESCLASSIFICATION", "RelatingClassification");
112const DOCUMENT: (&str, &str) = ("IFCRELASSOCIATESDOCUMENT", "RelatingDocument");
113const LIBRARY: (&str, &str) = ("IFCRELASSOCIATESLIBRARY", "RelatingLibrary");
114
115/// Validate and stage an `IfcRelAssociatesClassification` linking
116/// `draft.related_objects` to `target`, with `OwnerHistory` unset.
117///
118/// # Errors
119///
120/// A `target` the release's `RelatingClassification` does not accept
121/// (`IfcClassificationSelect`; in IFC2X3 `IfcClassificationNotationSelect`,
122/// which admits a reference or notation but no `IfcClassification`); a
123/// malformed `GlobalId`; an empty, duplicated or non-definition
124/// `RelatedObjects`; an IFC2X3 model, which requires `OwnerHistory`
125/// (`AuthoringRequired`: use
126/// [`associate_classification_with_owner_history`]); and a header binding
127/// no single known release. Nothing is staged on an error.
128pub fn associate_classification(
129    tx: &mut Transaction,
130    model: &Model,
131    draft: AssociationDraft<'_>,
132    target: EntityId,
133) -> ClassificationResult<EntityId> {
134    let (kind, attribute) = CLASSIFICATION;
135    associate(tx, model, draft, target, kind, attribute, None)
136}
137
138/// [`associate_classification`] with a caller-supplied `IfcOwnerHistory`,
139/// which IFC2X3 requires.
140///
141/// # Errors
142///
143/// Those of [`associate_classification`] except the IFC2X3 refusal, and an
144/// `owner_history` that does not resolve (`UnknownEntity`) or is not an
145/// `IfcOwnerHistory` (`AuthoringReferenceType`).
146pub fn associate_classification_with_owner_history(
147    tx: &mut Transaction,
148    model: &Model,
149    draft: AssociationDraft<'_>,
150    target: EntityId,
151    owner_history: EntityId,
152) -> ClassificationResult<EntityId> {
153    let (kind, attribute) = CLASSIFICATION;
154    associate(
155        tx,
156        model,
157        draft,
158        target,
159        kind,
160        attribute,
161        Some(owner_history),
162    )
163}
164
165/// Validate and stage an `IfcRelAssociatesDocument` linking
166/// `draft.related_objects` to `target`, with `OwnerHistory` unset.
167///
168/// # Errors
169///
170/// A `target` that is not an `IfcDocumentSelect`, the shared association
171/// preconditions of [`associate_classification`], and an IFC2X3 model
172/// (use [`associate_document_with_owner_history`]). Nothing is staged on an
173/// error.
174pub fn associate_document(
175    tx: &mut Transaction,
176    model: &Model,
177    draft: AssociationDraft<'_>,
178    target: EntityId,
179) -> ClassificationResult<EntityId> {
180    let (kind, attribute) = DOCUMENT;
181    associate(tx, model, draft, target, kind, attribute, None)
182}
183
184/// [`associate_document`] with a caller-supplied `IfcOwnerHistory`, which
185/// IFC2X3 requires.
186///
187/// # Errors
188///
189/// Those of [`associate_document`] except the IFC2X3 refusal, and the
190/// owner-history refusals of [`associate_classification_with_owner_history`].
191pub fn associate_document_with_owner_history(
192    tx: &mut Transaction,
193    model: &Model,
194    draft: AssociationDraft<'_>,
195    target: EntityId,
196    owner_history: EntityId,
197) -> ClassificationResult<EntityId> {
198    let (kind, attribute) = DOCUMENT;
199    associate(
200        tx,
201        model,
202        draft,
203        target,
204        kind,
205        attribute,
206        Some(owner_history),
207    )
208}
209
210/// Validate and stage an `IfcRelAssociatesLibrary` linking
211/// `draft.related_objects` to `target`, with `OwnerHistory` unset.
212///
213/// # Errors
214///
215/// A `target` that is not an `IfcLibrarySelect`, the shared association
216/// preconditions of [`associate_classification`], and an IFC2X3 model
217/// (use [`associate_library_with_owner_history`]). Nothing is staged on an
218/// error.
219pub fn associate_library(
220    tx: &mut Transaction,
221    model: &Model,
222    draft: AssociationDraft<'_>,
223    target: EntityId,
224) -> ClassificationResult<EntityId> {
225    let (kind, attribute) = LIBRARY;
226    associate(tx, model, draft, target, kind, attribute, None)
227}
228
229/// [`associate_library`] with a caller-supplied `IfcOwnerHistory`, which
230/// IFC2X3 requires.
231///
232/// # Errors
233///
234/// Those of [`associate_library`] except the IFC2X3 refusal, and the
235/// owner-history refusals of [`associate_classification_with_owner_history`].
236pub fn associate_library_with_owner_history(
237    tx: &mut Transaction,
238    model: &Model,
239    draft: AssociationDraft<'_>,
240    target: EntityId,
241    owner_history: EntityId,
242) -> ClassificationResult<EntityId> {
243    let (kind, attribute) = LIBRARY;
244    associate(
245        tx,
246        model,
247        draft,
248        target,
249        kind,
250        attribute,
251        Some(owner_history),
252    )
253}
254
255/// Stage an `IfcDocumentInformationRelationship` in the layout of `model`'s
256/// declared release.
257///
258/// This is how a document says it supersedes, amends or accompanies
259/// another. `RelationshipType` is a free label rather than an enum:
260/// the schema does not fix the vocabulary, so it is written as given.
261/// IFC2X3 declares only the three relationship attributes; IFC4 and IFC4X3
262/// add the inherited `Name` and `Description`, left unset here.
263///
264/// # Errors
265///
266/// Refuses a reference that is not an `IfcDocumentInformation`, an
267/// empty related set (`SET [1:?]`), a document related to itself, and a
268/// header binding no single known release. Nothing is staged on an error.
269pub fn relate_documents(
270    tx: &mut Transaction,
271    model: &Model,
272    relating: EntityId,
273    related: &[EntityId],
274    relationship_type: Option<&str>,
275) -> ClassificationResult<EntityId> {
276    const ENTITY: &str = "IFCDOCUMENTINFORMATIONRELATIONSHIP";
277    let release = Release::of(model);
278    release.require_entity(ENTITY)?;
279    require_accepts(tx, model, release, ENTITY, "RelatingDocument", relating)?;
280    if related.is_empty() {
281        return Err(ClassificationError::AuthoringInvalid {
282            entity: ENTITY,
283            attribute: "RelatedDocuments",
284            value: "expected at least one document, per SET [1:?]".to_owned(),
285        });
286    }
287    for document in related {
288        require_accepts(tx, model, release, ENTITY, "RelatedDocuments", *document)?;
289        // A document that supersedes itself is a cycle at depth one.
290        if *document == relating {
291            return Err(ClassificationError::AuthoringInvalid {
292                entity: ENTITY,
293                attribute: "RelatedDocuments",
294                value: "expected a document other than the relating one".to_owned(),
295            });
296        }
297    }
298    let record = release.record(
299        ENTITY,
300        vec![
301            ("RelatingDocument", Value::Ref(relating)),
302            ("RelatedDocuments", refs(related)),
303            ("RelationshipType", optional_text(relationship_type)),
304        ],
305    )?;
306    Ok(tx.create(record))
307}