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)]
31#[non_exhaustive]
32pub struct ActorDraft<'a> {
33    /// `GlobalId`, a compressed IFC GUID.
34    pub global_id: &'a str,
35    /// `TheActor`: the party this record speaks for.
36    ///
37    /// Required by the schema and not defaultable: a record naming
38    /// nobody identifies no party.
39    pub the_actor: EntityId,
40    /// `Name`.
41    pub name: Option<&'a str>,
42    /// `Description`.
43    pub description: Option<&'a str>,
44    /// `ObjectType`. Required when `predefined_type` is `USERDEFINED`.
45    pub object_type: Option<&'a str>,
46    /// `PredefinedType`, an `IfcOccupantTypeEnum` token.
47    ///
48    /// `IfcActor` declares no predefined type; supplying one for a
49    /// plain actor is refused rather than dropped.
50    pub predefined_type: Option<&'a str>,
51}
52
53impl<'a> ActorDraft<'a> {
54    /// Starts a draft with its required fields; the rest are unset.
55    #[must_use]
56    pub fn new(global_id: &'a str, the_actor: EntityId) -> Self {
57        Self {
58            global_id,
59            the_actor,
60            name: None,
61            description: None,
62            object_type: None,
63            predefined_type: None,
64        }
65    }
66
67    /// Sets [`Self::name`]: `Name`.
68    #[must_use]
69    pub fn name(mut self, value: &'a str) -> Self {
70        self.name = Some(value);
71        self
72    }
73
74    /// Sets [`Self::description`]: `Description`.
75    #[must_use]
76    pub fn description(mut self, value: &'a str) -> Self {
77        self.description = Some(value);
78        self
79    }
80
81    /// Sets [`Self::object_type`]: `ObjectType`. Required when `predefined_type` is `USERDEFINED`.
82    #[must_use]
83    pub fn object_type(mut self, value: &'a str) -> Self {
84        self.object_type = Some(value);
85        self
86    }
87
88    /// Sets [`Self::predefined_type`]: `PredefinedType`, an `IfcOccupantTypeEnum` token.
89    #[must_use]
90    pub fn predefined_type(mut self, value: &'a str) -> Self {
91        self.predefined_type = Some(value);
92        self
93    }
94}
95
96/// Attributes of an `IfcAsset`.
97///
98/// Every attribute past the group slots is optional: an asset is the
99/// accounting view of things modelled elsewhere, and a register may
100/// know an item's owner long before it knows its depreciated value.
101#[derive(Debug, Clone, Copy, Default)]
102#[non_exhaustive]
103pub struct AssetDraft<'a> {
104    /// `GlobalId`, a compressed IFC GUID.
105    pub global_id: &'a str,
106    /// `Name`.
107    pub name: Option<&'a str>,
108    /// `Description`.
109    pub description: Option<&'a str>,
110    /// `ObjectType`.
111    pub object_type: Option<&'a str>,
112    /// `Identification`: the asset register's own number.
113    pub identification: Option<&'a str>,
114    /// `OriginalValue`, an `IfcCostValue`.
115    pub original_value: Option<EntityId>,
116    /// `CurrentValue`, an `IfcCostValue`.
117    pub current_value: Option<EntityId>,
118    /// `TotalReplacementCost`, an `IfcCostValue`.
119    pub total_replacement_cost: Option<EntityId>,
120    /// `Owner`, an `IfcActorSelect`.
121    pub owner: Option<EntityId>,
122    /// `User`, an `IfcActorSelect`.
123    pub user: Option<EntityId>,
124    /// `ResponsiblePerson`, an `IfcPerson`.
125    pub responsible_person: Option<EntityId>,
126    /// `IncorporationDate`, an `IfcDate` in ISO 8601 form.
127    pub incorporation_date: Option<&'a str>,
128    /// `DepreciatedValue`, an `IfcCostValue`.
129    pub depreciated_value: Option<EntityId>,
130}
131
132impl<'a> AssetDraft<'a> {
133    /// Starts a draft with its required fields; the rest are unset.
134    #[must_use]
135    pub fn new(global_id: &'a str) -> Self {
136        Self {
137            global_id,
138            name: None,
139            description: None,
140            object_type: None,
141            identification: None,
142            original_value: None,
143            current_value: None,
144            total_replacement_cost: None,
145            owner: None,
146            user: None,
147            responsible_person: None,
148            incorporation_date: None,
149            depreciated_value: None,
150        }
151    }
152
153    /// Sets [`Self::name`]: `Name`.
154    #[must_use]
155    pub fn name(mut self, value: &'a str) -> Self {
156        self.name = Some(value);
157        self
158    }
159
160    /// Sets [`Self::description`]: `Description`.
161    #[must_use]
162    pub fn description(mut self, value: &'a str) -> Self {
163        self.description = Some(value);
164        self
165    }
166
167    /// Sets [`Self::object_type`]: `ObjectType`.
168    #[must_use]
169    pub fn object_type(mut self, value: &'a str) -> Self {
170        self.object_type = Some(value);
171        self
172    }
173
174    /// Sets [`Self::identification`]: `Identification`: the asset register's own number.
175    #[must_use]
176    pub fn identification(mut self, value: &'a str) -> Self {
177        self.identification = Some(value);
178        self
179    }
180
181    /// Sets [`Self::original_value`]: `OriginalValue`, an `IfcCostValue`.
182    #[must_use]
183    pub fn original_value(mut self, value: EntityId) -> Self {
184        self.original_value = Some(value);
185        self
186    }
187
188    /// Sets [`Self::current_value`]: `CurrentValue`, an `IfcCostValue`.
189    #[must_use]
190    pub fn current_value(mut self, value: EntityId) -> Self {
191        self.current_value = Some(value);
192        self
193    }
194
195    /// Sets [`Self::total_replacement_cost`]: `TotalReplacementCost`, an `IfcCostValue`.
196    #[must_use]
197    pub fn total_replacement_cost(mut self, value: EntityId) -> Self {
198        self.total_replacement_cost = Some(value);
199        self
200    }
201
202    /// Sets [`Self::owner`]: `Owner`, an `IfcActorSelect`.
203    #[must_use]
204    pub fn owner(mut self, value: EntityId) -> Self {
205        self.owner = Some(value);
206        self
207    }
208
209    /// Sets [`Self::user`]: `User`, an `IfcActorSelect`.
210    #[must_use]
211    pub fn user(mut self, value: EntityId) -> Self {
212        self.user = Some(value);
213        self
214    }
215
216    /// Sets [`Self::responsible_person`]: `ResponsiblePerson`, an `IfcPerson`.
217    #[must_use]
218    pub fn responsible_person(mut self, value: EntityId) -> Self {
219        self.responsible_person = Some(value);
220        self
221    }
222
223    /// Sets [`Self::incorporation_date`]: `IncorporationDate`, an `IfcDate` in ISO 8601 form.
224    #[must_use]
225    pub fn incorporation_date(mut self, value: &'a str) -> Self {
226        self.incorporation_date = Some(value);
227        self
228    }
229
230    /// Sets [`Self::depreciated_value`]: `DepreciatedValue`, an `IfcCostValue`.
231    #[must_use]
232    pub fn depreciated_value(mut self, value: EntityId) -> Self {
233        self.depreciated_value = Some(value);
234        self
235    }
236}
237
238impl ResourceEditor<'_> {
239    /// Stage an `IfcActor`, or an `IfcOccupant` when a predefined
240    /// type is given.
241    ///
242    /// `the_actor` must resolve to an `IfcActorSelect` member. A
243    /// record naming nobody identifies no party, so the reference is
244    /// checked before staging.
245    ///
246    /// # Errors
247    ///
248    /// Refuses a malformed or duplicate GlobalId, a `the_actor` outside
249    /// `IfcActorSelect`, a token outside `IfcOccupantTypeEnum`, a
250    /// predefined type on a plain `IfcActor`, and `USERDEFINED`
251    /// without `ObjectType` (WR31).
252    pub fn create_actor(&mut self, draft: ActorDraft<'_>) -> ResourceResult<EntityId> {
253        let occupant = draft.predefined_type.is_some();
254        let entity_type = if occupant { "IfcOccupant" } else { "IfcActor" };
255        self.validate_new_global_id(draft.global_id)?;
256        self.check_reference_select(
257            draft.the_actor,
258            "TheActor",
259            "IfcActorSelect",
260            ACTOR_SELECT,
261            draft.the_actor,
262        )?;
263        if let Some(value) = draft.predefined_type {
264            validate_enum(self.schema, entity_type, "PredefinedType", value)?;
265            if value == "USERDEFINED"
266                && draft
267                    .object_type
268                    .is_none_or(|value| value.trim().is_empty())
269            {
270                return Err(ResourceError::SemanticViolation {
271                    entity: None,
272                    rule: "USERDEFINED_REQUIRES_OBJECT_TYPE",
273                });
274            }
275        }
276        let entity = build_entity(
277            self.schema,
278            entity_type,
279            &[
280                ("GlobalId", Some(text(draft.global_id))),
281                ("Name", draft.name.map(text)),
282                ("Description", draft.description.map(text)),
283                ("ObjectType", draft.object_type.map(text)),
284                ("TheActor", Some(Value::Ref(draft.the_actor))),
285                (
286                    "PredefinedType",
287                    draft.predefined_type.map(|t| Value::Enum(t.into())),
288                ),
289            ],
290        )?;
291        self.commit_create(entity)
292    }
293
294    /// Stage an `IfcAsset`.
295    ///
296    /// The three value slots are `IfcCostValue` references and the
297    /// two party slots are `IfcActorSelect`; each is checked before
298    /// staging so an asset cannot claim a value or an owner that the
299    /// model does not hold.
300    ///
301    /// # Errors
302    ///
303    /// Refuses a malformed or duplicate GlobalId, a value reference
304    /// that is not an `IfcCostValue`, a party outside `IfcActorSelect`,
305    /// and a responsible person that is not an `IfcPerson`.
306    pub fn create_asset(&mut self, draft: AssetDraft<'_>) -> ResourceResult<EntityId> {
307        self.validate_new_global_id(draft.global_id)?;
308        for (attribute, target) in [
309            ("OriginalValue", draft.original_value),
310            ("CurrentValue", draft.current_value),
311            ("TotalReplacementCost", draft.total_replacement_cost),
312            ("DepreciatedValue", draft.depreciated_value),
313        ] {
314            if let Some(target) = target {
315                self.check_reference(target, attribute, "IfcCostValue", target)?;
316            }
317        }
318        for (attribute, target) in [("Owner", draft.owner), ("User", draft.user)] {
319            if let Some(target) = target {
320                self.check_reference_select(
321                    target,
322                    attribute,
323                    "IfcActorSelect",
324                    ACTOR_SELECT,
325                    target,
326                )?;
327            }
328        }
329        if let Some(person) = draft.responsible_person {
330            self.check_reference(person, "ResponsiblePerson", "IfcPerson", person)?;
331        }
332        let entity = build_entity(
333            self.schema,
334            "IfcAsset",
335            &[
336                ("GlobalId", Some(text(draft.global_id))),
337                ("Name", draft.name.map(text)),
338                ("Description", draft.description.map(text)),
339                ("ObjectType", draft.object_type.map(text)),
340                ("Identification", draft.identification.map(text)),
341                ("OriginalValue", draft.original_value.map(Value::Ref)),
342                ("CurrentValue", draft.current_value.map(Value::Ref)),
343                (
344                    "TotalReplacementCost",
345                    draft.total_replacement_cost.map(Value::Ref),
346                ),
347                ("Owner", draft.owner.map(Value::Ref)),
348                ("User", draft.user.map(Value::Ref)),
349                (
350                    "ResponsiblePerson",
351                    draft.responsible_person.map(Value::Ref),
352                ),
353                ("IncorporationDate", draft.incorporation_date.map(text)),
354                ("DepreciatedValue", draft.depreciated_value.map(Value::Ref)),
355            ],
356        )?;
357        self.commit_create(entity)
358    }
359
360    /// Stage an `IfcInventory`.
361    ///
362    /// # Errors
363    ///
364    /// Refuses a duplicate or malformed GlobalId, a `jurisdiction`
365    /// outside `IfcActorSelect`, a value that is not an `IfcCostValue`,
366    /// a `responsible_persons` entry that is not an `IfcPerson`, an
367    /// empty person set (the schema bounds it `SET [1:?]`), and
368    /// `USERDEFINED` without an `object_type`.
369    pub fn create_inventory(
370        &mut self,
371        draft: InventoryDraft<'_>,
372        responsible_persons: &[EntityId],
373    ) -> ResourceResult<EntityId> {
374        const ENTITY: &str = "IfcInventory";
375        self.validate_new_global_id(draft.global_id)?;
376        if let Some(token) = draft.predefined_type {
377            validate_enum(self.schema, ENTITY, "PredefinedType", token)?;
378            if token.eq_ignore_ascii_case("USERDEFINED")
379                && draft.object_type.is_none_or(|text| text.trim().is_empty())
380            {
381                return Err(ResourceError::SemanticViolation {
382                    entity: None,
383                    rule: "USERDEFINED_REQUIRES_OBJECT_TYPE",
384                });
385            }
386        }
387        if let Some(target) = draft.jurisdiction {
388            self.check_reference_select(
389                target,
390                "Jurisdiction",
391                "IfcActorSelect",
392                ACTOR_SELECT,
393                target,
394            )?;
395        }
396        for (attribute, target) in [
397            ("CurrentValue", draft.current_value),
398            ("OriginalValue", draft.original_value),
399        ] {
400            if let Some(target) = target {
401                self.check_reference(target, attribute, "IfcCostValue", target)?;
402            }
403        }
404        // SET [1:?]: an inventory nobody is responsible for is a
405        // record with no owner, which the schema does not allow to be
406        // stated as an empty set.
407        if responsible_persons.is_empty() {
408            return Err(ResourceError::InvalidDraft {
409                entity_type: ENTITY,
410                attribute: "ResponsiblePersons",
411                expected: "at least one person, per SET [1:?]",
412            });
413        }
414        for person in responsible_persons {
415            self.check_reference(*person, "ResponsiblePersons", "IfcPerson", *person)?;
416        }
417        let entity = build_entity(
418            self.schema,
419            ENTITY,
420            &[
421                ("GlobalId", Some(text(draft.global_id))),
422                ("Name", draft.name.map(text)),
423                ("Description", draft.description.map(text)),
424                ("ObjectType", draft.object_type.map(text)),
425                (
426                    "PredefinedType",
427                    draft.predefined_type.map(|t| Value::Enum(t.into())),
428                ),
429                ("Jurisdiction", draft.jurisdiction.map(Value::Ref)),
430                ("ResponsiblePersons", Some(refs(responsible_persons))),
431                ("LastUpdateDate", draft.last_update_date.map(text)),
432                ("CurrentValue", draft.current_value.map(Value::Ref)),
433                ("OriginalValue", draft.original_value.map(Value::Ref)),
434            ],
435        )?;
436        self.commit_create(entity)
437    }
438}
439
440/// Draft for one `IfcInventory`: a counted collection of things.
441#[derive(Debug, Clone, Copy, Default)]
442#[non_exhaustive]
443pub struct InventoryDraft<'a> {
444    /// `GlobalId`.
445    pub global_id: &'a str,
446    /// `Name`.
447    pub name: Option<&'a str>,
448    /// `Description`.
449    pub description: Option<&'a str>,
450    /// `ObjectType`. Required when `predefined_type` is `USERDEFINED`.
451    pub object_type: Option<&'a str>,
452    /// `PredefinedType`, an `IfcInventoryTypeEnum` token.
453    pub predefined_type: Option<&'a str>,
454    /// `Jurisdiction`, an `IfcActorSelect`.
455    pub jurisdiction: Option<EntityId>,
456    /// `LastUpdateDate`, an ISO 8601 date written as given.
457    pub last_update_date: Option<&'a str>,
458    /// `CurrentValue`, an `IfcCostValue`.
459    pub current_value: Option<EntityId>,
460    /// `OriginalValue`, an `IfcCostValue`.
461    pub original_value: Option<EntityId>,
462}
463
464impl<'a> InventoryDraft<'a> {
465    /// Starts a draft with its required fields; the rest are unset.
466    #[must_use]
467    pub fn new(global_id: &'a str) -> Self {
468        Self {
469            global_id,
470            name: None,
471            description: None,
472            object_type: None,
473            predefined_type: None,
474            jurisdiction: None,
475            last_update_date: None,
476            current_value: None,
477            original_value: None,
478        }
479    }
480
481    /// Sets [`Self::name`]: `Name`.
482    #[must_use]
483    pub fn name(mut self, value: &'a str) -> Self {
484        self.name = Some(value);
485        self
486    }
487
488    /// Sets [`Self::description`]: `Description`.
489    #[must_use]
490    pub fn description(mut self, value: &'a str) -> Self {
491        self.description = Some(value);
492        self
493    }
494
495    /// Sets [`Self::object_type`]: `ObjectType`. Required when `predefined_type` is `USERDEFINED`.
496    #[must_use]
497    pub fn object_type(mut self, value: &'a str) -> Self {
498        self.object_type = Some(value);
499        self
500    }
501
502    /// Sets [`Self::predefined_type`]: `PredefinedType`, an `IfcInventoryTypeEnum` token.
503    #[must_use]
504    pub fn predefined_type(mut self, value: &'a str) -> Self {
505        self.predefined_type = Some(value);
506        self
507    }
508
509    /// Sets [`Self::jurisdiction`]: `Jurisdiction`, an `IfcActorSelect`.
510    #[must_use]
511    pub fn jurisdiction(mut self, value: EntityId) -> Self {
512        self.jurisdiction = Some(value);
513        self
514    }
515
516    /// Sets [`Self::last_update_date`]: `LastUpdateDate`, an ISO 8601 date written as given.
517    #[must_use]
518    pub fn last_update_date(mut self, value: &'a str) -> Self {
519        self.last_update_date = Some(value);
520        self
521    }
522
523    /// Sets [`Self::current_value`]: `CurrentValue`, an `IfcCostValue`.
524    #[must_use]
525    pub fn current_value(mut self, value: EntityId) -> Self {
526        self.current_value = Some(value);
527        self
528    }
529
530    /// Sets [`Self::original_value`]: `OriginalValue`, an `IfcCostValue`.
531    #[must_use]
532    pub fn original_value(mut self, value: EntityId) -> Self {
533        self.original_value = Some(value);
534        self
535    }
536}