ifc_occurrence/authoring.rs
1//! Authoring built-element and distribution occurrences.
2//!
3//! # The type pairing is the interesting rule
4//!
5//! `CorrectPredefinedType` repeats what the type catalogue already
6//! enforces. `CorrectTypeAssigned` does not: it says an occurrence
7//! may be typed by at most one type, and that type must be the single
8//! class the schema pairs with it. An `IfcPump` typed by an
9//! `IfcValveType` is not a pump with an unusual type, it is a
10//! contradiction: the occurrence claims to be a pump while its shared
11//! definition describes a valve.
12//!
13//! The writer therefore resolves the referenced type entity and
14//! compares its STEP class against the pairing recorded in the
15//! catalogue. That costs a model lookup, which is why `create` takes
16//! a `&Model`: a pairing that is not checked against the real entity
17//! is not checked at all.
18//!
19//! # The release decides the layout
20//!
21//! The same `&Model` names the release the occurrence is written in
22//! (#202). Slots, the `PredefinedType` enumeration and the required
23//! attributes come from that release's table by attribute name, never
24//! from the IFC4X3 catalogue row; see `release.rs`.
25
26use ifc_model::guid::Guid;
27use ifc_model::{EntityId, Model, Transaction, Value};
28use ifc_schema::SchemaVersion;
29
30use crate::release::{bind, require_owner_history};
31use crate::table::Occurrence;
32
33/// Why an occurrence was refused.
34#[derive(Debug, Clone, PartialEq, Eq)]
35#[non_exhaustive]
36pub enum OccurrenceError {
37 /// `GlobalId` did not parse as a 22-character IFC GUID.
38 MalformedGuid {
39 /// The offending value.
40 value: String,
41 },
42 /// The token is not a member of this class's own enum.
43 UnknownPredefinedType {
44 /// STEP class.
45 entity: &'static str,
46 /// The offending token.
47 token: String,
48 },
49 /// The class has no `PredefinedType` attribute at all.
50 NoPredefinedType {
51 /// STEP class.
52 entity: &'static str,
53 },
54 /// `USERDEFINED` was given without an `ObjectType` naming it.
55 UserDefinedWithoutObjectType {
56 /// STEP class.
57 entity: &'static str,
58 },
59 /// The occurrence was typed by a class the schema does not pair with it.
60 WrongTypeClass {
61 /// STEP class of the occurrence.
62 entity: &'static str,
63 /// The only class `CorrectTypeAssigned` permits.
64 expected: &'static str,
65 /// What the referenced entity actually is.
66 found: String,
67 },
68 /// The class permits no type at all, but one was supplied.
69 TypingNotPermitted {
70 /// STEP class.
71 entity: &'static str,
72 },
73 /// The typed-by reference does not resolve in the model.
74 UnresolvedType {
75 /// The dangling id.
76 id: EntityId,
77 },
78 /// The model's header declares several schemas; authoring binds to
79 /// exactly one release.
80 MultipleSchemas {
81 /// Number of `FILE_SCHEMA` declarations.
82 schemas: usize,
83 },
84 /// The model's header declares one schema with no bundled table, so no
85 /// layout can be trusted.
86 UnsupportedSchema {
87 /// The `FILE_SCHEMA` token as written.
88 schema: String,
89 },
90 /// The model's release does not declare the class, or declares it
91 /// abstract, such as `IfcBorehole` (IFC4X3 only) in an IFC4 model.
92 EntityNotInSchema {
93 /// STEP class.
94 entity: &'static str,
95 /// The release the model declares.
96 schema: SchemaVersion,
97 },
98 /// A value for an attribute the model's release does not declare. It is
99 /// refused rather than dropped.
100 AuthoringNotInSchema {
101 /// STEP class.
102 entity: &'static str,
103 /// The attribute.
104 attribute: &'static str,
105 /// The release the model declares.
106 schema: SchemaVersion,
107 },
108 /// The model's release requires an attribute the call leaves unset,
109 /// such as the IFC2X3 `IfcRoot.OwnerHistory`.
110 AuthoringRequired {
111 /// STEP class.
112 entity: &'static str,
113 /// The required attribute, as the release names it.
114 attribute: &'static str,
115 /// The release the model declares.
116 schema: SchemaVersion,
117 },
118 /// The owner-history reference resolves neither in the model nor on the
119 /// transaction.
120 UnresolvedOwnerHistory {
121 /// The dangling id.
122 id: EntityId,
123 },
124 /// The owner-history reference is not an `IfcOwnerHistory`.
125 NotAnOwnerHistory {
126 /// The referenced id.
127 id: EntityId,
128 /// What the referenced entity actually is.
129 found: String,
130 },
131}
132
133/// Result alias for this crate.
134pub type OccurrenceResult<T> = Result<T, OccurrenceError>;
135
136/// Attributes shared by every occurrence.
137#[derive(Debug, Clone, Copy, Default)]
138pub struct OccurrenceDraft<'a> {
139 /// `Name`.
140 pub name: Option<&'a str>,
141 /// `Description`.
142 pub description: Option<&'a str>,
143 /// `ObjectType`; required when `PredefinedType` is `USERDEFINED`.
144 pub object_type: Option<&'a str>,
145 /// `ObjectPlacement`.
146 pub placement: Option<EntityId>,
147 /// `Representation`.
148 pub representation: Option<EntityId>,
149 /// `Tag`, slot 7 on every class in this catalogue.
150 pub tag: Option<&'a str>,
151}
152
153fn text(v: Option<&str>) -> Value {
154 v.map_or(Value::Null, |s| Value::Text(s.into()))
155}
156
157fn slot_ref(v: Option<EntityId>) -> Value {
158 v.map_or(Value::Null, Value::Ref)
159}
160
161/// Stage one occurrence.
162///
163/// `typed_by` is the `IfcTypeObject` this occurrence takes its shared
164/// definition from, if any. It is checked against the one class the
165/// schema pairs with `kind`; see [`OccurrenceError::WrongTypeClass`].
166///
167/// Refuses a malformed `global_id`, a `predefined_type` outside the
168/// class enum, `USERDEFINED` without `draft.object_type`, and a
169/// `typed_by` of the wrong class or one that does not resolve.
170///
171/// # Release
172///
173/// Written in the model's declared release (#202), laid out by attribute
174/// name from its table; a header without `FILE_SCHEMA` binds IFC4.
175/// `predefined_type` is checked against that release's enumeration, and a
176/// class the release does not declare is refused with
177/// [`OccurrenceError::EntityNotInSchema`]. `OwnerHistory` is left `$`,
178/// which IFC4 and IFC4X3 allow and IFC2X3 does not, so an IFC2X3 model is
179/// refused with [`OccurrenceError::AuthoringRequired`]; use
180/// [`create_with_owner_history`] there. Any other attribute the release
181/// requires and the draft cannot carry (IFC2X3 `IfcStair.ShapeType`, for
182/// one) is refused the same way. The type pairing is the IFC4X3
183/// `CorrectTypeAssigned` class of the catalogue row in every release.
184///
185/// # Errors
186///
187/// The refusals above; [`OccurrenceError::MultipleSchemas`] or
188/// [`OccurrenceError::UnsupportedSchema`] for a model that binds no single
189/// known release. Nothing is staged on an error.
190pub fn create(
191 tx: &mut Transaction,
192 model: &Model,
193 kind: Occurrence,
194 global_id: &str,
195 predefined_type: Option<&str>,
196 typed_by: Option<EntityId>,
197 draft: OccurrenceDraft<'_>,
198) -> OccurrenceResult<EntityId> {
199 let request = Request {
200 kind,
201 global_id,
202 predefined_type,
203 typed_by,
204 draft,
205 };
206 author(tx, model, request, None)
207}
208
209/// [`create`] with a caller-supplied `IfcOwnerHistory`, which IFC2X3
210/// requires on every `IfcRoot`.
211///
212/// `owner_history` must be in the model or staged earlier on `tx`, and must
213/// be an `IfcOwnerHistory`; one is never invented here (build it with
214/// `ifc-author`). In IFC4 and IFC4X3 the reference fills the optional slot.
215///
216/// # Errors
217///
218/// Those of [`create`] except the IFC2X3 `OwnerHistory` refusal, and
219/// [`OccurrenceError::UnresolvedOwnerHistory`] or
220/// [`OccurrenceError::NotAnOwnerHistory`] for an `owner_history` that does
221/// not resolve or is another entity. Nothing is staged on an error.
222#[allow(clippy::too_many_arguments)]
223pub fn create_with_owner_history(
224 tx: &mut Transaction,
225 model: &Model,
226 kind: Occurrence,
227 global_id: &str,
228 predefined_type: Option<&str>,
229 typed_by: Option<EntityId>,
230 draft: OccurrenceDraft<'_>,
231 owner_history: EntityId,
232) -> OccurrenceResult<EntityId> {
233 let request = Request {
234 kind,
235 global_id,
236 predefined_type,
237 typed_by,
238 draft,
239 };
240 author(tx, model, request, Some(owner_history))
241}
242
243/// The caller's arguments, bundled.
244struct Request<'a> {
245 kind: Occurrence,
246 global_id: &'a str,
247 predefined_type: Option<&'a str>,
248 typed_by: Option<EntityId>,
249 draft: OccurrenceDraft<'a>,
250}
251
252/// Stage one occurrence; `None` leaves `OwnerHistory` `$`.
253fn author(
254 tx: &mut Transaction,
255 model: &Model,
256 request: Request<'_>,
257 owner_history: Option<EntityId>,
258) -> OccurrenceResult<EntityId> {
259 let Request {
260 kind,
261 global_id,
262 predefined_type,
263 typed_by,
264 draft,
265 } = request;
266 let entity = kind.type_name;
267 if Guid::parse(global_id).is_none() {
268 return Err(OccurrenceError::MalformedGuid {
269 value: global_id.into(),
270 });
271 }
272 let layout = bind(model)?;
273 layout.require_entity(entity)?;
274 if let Some(token) = predefined_type {
275 let Some(members) = layout.predefined_members(entity) else {
276 return Err(OccurrenceError::NoPredefinedType { entity });
277 };
278 if !members.contains(&token) {
279 return Err(OccurrenceError::UnknownPredefinedType {
280 entity,
281 token: token.into(),
282 });
283 }
284 if token == "USERDEFINED" && draft.object_type.is_none_or(|s| s.trim().is_empty()) {
285 return Err(OccurrenceError::UserDefinedWithoutObjectType { entity });
286 }
287 }
288
289 if let Some(id) = typed_by {
290 let Some(expected) = kind.type_class else {
291 return Err(OccurrenceError::TypingNotPermitted { entity });
292 };
293 let found = model
294 .get(id)
295 .ok_or(OccurrenceError::UnresolvedType { id })?
296 .type_name
297 .to_ascii_uppercase();
298 if found != expected {
299 return Err(OccurrenceError::WrongTypeClass {
300 entity,
301 expected,
302 found,
303 });
304 }
305 }
306
307 if let Some(owner_history) = owner_history {
308 require_owner_history(tx, model, owner_history)?;
309 }
310 let record = layout.named_record(
311 entity,
312 vec![
313 ("GlobalId", Value::Text(global_id.into())),
314 ("OwnerHistory", slot_ref(owner_history)),
315 ("Name", text(draft.name)),
316 ("Description", text(draft.description)),
317 ("ObjectType", text(draft.object_type)),
318 ("ObjectPlacement", slot_ref(draft.placement)),
319 ("Representation", slot_ref(draft.representation)),
320 ("Tag", text(draft.tag)),
321 (
322 "PredefinedType",
323 predefined_type.map_or(Value::Null, |token| Value::Enum(token.into())),
324 ),
325 ],
326 )?;
327 Ok(tx.create(record))
328}