Skip to main content

ifc_control/
assignment.rs

1//! Staging `IfcRelAssignsToControl` for the controls this crate owns.
2//!
3//! # Who writes the assignment
4//!
5//! The crate that owns the relating control writes the assignment.
6//! `ifc-cost` binds items to its cost schedules, `ifc-schedule` binds
7//! tasks to its work controls, and this module binds work to a permit,
8//! a project order, an action request or a performance history. A
9//! relating control of any other type is refused here, even though the
10//! schema admits every `IfcControl`: the owning crate knows which
11//! objects make sense under it, this one does not.
12//!
13//! `IfcPerformanceHistory` is included because it is an `IfcControl`
14//! subtype in IFC4 ADD2 TC1 and IFC4X3 ADD2, so `RelatingControl :
15//! IfcControl` admits it; the schema draws no distinction among the four.
16//!
17//! # What the schema requires
18//!
19//! `IfcRelAssigns.RelatedObjects` is `SET [1:?] OF IfcObjectDefinition`:
20//! at least one member, no duplicates, each an object definition.
21//! `IfcRelAssignsToControl.NoSelfReference` forbids the relating control
22//! among its own related objects. All four are refused before staging.
23//!
24//! # RelatedObjectsType
25//!
26//! Always left unset. IFC4 ADD2 TC1 declares it `OPTIONAL
27//! IfcObjectTypeEnum` under `WR1 : IfcCorrectObjectAssignment`, which
28//! returns TRUE when it is absent; IFC4X3 ADD2 redeclares it `OPTIONAL
29//! IfcStrippedOptional`, a BOOLEAN kept only so older files parse. Null
30//! is the one value valid in both, and naming the related objects' type
31//! would add a constraint the caller never asked for.
32
33use std::collections::HashSet;
34
35use ifc_model::guid::Guid;
36use ifc_model::{Entity, EntityId, Model, Transaction, Value};
37use ifc_schema::Schema;
38
39use crate::authoring::ControlKind;
40use crate::error::{ControlError, ControlResult};
41use crate::release::{bind, projected_type, Release};
42
43const RELATION: &str = "IFCRELASSIGNSTOCONTROL";
44
45/// One `IfcRelAssignsToControl` to stage.
46#[derive(Debug, Clone, Copy)]
47pub struct ControlAssignmentDraft<'a> {
48    /// `GlobalId`: a 22-character compressed IFC GUID.
49    pub global_id: &'a str,
50    /// `Name`.
51    pub name: Option<&'a str>,
52    /// `Description`.
53    pub description: Option<&'a str>,
54    /// `RelatingControl`: a permit, project order, action request or
55    /// performance history, in the model or staged earlier in the
56    /// transaction.
57    pub control: EntityId,
58    /// `RelatedObjects`: the work the control governs.
59    pub related_objects: &'a [EntityId],
60}
61
62/// Stage one `IfcRelAssignsToControl` relating work to an owned control.
63///
64/// References may target the model or entities staged earlier in `tx`.
65/// The record is laid out by attribute name in `schema`, so a schema that
66/// moves a slot produces a correct record rather than a shifted one.
67/// `OwnerHistory` is left unset, which IFC4 and IFC4X3 allow; an IFC2X3
68/// `schema` requires it and is refused with
69/// [`ControlError::AuthoringRequired`] (use
70/// [`assign_to_control_with_owner_history`]).
71///
72/// # Errors
73///
74/// Refuses a malformed GlobalId; a relating control or related object
75/// that exists neither in `model` nor in `tx`; a relating control that
76/// is not one of the four [`ControlKind`]s; an empty or duplicated
77/// `RelatedObjects`; the control among its own related objects; a
78/// related object that is not an `IfcObjectDefinition`; a schema that
79/// does not declare the relationship; and an IFC2X3 schema. Nothing is
80/// staged on an error.
81pub fn assign_to_control(
82    tx: &mut Transaction,
83    model: &Model,
84    schema: &Schema,
85    draft: ControlAssignmentDraft<'_>,
86) -> ControlResult<EntityId> {
87    let record = assignment_record(tx, model, Release::of_schema(schema), draft, Value::Null)?;
88    Ok(tx.create(record))
89}
90
91/// [`assign_to_control`] in `model`'s declared release, with a
92/// caller-supplied `IfcOwnerHistory`, which IFC2X3 requires (#202).
93///
94/// The release is bound from `FILE_SCHEMA` (none binds IFC4); in IFC4 and
95/// IFC4X3 the record is that of [`assign_to_control`] with the reference in
96/// the optional slot. The owner history is never invented.
97///
98/// # Errors
99///
100/// Those of [`assign_to_control`] except the IFC2X3 refusal, and the
101/// release and owner-history refusals of
102/// [`create_control_with_owner_history`](crate::create_control_with_owner_history).
103/// Nothing is staged on an error.
104pub fn assign_to_control_with_owner_history(
105    tx: &mut Transaction,
106    model: &Model,
107    draft: ControlAssignmentDraft<'_>,
108    owner_history: EntityId,
109) -> ControlResult<EntityId> {
110    let release = bind(model)?;
111    let record = assignment_record(tx, model, release, draft, Value::Ref(owner_history))?;
112    release.require_owner_history(tx, model, RELATION, owner_history)?;
113    Ok(tx.create(record))
114}
115
116fn assignment_record(
117    tx: &Transaction,
118    model: &Model,
119    release: Release<'_>,
120    draft: ControlAssignmentDraft<'_>,
121    owner_history: Value,
122) -> ControlResult<Entity> {
123    let schema = release.schema();
124    if schema.attributes(RELATION).is_empty() {
125        return Err(ControlError::UnsupportedEntity {
126            schema: schema.name().to_owned(),
127            entity: RELATION,
128        });
129    }
130
131    if Guid::parse(draft.global_id).is_none() {
132        return Err(invalid("GlobalId", draft.global_id));
133    }
134
135    let control_type = projected_type(tx, model, draft.control)
136        .ok_or(ControlError::UnknownEntity { id: draft.control })?;
137    if !ControlKind::ALL
138        .iter()
139        .any(|kind| control_type.eq_ignore_ascii_case(kind.type_name()))
140    {
141        return Err(ControlError::ForeignControl {
142            id: draft.control,
143            actual: control_type,
144        });
145    }
146
147    if draft.related_objects.is_empty() {
148        return Err(invalid("RelatedObjects", "expected at least one member"));
149    }
150    let mut seen = HashSet::new();
151    for &object in draft.related_objects {
152        if !seen.insert(object) {
153            return Err(invalid(
154                "RelatedObjects",
155                format!("{object} is listed twice"),
156            ));
157        }
158        if object == draft.control {
159            // `NoSelfReference`.
160            return Err(invalid(
161                "RelatedObjects",
162                format!("{object} is the relating control"),
163            ));
164        }
165        let actual =
166            projected_type(tx, model, object).ok_or(ControlError::UnknownEntity { id: object })?;
167        if !schema.is_a(&actual, "IFCOBJECTDEFINITION") {
168            return Err(invalid(
169                "RelatedObjects",
170                format!("{object} is {actual}, not an IfcObjectDefinition"),
171            ));
172        }
173    }
174
175    release.record(
176        RELATION,
177        vec![
178            ("GlobalId", Value::Text(draft.global_id.into())),
179            ("OwnerHistory", owner_history),
180            ("Name", text(draft.name)),
181            ("Description", text(draft.description)),
182            (
183                "RelatedObjects",
184                Value::List(
185                    draft
186                        .related_objects
187                        .iter()
188                        .copied()
189                        .map(Value::Ref)
190                        .collect(),
191                ),
192            ),
193            // RelatedObjectsType: always unset, see the module docs.
194            ("RelatedObjectsType", Value::Null),
195            ("RelatingControl", Value::Ref(draft.control)),
196        ],
197    )
198}
199
200fn invalid(attribute: &'static str, value: impl Into<String>) -> ControlError {
201    ControlError::AuthoringInvalid {
202        entity: RELATION,
203        attribute,
204        value: value.into(),
205    }
206}
207
208fn text(value: Option<&str>) -> Value {
209    value.map_or(Value::Null, |v| Value::Text(v.into()))
210}