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//! This module is stricter than `EXISTS`: a blank fallback string satisfies
24//! EXPRESS but names nothing, so it is refused too. And the writer takes an
25//! [`ElementType`] from the catalogue rather than a type-name string, so an
26//! entity the catalogue does not know cannot be written at all.
27//!
28//! # Which release is written
29//!
30//! [`create_type`] takes no model and writes the catalogue's IFC4X3
31//! layout. [`create_type_in`] and [`create_type_with_owner_history`] write
32//! the model's declared release (#202): see `release.rs`.
33
34use ifc_model::guid::Guid;
35use ifc_model::{EntityId, Model, Transaction, Value};
36
37use crate::error::{ElementTypeError, ElementTypeResult};
38use crate::release::{bind, require_owner_history, Layout};
39use crate::table::{ElementType, Family};
40
41pub(crate) fn invalid(
42    entity: &'static str,
43    attribute: &'static str,
44    value: impl Into<String>,
45) -> ElementTypeError {
46    ElementTypeError::Invalid {
47        entity,
48        attribute,
49        value: value.into(),
50    }
51}
52
53/// Attributes shared by every type definition.
54///
55/// `tag_or_long_description` and `maps_or_identification` occupy slots
56/// 7 and 6, whose meaning depends on [`Family`]. Naming them for both
57/// readings keeps a caller from assuming the element-type reading on a
58/// resource type, where it would file a tag as a description.
59#[derive(Debug, Clone, Copy, Default)]
60pub struct TypeDraft<'a> {
61    /// `Name`.
62    pub name: Option<&'a str>,
63    /// `Description`.
64    pub description: Option<&'a str>,
65    /// `ApplicableOccurrence`, slot 4.
66    pub applicable_occurrence: Option<&'a str>,
67    /// Slot 6: `RepresentationMaps` refs, or `Identification` text.
68    pub maps_or_identification: Option<Slot6<'a>>,
69    /// Slot 7: `Tag` on element types, `LongDescription` otherwise.
70    pub tag_or_long_description: Option<&'a str>,
71    /// Slot 8: the `USERDEFINED` fallback. Required when the
72    /// predefined type is `USERDEFINED`.
73    pub fallback: Option<&'a str>,
74}
75
76/// What slot 6 holds, which differs by [`Family`].
77#[derive(Debug, Clone, Copy)]
78pub enum Slot6<'a> {
79    /// `RepresentationMaps`: shape definitions the occurrences map.
80    RepresentationMaps(&'a [EntityId]),
81    /// `Identification`: a catalogue or article number.
82    Identification(&'a str),
83}
84
85/// Stage one type definition.
86///
87/// `predefined_type` must be a token the entity's own enum declares.
88/// A token borrowed from a sibling enum is refused: `IfcPumpTypeEnum`
89/// has no `SUBMERSIBLEPUMP` member merely because some other pump-like
90/// enum does.
91///
92/// # Release
93///
94/// Takes no model, so it writes the catalogue's IFC4X3 layout with
95/// `OwnerHistory` `$`, and cannot refuse a model that declares another
96/// release. That record is valid in IFC4X3 and, where IFC4 declares the
97/// type with the same layout and token, in IFC4; it is never valid IFC2X3,
98/// which requires `OwnerHistory`. Use [`create_type_in`] or
99/// [`create_type_with_owner_history`] to write the model's declared
100/// release.
101///
102/// # Errors
103///
104/// Refuses a malformed GlobalId, a token outside the entity's enum, a
105/// missing predefined type where the schema requires one, `USERDEFINED`
106/// without the fallback attribute, and a slot-6 value of the wrong
107/// shape for the entity's family.
108pub fn create_type(
109    tx: &mut Transaction,
110    kind: ElementType,
111    global_id: &str,
112    predefined_type: Option<&str>,
113    draft: TypeDraft<'_>,
114) -> ElementTypeResult<EntityId> {
115    let request = Request {
116        kind,
117        global_id,
118        predefined_type,
119        draft,
120    };
121    author(tx, Layout::catalogue()?, request, None)
122}
123
124/// [`create_type`] in the model's declared release (#202).
125///
126/// The record is laid out by attribute name from that release's table, and
127/// `predefined_type` is checked against that release's enumeration.
128/// `OwnerHistory` is left `$`, which IFC4 and IFC4X3 allow and IFC2X3 does
129/// not, so an IFC2X3 model is refused with
130/// [`ElementTypeError::AuthoringRequired`]; use
131/// [`create_type_with_owner_history`] there. A header without
132/// `FILE_SCHEMA` binds IFC4.
133///
134/// # Errors
135///
136/// Those of [`create_type`], checked against the bound release, and:
137/// [`ElementTypeError::MultipleSchemas`] or
138/// [`ElementTypeError::UnsupportedSchema`] for a model that binds no single
139/// known release; [`ElementTypeError::EntityNotInSchema`] for a type the
140/// release does not declare (IFC2X3 has no `IfcDoorType`, IFC4 no
141/// `IfcBearingType`); [`ElementTypeError::AuthoringNotInSchema`] for a
142/// token where the release declares no `PredefinedType`, and
143/// [`ElementTypeError::AuthoringRequired`] for any other attribute the
144/// release requires that the draft cannot carry. Nothing is staged on an
145/// error.
146pub fn create_type_in(
147    tx: &mut Transaction,
148    model: &Model,
149    kind: ElementType,
150    global_id: &str,
151    predefined_type: Option<&str>,
152    draft: TypeDraft<'_>,
153) -> ElementTypeResult<EntityId> {
154    let request = Request {
155        kind,
156        global_id,
157        predefined_type,
158        draft,
159    };
160    author(tx, bind(model)?, request, None)
161}
162
163/// [`create_type_in`] with a caller-supplied `IfcOwnerHistory`, which
164/// IFC2X3 requires on every `IfcRoot`.
165///
166/// `owner_history` must be in the model or staged earlier on `tx`, and must
167/// be an `IfcOwnerHistory`; one is never invented here (build it with
168/// `ifc-author`). In IFC4 and IFC4X3 the reference fills the optional slot.
169///
170/// # Errors
171///
172/// Those of [`create_type_in`] except the IFC2X3 `OwnerHistory` refusal,
173/// and [`ElementTypeError::MissingEntity`] for an `owner_history` that does
174/// not resolve or [`ElementTypeError::Invalid`] on `OwnerHistory` for one
175/// that is another entity. Nothing is staged on an error.
176pub fn create_type_with_owner_history(
177    tx: &mut Transaction,
178    model: &Model,
179    kind: ElementType,
180    global_id: &str,
181    predefined_type: Option<&str>,
182    draft: TypeDraft<'_>,
183    owner_history: EntityId,
184) -> ElementTypeResult<EntityId> {
185    let layout = bind(model)?;
186    // Checked before the draft so a wrong reference is reported as such.
187    require_owner_history(tx, model, kind.type_name, owner_history)?;
188    let request = Request {
189        kind,
190        global_id,
191        predefined_type,
192        draft,
193    };
194    author(tx, layout, request, Some(owner_history))
195}
196
197/// The caller's arguments, bundled.
198struct Request<'a> {
199    kind: ElementType,
200    global_id: &'a str,
201    predefined_type: Option<&'a str>,
202    draft: TypeDraft<'a>,
203}
204
205/// Stage one type definition in `layout`; `None` leaves `OwnerHistory` `$`.
206/// The owner history, if any, has been checked by the caller.
207fn author(
208    tx: &mut Transaction,
209    layout: Layout,
210    request: Request<'_>,
211    owner_history: Option<EntityId>,
212) -> ElementTypeResult<EntityId> {
213    let Request {
214        kind,
215        global_id,
216        predefined_type,
217        draft,
218    } = request;
219    let entity = kind.type_name;
220    if Guid::parse(global_id).is_none() {
221        return Err(invalid(entity, "GlobalId", global_id));
222    }
223    // `IfcTypeObject.NameRequired` is inherited by all 132 catalogue
224    // types. `Name` is OPTIONAL in the slot table and mandatory by
225    // rule, so a writer trusting the slot table alone files a nameless
226    // type that parses and cannot be referred to.
227    if blank(draft.name) {
228        return Err(invalid(entity, "Name", "NameRequired"));
229    }
230    layout.require_entity(entity)?;
231
232    // The release's own `PredefinedType`: whether it is required, and its
233    // tokens. For the catalogue's release these are the row's own.
234    match layout.attribute(entity, "PredefinedType") {
235        None if predefined_type.is_some() => {
236            return Err(ElementTypeError::AuthoringNotInSchema {
237                entity,
238                attribute: "PredefinedType",
239                schema: layout.version(),
240            });
241        }
242        None => {}
243        Some(declared) => {
244            let members = layout.members(entity, "PredefinedType").unwrap_or_default();
245            match predefined_type {
246                None if !declared.optional => {
247                    return Err(invalid(entity, "PredefinedType", "required"));
248                }
249                Some(token) if !members.contains(&token) => {
250                    return Err(invalid(entity, "PredefinedType", token));
251                }
252                _ => {}
253            }
254        }
255    }
256    if predefined_type == Some("USERDEFINED") && blank(draft.fallback) {
257        return Err(invalid(
258            entity,
259            kind.fallback_attr,
260            "required by USERDEFINED",
261        ));
262    }
263
264    let (slot6_name, slot6) = match (draft.maps_or_identification, kind.family) {
265        (Some(Slot6::RepresentationMaps(maps)), Family::Element) => {
266            if maps.is_empty() {
267                return Err(invalid(entity, "RepresentationMaps", "empty"));
268            }
269            (
270                "RepresentationMaps",
271                Value::List(maps.iter().copied().map(Value::Ref).collect()),
272            )
273        }
274        (Some(Slot6::Identification(id)), Family::ResourceOrProcess) => {
275            ("Identification", Value::Text(id.into()))
276        }
277        (Some(Slot6::RepresentationMaps(_)), Family::ResourceOrProcess) => {
278            return Err(invalid(entity, "Identification", "expected text, got maps"));
279        }
280        (Some(Slot6::Identification(_)), Family::Element) => {
281            return Err(invalid(
282                entity,
283                "RepresentationMaps",
284                "expected maps, got text",
285            ));
286        }
287        (None, _) => ("RepresentationMaps", Value::Null),
288    };
289    let slot7_name = match kind.family {
290        Family::Element => "Tag",
291        Family::ResourceOrProcess => "LongDescription",
292    };
293
294    let record = layout.named_record(
295        entity,
296        vec![
297            ("GlobalId", Value::Text(global_id.into())),
298            (
299                "OwnerHistory",
300                owner_history.map_or(Value::Null, Value::Ref),
301            ),
302            ("Name", text(draft.name)),
303            ("Description", text(draft.description)),
304            ("ApplicableOccurrence", text(draft.applicable_occurrence)),
305            (slot6_name, slot6),
306            (slot7_name, text(draft.tag_or_long_description)),
307            (kind.fallback_attr, text(draft.fallback)),
308            (
309                "PredefinedType",
310                predefined_type.map_or(Value::Null, |t| Value::Enum(t.into())),
311            ),
312        ],
313    )?;
314    Ok(tx.create(record))
315}
316
317fn text(value: Option<&str>) -> Value {
318    value.map_or(Value::Null, |v| Value::Text(v.into()))
319}
320
321fn blank(value: Option<&str>) -> bool {
322    value.is_none_or(|v| v.trim().is_empty())
323}