Skip to main content

ifc_constraint/
association.rs

1//! `IfcRelAssociatesConstraint`, the one rooted record of this crate.
2//!
3//! It is the one writer bound to the model's declared release (see
4//! `release.rs`), because it carries `IfcRoot.OwnerHistory`, which IFC2X3
5//! requires.
6
7use std::collections::HashSet;
8
9use ifc_model::guid::Guid;
10use ifc_model::{EntityId, Model, Transaction, Value};
11
12use crate::authoring::{final_type, optional_ref, optional_text, refs, text, validate_target_in};
13use crate::release::{bind, require_owner_history};
14use crate::{ConstraintError, ConstraintResult};
15
16const ASSIGNMENT: &str = "IFCRELASSOCIATESCONSTRAINT";
17
18/// Draft for one rooted constraint association.
19#[derive(Debug, Clone, Copy)]
20pub struct ConstraintAssociationDraft<'a> {
21    /// Compressed IFC GlobalId.
22    pub global_id: &'a str,
23    /// Optional relationship name.
24    pub name: Option<&'a str>,
25    /// Optional relationship description.
26    pub description: Option<&'a str>,
27    /// Non-empty unique definition-select targets.
28    pub related_objects: &'a [EntityId],
29    /// Optional association intent.
30    pub intent: Option<&'a str>,
31    /// Existing or earlier-staged metric/objective.
32    pub relating_constraint: EntityId,
33}
34
35/// Validate and stage one rooted constraint association.
36///
37/// # Release
38///
39/// Bound to the model's declared release (#202): the record is laid out by
40/// attribute name from that release's table, and its references are checked
41/// against it (`RelatedObjects` is `IfcDefinitionSelect` in IFC4 and IFC4X3,
42/// `IfcRoot` restricted by WR21 in IFC2X3). `OwnerHistory` is left `$`,
43/// which IFC4 and IFC4X3 allow and IFC2X3 does not, so an IFC2X3 model is
44/// refused with [`ConstraintError::AuthoringRequired`]. Use
45/// [`associate_constraint_with_owner_history`] there. IFC2X3 also requires
46/// `Intent`, which IFC4 made optional.
47///
48/// # Errors
49///
50/// A malformed GlobalId, a relating constraint that is not an
51/// `IfcConstraint`, an empty, duplicated or out-of-type `RelatedObjects`; a
52/// model that binds no single known release
53/// ([`ConstraintError::MultipleSchemas`],
54/// [`ConstraintError::UnsupportedSchema`]), and an IFC2X3 model. Nothing is
55/// staged on an error.
56pub fn associate_constraint(
57    tx: &mut Transaction,
58    model: &Model,
59    draft: ConstraintAssociationDraft<'_>,
60) -> ConstraintResult<EntityId> {
61    associate(tx, model, draft, None)
62}
63
64/// [`associate_constraint`] with a caller-supplied `IfcOwnerHistory`, which
65/// IFC2X3 requires on every `IfcRoot`.
66///
67/// `owner_history` must be in the model or staged earlier on `tx`, and must
68/// be an `IfcOwnerHistory`; one is never invented here (build it with
69/// `ifc-author`). In IFC4 and IFC4X3 the reference fills the optional slot.
70///
71/// # Errors
72///
73/// Those of [`associate_constraint`] except the IFC2X3 `OwnerHistory`
74/// refusal (an IFC2X3 association without `intent` is still refused with
75/// [`ConstraintError::AuthoringRequired`]), and
76/// [`ConstraintError::UnknownEntity`] for an `owner_history` that does not
77/// resolve or [`ConstraintError::AuthoringReferenceType`] for one that is
78/// not an `IfcOwnerHistory`. Nothing is staged on an error.
79pub fn associate_constraint_with_owner_history(
80    tx: &mut Transaction,
81    model: &Model,
82    draft: ConstraintAssociationDraft<'_>,
83    owner_history: EntityId,
84) -> ConstraintResult<EntityId> {
85    associate(tx, model, draft, Some(owner_history))
86}
87
88/// Stage an `IfcRelAssociatesConstraint`; `None` leaves `OwnerHistory` `$`.
89fn associate(
90    tx: &mut Transaction,
91    model: &Model,
92    draft: ConstraintAssociationDraft<'_>,
93    owner_history: Option<EntityId>,
94) -> ConstraintResult<EntityId> {
95    let layout = bind(model)?;
96    if Guid::parse(draft.global_id).is_none() {
97        return Err(ConstraintError::AuthoringInvalid {
98            entity: ASSIGNMENT,
99            attribute: "GlobalId",
100            value: draft.global_id.into(),
101        });
102    }
103    validate_target_in(
104        layout.schema(),
105        tx,
106        model,
107        draft.relating_constraint,
108        "IfcConstraint",
109    )?;
110    if draft.related_objects.is_empty() {
111        return Err(ConstraintError::AuthoringInvalid {
112            entity: ASSIGNMENT,
113            attribute: "RelatedObjects",
114            value: "empty SET [1:?]".into(),
115        });
116    }
117    let mut seen = HashSet::new();
118    for &target in draft.related_objects {
119        if !seen.insert(target) {
120            return Err(ConstraintError::AuthoringInvalid {
121                entity: ASSIGNMENT,
122                attribute: "RelatedObjects",
123                value: format!("duplicate {target}"),
124            });
125        }
126        let actual =
127            final_type(tx, model, target).ok_or(ConstraintError::UnknownEntity { id: target })?;
128        if !layout.is_definition(actual) {
129            return Err(ConstraintError::AuthoringReferenceType {
130                target,
131                expected: "IfcDefinitionSelect",
132                actual: actual.into(),
133            });
134        }
135    }
136    if let Some(owner_history) = owner_history {
137        require_owner_history(tx, model, owner_history)?;
138    }
139    let record = layout.named_record(
140        ASSIGNMENT,
141        vec![
142            ("GlobalId", text(draft.global_id)),
143            ("OwnerHistory", optional_ref(owner_history)),
144            ("Name", optional_text(draft.name)),
145            ("Description", optional_text(draft.description)),
146            ("RelatedObjects", refs(draft.related_objects)),
147            ("Intent", optional_text(draft.intent)),
148            ("RelatingConstraint", Value::Ref(draft.relating_constraint)),
149        ],
150    )?;
151    Ok(tx.create(record))
152}