Skip to main content

ifc_element_type/
supertype.rs

1//! Type definitions that carry no `PredefinedType`.
2//!
3//! The generated catalogue in [`crate::table`] is keyed on a predefined
4//! type enum, so the seven structural type definitions that declare no
5//! such enum have no row there and cannot be staged by
6//! [`crate::create_type`].
7//!
8//! They are still concrete. `SUPERTYPE OF (ONEOF ...)` constrains which
9//! subtype an instance may additionally be; it does not make the
10//! supertype uninstantiable, and the schema marks none of these
11//! ABSTRACT. The occurrence side already works this way: `IfcBuiltElement`
12//! is authored on exactly the same footing as `IfcBuiltElementType` is
13//! here.
14//!
15//! # The rule this module exists to enforce
16//!
17//! `IfcTypeObject` states `NameRequired`:
18//!
19//! ```text
20//! NameRequired : EXISTS(SELF\IfcRoot.Name);
21//! ```
22//!
23//! `Name` is OPTIONAL in the attribute list and mandatory by rule. A
24//! writer reading only the slot table files a nameless type, which
25//! parses and then cannot be referred to by anything.
26
27use ifc_model::guid::Guid;
28use ifc_model::{EntityId, Model, Transaction, Value};
29
30use crate::authoring::invalid;
31use crate::error::ElementTypeResult;
32use crate::release::{bind, require_owner_history, Layout};
33
34/// A type definition with no `PredefinedType` enum.
35///
36/// The three arities are the three inheritance depths: `IfcTypeObject`
37/// stops at `HasPropertySets`, `IfcTypeProduct` adds
38/// `RepresentationMaps` and `Tag`, and the element types add
39/// `ElementType`. Writing all seven at one arity would leave trailing
40/// slots on the shallow ones and truncate the deep ones.
41#[derive(Debug, Clone, Copy, PartialEq, Eq)]
42pub struct SupertypeKind {
43    /// STEP type name, upper-case as stored.
44    ///
45    /// Upper-case because `Entity::new` does not normalise and the model
46    /// indexes by the stored string: a mixed-case record is invisible to
47    /// `of_type` lookups that use the catalogue's own spelling.
48    pub type_name: &'static str,
49    /// Total attribute count, including inherited.
50    pub arity: usize,
51}
52
53/// `IfcTypeObject`: the root of every type definition.
54pub const TYPE_OBJECT: SupertypeKind = SupertypeKind {
55    type_name: "IFCTYPEOBJECT",
56    arity: 6,
57};
58
59/// `IfcTypeProduct`: adds `RepresentationMaps` and `Tag`.
60pub const TYPE_PRODUCT: SupertypeKind = SupertypeKind {
61    type_name: "IFCTYPEPRODUCT",
62    arity: 8,
63};
64
65/// `IfcBuiltElementType`: the built-element branch.
66pub const BUILT_ELEMENT_TYPE: SupertypeKind = SupertypeKind {
67    type_name: "IFCBUILTELEMENTTYPE",
68    arity: 9,
69};
70
71/// `IfcCivilElementType`: civil works with no more specific class.
72pub const CIVIL_ELEMENT_TYPE: SupertypeKind = SupertypeKind {
73    type_name: "IFCCIVILELEMENTTYPE",
74    arity: 9,
75};
76
77/// `IfcDeepFoundationType`: piles and caisson foundations.
78pub const DEEP_FOUNDATION_TYPE: SupertypeKind = SupertypeKind {
79    type_name: "IFCDEEPFOUNDATIONTYPE",
80    arity: 9,
81};
82
83/// `IfcDistributionElementType`: the distribution branch.
84pub const DISTRIBUTION_ELEMENT_TYPE: SupertypeKind = SupertypeKind {
85    type_name: "IFCDISTRIBUTIONELEMENTTYPE",
86    arity: 9,
87};
88
89/// `IfcFurnishingElementType`: furniture and system furniture.
90pub const FURNISHING_ELEMENT_TYPE: SupertypeKind = SupertypeKind {
91    type_name: "IFCFURNISHINGELEMENTTYPE",
92    arity: 9,
93};
94
95/// Every supertype this module can stage.
96pub const ALL_SUPERTYPES: &[SupertypeKind] = &[
97    TYPE_OBJECT,
98    TYPE_PRODUCT,
99    BUILT_ELEMENT_TYPE,
100    CIVIL_ELEMENT_TYPE,
101    DEEP_FOUNDATION_TYPE,
102    DISTRIBUTION_ELEMENT_TYPE,
103    FURNISHING_ELEMENT_TYPE,
104];
105
106/// Attributes of a supertype definition.
107///
108/// `property_sets` carries `(name, id)` pairs rather than bare ids:
109/// `UniquePropertySetNames` is stated over the sets' names, and a
110/// staged entity cannot be read back out of a `Transaction` to supply
111/// them. Matches the convention `add_property_set` already uses.
112#[derive(Debug, Clone, Copy, Default)]
113pub struct SupertypeDraft<'a> {
114    /// `Description`.
115    pub description: Option<&'a str>,
116    /// `ApplicableOccurrence`, slot 4.
117    pub applicable_occurrence: Option<&'a str>,
118    /// `HasPropertySets`, slot 5: `(Name, id)` per set.
119    pub property_sets: &'a [(&'a str, EntityId)],
120    /// `RepresentationMaps`, slot 6. Rejected below arity 8.
121    pub representation_maps: &'a [EntityId],
122    /// `Tag`, slot 7. Rejected below arity 8.
123    pub tag: Option<&'a str>,
124    /// `ElementType`, slot 8. Rejected below arity 9.
125    pub element_type: Option<&'a str>,
126}
127
128fn text(value: Option<&str>) -> Value {
129    value.map_or(Value::Null, |v| Value::Text(v.into()))
130}
131
132/// Stage a type definition that carries no `PredefinedType`.
133///
134/// `name` is taken by value, not as an `Option`: `NameRequired` makes it
135/// mandatory for every type object, so there is no legal way to omit it
136/// and the signature says so.
137///
138/// # Release
139///
140/// Takes no model, so it writes the IFC4X3 layout with `OwnerHistory` `$`
141/// and cannot refuse a model that declares another release; that record is
142/// never valid IFC2X3, which requires `OwnerHistory`. Use
143/// [`create_supertype_in`] or [`create_supertype_with_owner_history`] to
144/// write the model's declared release.
145///
146/// # Errors
147///
148/// Refuses a malformed GlobalId, a blank name (NameRequired), an empty
149/// but present property-set list, duplicate property-set names
150/// (UniquePropertySetNames), an empty representation-map list, and any
151/// attribute the entity's arity does not reach.
152pub fn create_supertype(
153    tx: &mut Transaction,
154    kind: SupertypeKind,
155    global_id: &str,
156    name: &str,
157    draft: SupertypeDraft<'_>,
158) -> ElementTypeResult<EntityId> {
159    let request = Request {
160        kind,
161        global_id,
162        name,
163        draft,
164    };
165    author(tx, Layout::catalogue()?, request, None)
166}
167
168/// [`create_supertype`] in the model's declared release (#202).
169///
170/// The record is laid out by attribute name from that release's table.
171/// `OwnerHistory` is left `$`, which IFC4 and IFC4X3 allow and IFC2X3 does
172/// not, so an IFC2X3 model is refused with
173/// [`AuthoringRequired`](crate::ElementTypeError::AuthoringRequired); use
174/// [`create_supertype_with_owner_history`] there. A header without
175/// `FILE_SCHEMA` binds IFC4.
176///
177/// # Errors
178///
179/// Those of [`create_supertype`], where "not declared" means not declared
180/// by the bound release, and: [`MultipleSchemas`](crate::ElementTypeError::MultipleSchemas) or
181/// [`UnsupportedSchema`](crate::ElementTypeError::UnsupportedSchema) for a model that binds no single
182/// known release; [`EntityNotInSchema`](crate::ElementTypeError::EntityNotInSchema) for a type the
183/// release does not declare or declares abstract (`IfcBuiltElementType` is
184/// IFC4X3 only). Nothing is staged on an error.
185pub fn create_supertype_in(
186    tx: &mut Transaction,
187    model: &Model,
188    kind: SupertypeKind,
189    global_id: &str,
190    name: &str,
191    draft: SupertypeDraft<'_>,
192) -> ElementTypeResult<EntityId> {
193    let request = Request {
194        kind,
195        global_id,
196        name,
197        draft,
198    };
199    author(tx, bind(model)?, request, None)
200}
201
202/// [`create_supertype_in`] with a caller-supplied `IfcOwnerHistory`, which
203/// IFC2X3 requires on every `IfcRoot`.
204///
205/// `owner_history` must be in the model or staged earlier on `tx`, and must
206/// be an `IfcOwnerHistory`; one is never invented here.
207///
208/// # Errors
209///
210/// Those of [`create_supertype_in`] except the IFC2X3 `OwnerHistory`
211/// refusal, and [`MissingEntity`](crate::ElementTypeError::MissingEntity) or
212/// [`Invalid`](crate::ElementTypeError::Invalid) on `OwnerHistory` for an `owner_history`
213/// that does not resolve or is another entity. Nothing is staged on an
214/// error.
215pub fn create_supertype_with_owner_history(
216    tx: &mut Transaction,
217    model: &Model,
218    kind: SupertypeKind,
219    global_id: &str,
220    name: &str,
221    draft: SupertypeDraft<'_>,
222    owner_history: EntityId,
223) -> ElementTypeResult<EntityId> {
224    let layout = bind(model)?;
225    require_owner_history(tx, model, kind.type_name, owner_history)?;
226    let request = Request {
227        kind,
228        global_id,
229        name,
230        draft,
231    };
232    author(tx, layout, request, Some(owner_history))
233}
234
235/// The caller's arguments, bundled.
236struct Request<'a> {
237    kind: SupertypeKind,
238    global_id: &'a str,
239    name: &'a str,
240    draft: SupertypeDraft<'a>,
241}
242
243/// Stage a supertype in `layout`; `None` leaves `OwnerHistory` `$`.
244fn author(
245    tx: &mut Transaction,
246    layout: Layout,
247    request: Request<'_>,
248    owner_history: Option<EntityId>,
249) -> ElementTypeResult<EntityId> {
250    let Request {
251        kind,
252        global_id,
253        name,
254        draft,
255    } = request;
256    let entity = kind.type_name;
257    if Guid::parse(global_id).is_none() {
258        return Err(invalid(entity, "GlobalId", global_id));
259    }
260    if name.trim().is_empty() {
261        return Err(invalid(entity, "Name", "NameRequired"));
262    }
263    layout.require_entity(entity)?;
264
265    let mut values = vec![
266        ("GlobalId", Value::Text(global_id.into())),
267        (
268            "OwnerHistory",
269            owner_history.map_or(Value::Null, Value::Ref),
270        ),
271        ("Name", Value::Text(name.into())),
272        ("Description", text(draft.description)),
273        ("ApplicableOccurrence", text(draft.applicable_occurrence)),
274    ];
275
276    if !draft.property_sets.is_empty() {
277        for (index, (set_name, _)) in draft.property_sets.iter().enumerate() {
278            // A blank name defeats the uniqueness rule rather than
279            // satisfying it: two unnamed sets are not distinguishable
280            // by the name the rule compares.
281            if set_name.trim().is_empty() {
282                return Err(invalid(entity, "HasPropertySets", "blank name"));
283            }
284            if draft.property_sets[..index]
285                .iter()
286                .any(|(seen, _)| seen == set_name)
287            {
288                return Err(invalid(entity, "HasPropertySets", (*set_name).to_owned()));
289            }
290        }
291        let sets = draft.property_sets.iter().map(|(_, id)| Value::Ref(*id));
292        values.push(("HasPropertySets", Value::List(sets.collect())));
293    }
294
295    // `RepresentationMaps`, `Tag` and `ElementType` exist only on the
296    // deeper types. An attribute the entity does not declare is refused,
297    // not dropped: silently discarding a caller's Tag writes a file
298    // missing data they believe they supplied.
299    let declared = |attribute| layout.attribute(entity, attribute).is_some();
300    if !draft.representation_maps.is_empty() {
301        if !declared("RepresentationMaps") {
302            return Err(invalid(entity, "RepresentationMaps", "not declared"));
303        }
304        let maps = draft.representation_maps.iter().copied().map(Value::Ref);
305        values.push(("RepresentationMaps", Value::List(maps.collect())));
306    }
307    if draft.tag.is_some() {
308        if !declared("Tag") {
309            return Err(invalid(entity, "Tag", "not declared"));
310        }
311        values.push(("Tag", text(draft.tag)));
312    }
313    if draft.element_type.is_some() {
314        if !declared("ElementType") {
315            return Err(invalid(entity, "ElementType", "not declared"));
316        }
317        values.push(("ElementType", text(draft.element_type)));
318    }
319
320    Ok(tx.create(layout.named_record(entity, values)?))
321}