Skip to main content

ifc_resource/author/
actor.rs

1//! Staging actors, occupants, and assets.
2//!
3//! # Who a record names
4//!
5//! `IfcActor` names a party playing a role on the project; an
6//! `IfcOccupant` is that party in a tenancy role. Both point at an
7//! `IfcActorSelect`, which is the party itself: a person, an
8//! organization, or a person acting for one. An actor whose
9//! `TheActor` does not resolve names nobody, and the assignment
10//! relationships that hang off it inherit that emptiness.
11//!
12//! `IfcAsset` is a group with a monetary tail: three cost values, a
13//! depreciated value, an owner, a user, and a responsible person. It
14//! is the accounting view of things already modelled elsewhere, so
15//! every attribute past the group slots is optional.
16
17use ifc_model::{EntityId, Value};
18
19use crate::author::editor::{build_entity, refs, text, validate_enum, ResourceEditor};
20use crate::error::{ResourceError, ResourceResult};
21
22/// The three `IfcActorSelect` members.
23///
24/// Named rather than inlined because both the actor writer and the
25/// asset writer point at the same select, and a mismatch between
26/// them would accept a party in one place and refuse it in the other.
27const ACTOR_SELECT: &[&str] = &["IfcOrganization", "IfcPerson", "IfcPersonAndOrganization"];
28
29/// Attributes of an `IfcActor` or `IfcOccupant` beyond the party.
30#[derive(Debug, Clone, Copy)]
31pub struct ActorDraft<'a> {
32    /// `GlobalId`, a compressed IFC GUID.
33    pub global_id: &'a str,
34    /// `TheActor`: the party this record speaks for.
35    ///
36    /// Required by the schema and not defaultable: a record naming
37    /// nobody identifies no party.
38    pub the_actor: EntityId,
39    /// `Name`.
40    pub name: Option<&'a str>,
41    /// `Description`.
42    pub description: Option<&'a str>,
43    /// `ObjectType`. Required when `predefined_type` is `USERDEFINED`.
44    pub object_type: Option<&'a str>,
45    /// `PredefinedType`, an `IfcOccupantTypeEnum` token.
46    ///
47    /// `IfcActor` declares no predefined type; supplying one for a
48    /// plain actor is refused rather than dropped.
49    pub predefined_type: Option<&'a str>,
50}
51
52/// Attributes of an `IfcAsset`.
53///
54/// Every attribute past the group slots is optional: an asset is the
55/// accounting view of things modelled elsewhere, and a register may
56/// know an item's owner long before it knows its depreciated value.
57#[derive(Debug, Clone, Copy, Default)]
58pub struct AssetDraft<'a> {
59    /// `GlobalId`, a compressed IFC GUID.
60    pub global_id: &'a str,
61    /// `Name`.
62    pub name: Option<&'a str>,
63    /// `Description`.
64    pub description: Option<&'a str>,
65    /// `ObjectType`.
66    pub object_type: Option<&'a str>,
67    /// `Identification`: the asset register's own number.
68    pub identification: Option<&'a str>,
69    /// `OriginalValue`, an `IfcCostValue`.
70    pub original_value: Option<EntityId>,
71    /// `CurrentValue`, an `IfcCostValue`.
72    pub current_value: Option<EntityId>,
73    /// `TotalReplacementCost`, an `IfcCostValue`.
74    pub total_replacement_cost: Option<EntityId>,
75    /// `Owner`, an `IfcActorSelect`.
76    pub owner: Option<EntityId>,
77    /// `User`, an `IfcActorSelect`.
78    pub user: Option<EntityId>,
79    /// `ResponsiblePerson`, an `IfcPerson`.
80    pub responsible_person: Option<EntityId>,
81    /// `IncorporationDate`, an `IfcDate` in ISO 8601 form.
82    pub incorporation_date: Option<&'a str>,
83    /// `DepreciatedValue`, an `IfcCostValue`.
84    pub depreciated_value: Option<EntityId>,
85}
86
87impl ResourceEditor<'_> {
88    /// Stage an `IfcActor`, or an `IfcOccupant` when a predefined
89    /// type is given.
90    ///
91    /// `the_actor` must resolve to an `IfcActorSelect` member. A
92    /// record naming nobody identifies no party, so the reference is
93    /// checked before staging.
94    ///
95    /// # Errors
96    ///
97    /// Refuses a malformed or duplicate GlobalId, a `the_actor` outside
98    /// `IfcActorSelect`, a token outside `IfcOccupantTypeEnum`, a
99    /// predefined type on a plain `IfcActor`, and `USERDEFINED`
100    /// without `ObjectType` (WR31).
101    pub fn create_actor(&mut self, draft: ActorDraft<'_>) -> ResourceResult<EntityId> {
102        let occupant = draft.predefined_type.is_some();
103        let entity_type = if occupant { "IfcOccupant" } else { "IfcActor" };
104        self.validate_new_global_id(draft.global_id)?;
105        self.check_reference_select(
106            draft.the_actor,
107            "TheActor",
108            "IfcActorSelect",
109            ACTOR_SELECT,
110            draft.the_actor,
111        )?;
112        if let Some(value) = draft.predefined_type {
113            validate_enum(self.schema, entity_type, "PredefinedType", value)?;
114            if value == "USERDEFINED"
115                && draft
116                    .object_type
117                    .is_none_or(|value| value.trim().is_empty())
118            {
119                return Err(ResourceError::SemanticViolation {
120                    entity: None,
121                    rule: "USERDEFINED_REQUIRES_OBJECT_TYPE",
122                });
123            }
124        }
125        let entity = build_entity(
126            self.schema,
127            entity_type,
128            &[
129                ("GlobalId", Some(text(draft.global_id))),
130                ("Name", draft.name.map(text)),
131                ("Description", draft.description.map(text)),
132                ("ObjectType", draft.object_type.map(text)),
133                ("TheActor", Some(Value::Ref(draft.the_actor))),
134                (
135                    "PredefinedType",
136                    draft.predefined_type.map(|t| Value::Enum(t.into())),
137                ),
138            ],
139        )?;
140        self.commit_create(entity)
141    }
142
143    /// Stage an `IfcAsset`.
144    ///
145    /// The three value slots are `IfcCostValue` references and the
146    /// two party slots are `IfcActorSelect`; each is checked before
147    /// staging so an asset cannot claim a value or an owner that the
148    /// model does not hold.
149    ///
150    /// # Errors
151    ///
152    /// Refuses a malformed or duplicate GlobalId, a value reference
153    /// that is not an `IfcCostValue`, a party outside `IfcActorSelect`,
154    /// and a responsible person that is not an `IfcPerson`.
155    pub fn create_asset(&mut self, draft: AssetDraft<'_>) -> ResourceResult<EntityId> {
156        self.validate_new_global_id(draft.global_id)?;
157        for (attribute, target) in [
158            ("OriginalValue", draft.original_value),
159            ("CurrentValue", draft.current_value),
160            ("TotalReplacementCost", draft.total_replacement_cost),
161            ("DepreciatedValue", draft.depreciated_value),
162        ] {
163            if let Some(target) = target {
164                self.check_reference(target, attribute, "IfcCostValue", target)?;
165            }
166        }
167        for (attribute, target) in [("Owner", draft.owner), ("User", draft.user)] {
168            if let Some(target) = target {
169                self.check_reference_select(
170                    target,
171                    attribute,
172                    "IfcActorSelect",
173                    ACTOR_SELECT,
174                    target,
175                )?;
176            }
177        }
178        if let Some(person) = draft.responsible_person {
179            self.check_reference(person, "ResponsiblePerson", "IfcPerson", person)?;
180        }
181        let entity = build_entity(
182            self.schema,
183            "IfcAsset",
184            &[
185                ("GlobalId", Some(text(draft.global_id))),
186                ("Name", draft.name.map(text)),
187                ("Description", draft.description.map(text)),
188                ("ObjectType", draft.object_type.map(text)),
189                ("Identification", draft.identification.map(text)),
190                ("OriginalValue", draft.original_value.map(Value::Ref)),
191                ("CurrentValue", draft.current_value.map(Value::Ref)),
192                (
193                    "TotalReplacementCost",
194                    draft.total_replacement_cost.map(Value::Ref),
195                ),
196                ("Owner", draft.owner.map(Value::Ref)),
197                ("User", draft.user.map(Value::Ref)),
198                (
199                    "ResponsiblePerson",
200                    draft.responsible_person.map(Value::Ref),
201                ),
202                ("IncorporationDate", draft.incorporation_date.map(text)),
203                ("DepreciatedValue", draft.depreciated_value.map(Value::Ref)),
204            ],
205        )?;
206        self.commit_create(entity)
207    }
208
209    /// Stage an `IfcInventory`.
210    ///
211    /// # Errors
212    ///
213    /// Refuses a duplicate or malformed GlobalId, a `jurisdiction`
214    /// outside `IfcActorSelect`, a value that is not an `IfcCostValue`,
215    /// a `responsible_persons` entry that is not an `IfcPerson`, an
216    /// empty person set (the schema bounds it `SET [1:?]`), and
217    /// `USERDEFINED` without an `object_type`.
218    pub fn create_inventory(
219        &mut self,
220        draft: InventoryDraft<'_>,
221        responsible_persons: &[EntityId],
222    ) -> ResourceResult<EntityId> {
223        const ENTITY: &str = "IfcInventory";
224        self.validate_new_global_id(draft.global_id)?;
225        if let Some(token) = draft.predefined_type {
226            validate_enum(self.schema, ENTITY, "PredefinedType", token)?;
227            if token.eq_ignore_ascii_case("USERDEFINED")
228                && draft.object_type.is_none_or(|text| text.trim().is_empty())
229            {
230                return Err(ResourceError::SemanticViolation {
231                    entity: None,
232                    rule: "USERDEFINED_REQUIRES_OBJECT_TYPE",
233                });
234            }
235        }
236        if let Some(target) = draft.jurisdiction {
237            self.check_reference_select(
238                target,
239                "Jurisdiction",
240                "IfcActorSelect",
241                ACTOR_SELECT,
242                target,
243            )?;
244        }
245        for (attribute, target) in [
246            ("CurrentValue", draft.current_value),
247            ("OriginalValue", draft.original_value),
248        ] {
249            if let Some(target) = target {
250                self.check_reference(target, attribute, "IfcCostValue", target)?;
251            }
252        }
253        // SET [1:?]: an inventory nobody is responsible for is a
254        // record with no owner, which the schema does not allow to be
255        // stated as an empty set.
256        if responsible_persons.is_empty() {
257            return Err(ResourceError::InvalidDraft {
258                entity_type: ENTITY,
259                attribute: "ResponsiblePersons",
260                expected: "at least one person, per SET [1:?]",
261            });
262        }
263        for person in responsible_persons {
264            self.check_reference(*person, "ResponsiblePersons", "IfcPerson", *person)?;
265        }
266        let entity = build_entity(
267            self.schema,
268            ENTITY,
269            &[
270                ("GlobalId", Some(text(draft.global_id))),
271                ("Name", draft.name.map(text)),
272                ("Description", draft.description.map(text)),
273                ("ObjectType", draft.object_type.map(text)),
274                (
275                    "PredefinedType",
276                    draft.predefined_type.map(|t| Value::Enum(t.into())),
277                ),
278                ("Jurisdiction", draft.jurisdiction.map(Value::Ref)),
279                ("ResponsiblePersons", Some(refs(responsible_persons))),
280                ("LastUpdateDate", draft.last_update_date.map(text)),
281                ("CurrentValue", draft.current_value.map(Value::Ref)),
282                ("OriginalValue", draft.original_value.map(Value::Ref)),
283            ],
284        )?;
285        self.commit_create(entity)
286    }
287}
288
289/// Draft for one `IfcInventory`: a counted collection of things.
290#[derive(Debug, Clone, Copy, Default)]
291pub struct InventoryDraft<'a> {
292    /// `GlobalId`.
293    pub global_id: &'a str,
294    /// `Name`.
295    pub name: Option<&'a str>,
296    /// `Description`.
297    pub description: Option<&'a str>,
298    /// `ObjectType`. Required when `predefined_type` is `USERDEFINED`.
299    pub object_type: Option<&'a str>,
300    /// `PredefinedType`, an `IfcInventoryTypeEnum` token.
301    pub predefined_type: Option<&'a str>,
302    /// `Jurisdiction`, an `IfcActorSelect`.
303    pub jurisdiction: Option<EntityId>,
304    /// `LastUpdateDate`, an ISO 8601 date written as given.
305    pub last_update_date: Option<&'a str>,
306    /// `CurrentValue`, an `IfcCostValue`.
307    pub current_value: Option<EntityId>,
308    /// `OriginalValue`, an `IfcCostValue`.
309    pub original_value: Option<EntityId>,
310}