Skip to main content

ifc_element_type/
authoring.rs

1//! Authoring element, resource, and process type definitions.
2//!
3//! # What a type definition is for
4//!
5//! An `IfcXxxType` carries what every occurrence of a product shares:
6//! a door type names the operation and panel layout that each door
7//! placed from it inherits. Authoring one wrong does not corrupt a
8//! single door; it corrupts every door of that type.
9//!
10//! # The rule this module exists to enforce
11//!
12//! All 132 types carry `CorrectPredefinedType`:
13//!
14//! ```text
15//! (PredefinedType <> USERDEFINED) OR
16//! ((PredefinedType = USERDEFINED) AND EXISTS(<fallback>))
17//! ```
18//!
19//! `USERDEFINED` means "the enum has no token for this, the name is
20//! given elsewhere". Without that elsewhere the value asserts a name
21//! exists and then withholds it, which no reader can resolve.
22//!
23//! The same holds for `IfcEventType.CorrectEventTriggerType`, stated over
24//! `EventTriggerType` and `UserDefinedEventTriggerType`.
25//!
26//! This module is stricter than `EXISTS`: a blank fallback string satisfies
27//! EXPRESS but names nothing, so it is refused too. And the writer takes an
28//! [`ElementType`] from the catalogue rather than a type-name string, so an
29//! entity the catalogue does not know cannot be written at all.
30//!
31//! # Which release is written
32//!
33//! [`create_type`] takes no model and writes the catalogue's IFC4X3
34//! layout. [`create_type_in`] and [`create_type_with_owner_history`] write
35//! the model's declared release (#202): see `release.rs`.
36
37use ifc_model::guid::Guid;
38use ifc_model::{EntityId, Model, Transaction, Value};
39
40use crate::error::{ElementTypeError, ElementTypeResult};
41use crate::release::{bind, require_owner_history, Layout};
42use crate::table::{ElementType, Family};
43
44pub(crate) fn invalid(
45    entity: &'static str,
46    attribute: &'static str,
47    value: impl Into<String>,
48) -> ElementTypeError {
49    ElementTypeError::Invalid {
50        entity,
51        attribute,
52        value: value.into(),
53    }
54}
55
56/// Attributes of a type definition: those every type shares, and the few
57/// one type requires on top.
58///
59/// `tag_or_long_description` and `maps_or_identification` occupy slots
60/// 7 and 6, whose meaning depends on [`Family`]. Naming them for both
61/// readings keeps a caller from assuming the element-type reading on a
62/// resource type, where it would file a tag as a description.
63///
64/// The type-specific fields (#214) carry the attributes IFC4 and IFC4X3
65/// require on four types, as `references/ifc-spec` declares them:
66///
67/// ```text
68/// IfcDoorType       OperationType    : IfcDoorTypeOperationEnum;
69/// IfcWindowType     PartitioningType : IfcWindowTypePartitioningEnum;
70/// IfcEventType      EventTriggerType : IfcEventTriggerTypeEnum;
71/// IfcFurnitureType  AssemblyPlace    : IfcAssemblyPlaceEnum;  (IFC2X3 too)
72/// ```
73///
74/// A value for an attribute the type does not declare in the bound release
75/// is refused, never dropped.
76///
77/// The struct is `#[non_exhaustive]`: build it with [`TypeDraft::new`] and
78/// the setters, so a field a later release needs can be added without
79/// breaking callers.
80#[derive(Debug, Clone, Copy, Default)]
81#[non_exhaustive]
82pub struct TypeDraft<'a> {
83    /// `Name`.
84    pub name: Option<&'a str>,
85    /// `Description`.
86    pub description: Option<&'a str>,
87    /// `ApplicableOccurrence`, slot 4.
88    pub applicable_occurrence: Option<&'a str>,
89    /// Slot 6: `RepresentationMaps` refs, or `Identification` text.
90    pub maps_or_identification: Option<Slot6<'a>>,
91    /// Slot 7: `Tag` on element types, `LongDescription` otherwise.
92    pub tag_or_long_description: Option<&'a str>,
93    /// Slot 8: the `USERDEFINED` fallback. Required when the
94    /// predefined type is `USERDEFINED`.
95    pub fallback: Option<&'a str>,
96    /// `IfcDoorType.OperationType`, an `IfcDoorTypeOperationEnum` token;
97    /// required on `IfcDoorType`.
98    pub operation_type: Option<&'a str>,
99    /// `IfcDoorType.UserDefinedOperationType`.
100    pub user_defined_operation_type: Option<&'a str>,
101    /// `IfcWindowType.PartitioningType`, an `IfcWindowTypePartitioningEnum`
102    /// token; required on `IfcWindowType`.
103    pub partitioning_type: Option<&'a str>,
104    /// `IfcWindowType.UserDefinedPartitioningType`.
105    pub user_defined_partitioning_type: Option<&'a str>,
106    /// `ParameterTakesPrecedence` on `IfcDoorType` and `IfcWindowType`.
107    pub parameter_takes_precedence: Option<bool>,
108    /// `IfcEventType.EventTriggerType`, an `IfcEventTriggerTypeEnum` token;
109    /// required on `IfcEventType`.
110    pub event_trigger_type: Option<&'a str>,
111    /// `IfcEventType.UserDefinedEventTriggerType`. Required when
112    /// `event_trigger_type` is `USERDEFINED` (`CorrectEventTriggerType`).
113    pub user_defined_event_trigger_type: Option<&'a str>,
114    /// `IfcFurnitureType.AssemblyPlace`, an `IfcAssemblyPlaceEnum` token;
115    /// required on `IfcFurnitureType`.
116    pub assembly_place: Option<&'a str>,
117}
118
119impl<'a> TypeDraft<'a> {
120    /// Starts an empty draft with every attribute unset.
121    #[must_use]
122    pub fn new() -> Self {
123        Self::default()
124    }
125
126    /// Sets `Name`, which `NameRequired` makes mandatory.
127    #[must_use]
128    pub fn name(mut self, value: &'a str) -> Self {
129        self.name = Some(value);
130        self
131    }
132
133    /// Sets `Description`.
134    #[must_use]
135    pub fn description(mut self, value: &'a str) -> Self {
136        self.description = Some(value);
137        self
138    }
139
140    /// Sets `ApplicableOccurrence`.
141    #[must_use]
142    pub fn applicable_occurrence(mut self, value: &'a str) -> Self {
143        self.applicable_occurrence = Some(value);
144        self
145    }
146
147    /// Sets slot 6: `RepresentationMaps` or `Identification`.
148    #[must_use]
149    pub fn maps_or_identification(mut self, value: Slot6<'a>) -> Self {
150        self.maps_or_identification = Some(value);
151        self
152    }
153
154    /// Sets slot 7: `Tag` or `LongDescription`.
155    #[must_use]
156    pub fn tag_or_long_description(mut self, value: &'a str) -> Self {
157        self.tag_or_long_description = Some(value);
158        self
159    }
160
161    /// Sets the slot-8 `USERDEFINED` fallback.
162    #[must_use]
163    pub fn fallback(mut self, value: &'a str) -> Self {
164        self.fallback = Some(value);
165        self
166    }
167
168    /// Sets `IfcDoorType.OperationType`.
169    #[must_use]
170    pub fn operation_type(mut self, value: &'a str) -> Self {
171        self.operation_type = Some(value);
172        self
173    }
174
175    /// Sets `IfcDoorType.UserDefinedOperationType`.
176    #[must_use]
177    pub fn user_defined_operation_type(mut self, value: &'a str) -> Self {
178        self.user_defined_operation_type = Some(value);
179        self
180    }
181
182    /// Sets `IfcWindowType.PartitioningType`.
183    #[must_use]
184    pub fn partitioning_type(mut self, value: &'a str) -> Self {
185        self.partitioning_type = Some(value);
186        self
187    }
188
189    /// Sets `IfcWindowType.UserDefinedPartitioningType`.
190    #[must_use]
191    pub fn user_defined_partitioning_type(mut self, value: &'a str) -> Self {
192        self.user_defined_partitioning_type = Some(value);
193        self
194    }
195
196    /// Sets `ParameterTakesPrecedence` (`IfcDoorType`, `IfcWindowType`).
197    #[must_use]
198    pub fn parameter_takes_precedence(mut self, value: bool) -> Self {
199        self.parameter_takes_precedence = Some(value);
200        self
201    }
202
203    /// Sets `IfcEventType.EventTriggerType`.
204    #[must_use]
205    pub fn event_trigger_type(mut self, value: &'a str) -> Self {
206        self.event_trigger_type = Some(value);
207        self
208    }
209
210    /// Sets `IfcEventType.UserDefinedEventTriggerType`.
211    #[must_use]
212    pub fn user_defined_event_trigger_type(mut self, value: &'a str) -> Self {
213        self.user_defined_event_trigger_type = Some(value);
214        self
215    }
216
217    /// Sets `IfcFurnitureType.AssemblyPlace`.
218    #[must_use]
219    pub fn assembly_place(mut self, value: &'a str) -> Self {
220        self.assembly_place = Some(value);
221        self
222    }
223}
224
225/// What slot 6 holds, which differs by [`Family`].
226#[derive(Debug, Clone, Copy)]
227#[non_exhaustive]
228pub enum Slot6<'a> {
229    /// `RepresentationMaps`: shape definitions the occurrences map.
230    RepresentationMaps(&'a [EntityId]),
231    /// `Identification`: a catalogue or article number.
232    Identification(&'a str),
233}
234
235/// Stage one type definition.
236///
237/// `predefined_type` must be a token the entity's own enum declares.
238/// A token borrowed from a sibling enum is refused: `IfcPumpTypeEnum`
239/// has no `SUBMERSIBLEPUMP` member merely because some other pump-like
240/// enum does.
241///
242/// # Release
243///
244/// Takes no model, so it writes the catalogue's IFC4X3 layout with
245/// `OwnerHistory` `$`, and cannot refuse a model that declares another
246/// release. That record is valid in IFC4X3 and, where IFC4 declares the
247/// type with the same layout and token, in IFC4; it is never valid IFC2X3,
248/// which requires `OwnerHistory`. Use [`create_type_in`] or
249/// [`create_type_with_owner_history`] to write the model's declared
250/// release.
251///
252/// # Errors
253///
254/// Refuses a malformed GlobalId, a token outside the entity's enum, a
255/// missing predefined type where the schema requires one, `USERDEFINED`
256/// without the fallback attribute, and a slot-6 value of the wrong
257/// shape for the entity's family. A type-specific token outside its
258/// enumeration is [`ElementTypeError::Invalid`], one for an attribute the
259/// type does not declare [`ElementTypeError::AuthoringNotInSchema`], and
260/// `USERDEFINED` `event_trigger_type` without
261/// `user_defined_event_trigger_type` is refused (`CorrectEventTriggerType`).
262/// A required attribute left unset, such as `IfcDoorType.OperationType` or
263/// `IfcFurnitureType.AssemblyPlace`, is
264/// [`ElementTypeError::AuthoringRequired`], never written `$` (#214).
265/// Nothing is staged on an error.
266pub fn create_type(
267    tx: &mut Transaction,
268    kind: ElementType,
269    global_id: &str,
270    predefined_type: Option<&str>,
271    draft: TypeDraft<'_>,
272) -> ElementTypeResult<EntityId> {
273    let request = Request {
274        kind,
275        global_id,
276        predefined_type,
277        draft,
278    };
279    author(tx, Layout::catalogue()?, request, None)
280}
281
282/// [`create_type`] in the model's declared release (#202).
283///
284/// The record is laid out by attribute name from that release's table, and
285/// `predefined_type` is checked against that release's enumeration.
286/// `OwnerHistory` is left `$`, which IFC4 and IFC4X3 allow and IFC2X3 does
287/// not, so an IFC2X3 model is refused with
288/// [`ElementTypeError::AuthoringRequired`]; use
289/// [`create_type_with_owner_history`] there. A header without
290/// `FILE_SCHEMA` binds IFC4.
291///
292/// # Errors
293///
294/// Those of [`create_type`], checked against the bound release, and:
295/// [`ElementTypeError::MultipleSchemas`] or
296/// [`ElementTypeError::UnsupportedSchema`] for a model that binds no single
297/// known release; [`ElementTypeError::EntityNotInSchema`] for a type the
298/// release does not declare (IFC2X3 has no `IfcDoorType`, IFC4 no
299/// `IfcBearingType`); [`ElementTypeError::AuthoringNotInSchema`] for a
300/// token where the release declares no `PredefinedType`, and
301/// [`ElementTypeError::AuthoringRequired`] for any other attribute the
302/// release requires that the draft leaves unset. Nothing is staged on an
303/// error.
304pub fn create_type_in(
305    tx: &mut Transaction,
306    model: &Model,
307    kind: ElementType,
308    global_id: &str,
309    predefined_type: Option<&str>,
310    draft: TypeDraft<'_>,
311) -> ElementTypeResult<EntityId> {
312    let request = Request {
313        kind,
314        global_id,
315        predefined_type,
316        draft,
317    };
318    author(tx, bind(model)?, request, None)
319}
320
321/// [`create_type_in`] with a caller-supplied `IfcOwnerHistory`, which
322/// IFC2X3 requires on every `IfcRoot`.
323///
324/// `owner_history` must be in the model or staged earlier on `tx`, and must
325/// be an `IfcOwnerHistory`; one is never invented here (build it with
326/// `ifc-author`). In IFC4 and IFC4X3 the reference fills the optional slot.
327///
328/// # Errors
329///
330/// Those of [`create_type_in`] except the IFC2X3 `OwnerHistory` refusal,
331/// and [`ElementTypeError::MissingEntity`] for an `owner_history` that does
332/// not resolve or [`ElementTypeError::Invalid`] on `OwnerHistory` for one
333/// that is another entity. Nothing is staged on an error.
334pub fn create_type_with_owner_history(
335    tx: &mut Transaction,
336    model: &Model,
337    kind: ElementType,
338    global_id: &str,
339    predefined_type: Option<&str>,
340    draft: TypeDraft<'_>,
341    owner_history: EntityId,
342) -> ElementTypeResult<EntityId> {
343    let layout = bind(model)?;
344    // Checked before the draft so a wrong reference is reported as such.
345    require_owner_history(tx, model, kind.type_name, owner_history)?;
346    let request = Request {
347        kind,
348        global_id,
349        predefined_type,
350        draft,
351    };
352    author(tx, layout, request, Some(owner_history))
353}
354
355/// The caller's arguments, bundled.
356struct Request<'a> {
357    kind: ElementType,
358    global_id: &'a str,
359    predefined_type: Option<&'a str>,
360    draft: TypeDraft<'a>,
361}
362
363/// Stage one type definition in `layout`; `None` leaves `OwnerHistory` `$`.
364/// The owner history, if any, has been checked by the caller.
365fn author(
366    tx: &mut Transaction,
367    layout: Layout,
368    request: Request<'_>,
369    owner_history: Option<EntityId>,
370) -> ElementTypeResult<EntityId> {
371    let Request {
372        kind,
373        global_id,
374        predefined_type,
375        draft,
376    } = request;
377    let entity = kind.type_name;
378    if Guid::parse(global_id).is_none() {
379        return Err(invalid(entity, "GlobalId", global_id));
380    }
381    // `IfcTypeObject.NameRequired` is inherited by all 132 catalogue
382    // types. `Name` is OPTIONAL in the slot table and mandatory by
383    // rule, so a writer trusting the slot table alone files a nameless
384    // type that parses and cannot be referred to.
385    if blank(draft.name) {
386        return Err(invalid(entity, "Name", "NameRequired"));
387    }
388    layout.require_entity(entity)?;
389
390    // The release's own `PredefinedType`: whether it is required, and its
391    // tokens. For the catalogue's release these are the row's own.
392    match layout.attribute(entity, "PredefinedType") {
393        None if predefined_type.is_some() => {
394            return Err(ElementTypeError::AuthoringNotInSchema {
395                entity,
396                attribute: "PredefinedType",
397                schema: layout.version(),
398            });
399        }
400        None => {}
401        Some(declared) => {
402            let members = layout.members(entity, "PredefinedType").unwrap_or_default();
403            match predefined_type {
404                None if !declared.optional => {
405                    return Err(invalid(entity, "PredefinedType", "required"));
406                }
407                Some(token) if !members.contains(&token) => {
408                    return Err(invalid(entity, "PredefinedType", token));
409                }
410                _ => {}
411            }
412        }
413    }
414    if predefined_type == Some("USERDEFINED") && blank(draft.fallback) {
415        return Err(invalid(
416            entity,
417            kind.fallback_attr,
418            "required by USERDEFINED",
419        ));
420    }
421
422    let (slot6_name, slot6) = match (draft.maps_or_identification, kind.family) {
423        (Some(Slot6::RepresentationMaps(maps)), Family::Element) => {
424            if maps.is_empty() {
425                return Err(invalid(entity, "RepresentationMaps", "empty"));
426            }
427            (
428                "RepresentationMaps",
429                Value::List(maps.iter().copied().map(Value::Ref).collect()),
430            )
431        }
432        (Some(Slot6::Identification(id)), Family::ResourceOrProcess) => {
433            ("Identification", Value::Text(id.into()))
434        }
435        (Some(Slot6::RepresentationMaps(_)), Family::ResourceOrProcess) => {
436            return Err(invalid(entity, "Identification", "expected text, got maps"));
437        }
438        (Some(Slot6::Identification(_)), Family::Element) => {
439            return Err(invalid(
440                entity,
441                "RepresentationMaps",
442                "expected maps, got text",
443            ));
444        }
445        (None, _) => ("RepresentationMaps", Value::Null),
446    };
447    let slot7_name = match kind.family {
448        Family::Element => "Tag",
449        Family::ResourceOrProcess => "LongDescription",
450    };
451
452    let specific = specific_values(layout, entity, &draft)?;
453    let mut values = vec![
454        ("GlobalId", Value::Text(global_id.into())),
455        (
456            "OwnerHistory",
457            owner_history.map_or(Value::Null, Value::Ref),
458        ),
459        ("Name", text(draft.name)),
460        ("Description", text(draft.description)),
461        ("ApplicableOccurrence", text(draft.applicable_occurrence)),
462        (slot6_name, slot6),
463        (slot7_name, text(draft.tag_or_long_description)),
464        (kind.fallback_attr, text(draft.fallback)),
465        (
466            "PredefinedType",
467            predefined_type.map_or(Value::Null, |t| Value::Enum(t.into())),
468        ),
469    ];
470    values.extend(specific);
471    let record = layout.named_record(entity, values)?;
472    Ok(tx.create(record))
473}
474
475/// The type-specific attributes of `draft` (#214), checked against the
476/// bound release: a token must be a member of the enumeration the release
477/// declares for that attribute on `entity`, and a value for an attribute
478/// `entity` does not declare there is
479/// [`ElementTypeError::AuthoringNotInSchema`]. Unset fields yield `$`,
480/// which [`Layout::named_record`] drops for an undeclared attribute and
481/// refuses for a required one.
482fn specific_values(
483    layout: Layout,
484    entity: &'static str,
485    draft: &TypeDraft<'_>,
486) -> ElementTypeResult<Vec<(&'static str, Value)>> {
487    let enums = [
488        ("OperationType", draft.operation_type),
489        ("PartitioningType", draft.partitioning_type),
490        ("EventTriggerType", draft.event_trigger_type),
491        ("AssemblyPlace", draft.assembly_place),
492    ];
493    let mut values = Vec::new();
494    for (attribute, token) in enums {
495        let Some(token) = token else {
496            continue;
497        };
498        let Some(members) = layout.members(entity, attribute) else {
499            return Err(ElementTypeError::AuthoringNotInSchema {
500                entity,
501                attribute,
502                schema: layout.version(),
503            });
504        };
505        if !members.contains(&token) {
506            return Err(invalid(entity, attribute, token));
507        }
508        values.push((attribute, Value::Enum(token.into())));
509    }
510    // `IfcEventType.CorrectEventTriggerType`: USERDEFINED names its trigger
511    // in `UserDefinedEventTriggerType`. Blank is refused as for `fallback`.
512    if draft.event_trigger_type == Some("USERDEFINED")
513        && blank(draft.user_defined_event_trigger_type)
514    {
515        return Err(invalid(
516            entity,
517            "UserDefinedEventTriggerType",
518            "required by USERDEFINED",
519        ));
520    }
521    values.extend([
522        (
523            "UserDefinedOperationType",
524            text(draft.user_defined_operation_type),
525        ),
526        (
527            "UserDefinedPartitioningType",
528            text(draft.user_defined_partitioning_type),
529        ),
530        (
531            "UserDefinedEventTriggerType",
532            text(draft.user_defined_event_trigger_type),
533        ),
534        (
535            "ParameterTakesPrecedence",
536            draft
537                .parameter_takes_precedence
538                .map_or(Value::Null, Value::Bool),
539        ),
540    ]);
541    Ok(values)
542}
543
544fn text(value: Option<&str>) -> Value {
545    value.map_or(Value::Null, |v| Value::Text(v.into()))
546}
547
548fn blank(value: Option<&str>) -> bool {
549    value.is_none_or(|v| v.trim().is_empty())
550}