Skip to main content

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}