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}