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)]
42#[non_exhaustive]
43pub struct SupertypeKind {
44    /// STEP type name, upper-case as stored.
45    ///
46    /// Upper-case because `Entity::new` does not normalise and the model
47    /// indexes by the stored string: a mixed-case record is invisible to
48    /// `of_type` lookups that use the catalogue's own spelling.
49    pub type_name: &'static str,
50    /// Total attribute count, including inherited.
51    pub arity: usize,
52}
53
54/// `IfcTypeObject`: the root of every type definition.
55pub const TYPE_OBJECT: SupertypeKind = SupertypeKind {
56    type_name: "IFCTYPEOBJECT",
57    arity: 6,
58};
59
60/// `IfcTypeProduct`: adds `RepresentationMaps` and `Tag`.
61pub const TYPE_PRODUCT: SupertypeKind = SupertypeKind {
62    type_name: "IFCTYPEPRODUCT",
63    arity: 8,
64};
65
66/// `IfcBuiltElementType`: the built-element branch.
67pub const BUILT_ELEMENT_TYPE: SupertypeKind = SupertypeKind {
68    type_name: "IFCBUILTELEMENTTYPE",
69    arity: 9,
70};
71
72/// `IfcCivilElementType`: civil works with no more specific class.
73pub const CIVIL_ELEMENT_TYPE: SupertypeKind = SupertypeKind {
74    type_name: "IFCCIVILELEMENTTYPE",
75    arity: 9,
76};
77
78/// `IfcDeepFoundationType`: piles and caisson foundations.
79pub const DEEP_FOUNDATION_TYPE: SupertypeKind = SupertypeKind {
80    type_name: "IFCDEEPFOUNDATIONTYPE",
81    arity: 9,
82};
83
84/// `IfcDistributionElementType`: the distribution branch.
85pub const DISTRIBUTION_ELEMENT_TYPE: SupertypeKind = SupertypeKind {
86    type_name: "IFCDISTRIBUTIONELEMENTTYPE",
87    arity: 9,
88};
89
90/// `IfcFurnishingElementType`: furniture and system furniture.
91pub const FURNISHING_ELEMENT_TYPE: SupertypeKind = SupertypeKind {
92    type_name: "IFCFURNISHINGELEMENTTYPE",
93    arity: 9,
94};
95
96/// Every supertype this module can stage.
97pub const ALL_SUPERTYPES: &[SupertypeKind] = &[
98    TYPE_OBJECT,
99    TYPE_PRODUCT,
100    BUILT_ELEMENT_TYPE,
101    CIVIL_ELEMENT_TYPE,
102    DEEP_FOUNDATION_TYPE,
103    DISTRIBUTION_ELEMENT_TYPE,
104    FURNISHING_ELEMENT_TYPE,
105];
106
107/// Attributes of a supertype definition.
108///
109/// `property_sets` carries `(name, id)` pairs rather than bare ids:
110/// `UniquePropertySetNames` is stated over the sets' names, and a
111/// staged entity cannot be read back out of a `Transaction` to supply
112/// them. Matches the convention `add_property_set` already uses.
113///
114/// The struct is `#[non_exhaustive]`: build it with
115/// [`SupertypeDraft::new`] and the setters.
116#[derive(Debug, Clone, Copy, Default)]
117#[non_exhaustive]
118pub struct SupertypeDraft<'a> {
119    /// `Description`.
120    pub description: Option<&'a str>,
121    /// `ApplicableOccurrence`, slot 4.
122    pub applicable_occurrence: Option<&'a str>,
123    /// `HasPropertySets`, slot 5: `(Name, id)` per set.
124    pub property_sets: &'a [(&'a str, EntityId)],
125    /// `RepresentationMaps`, slot 6. Rejected below arity 8.
126    pub representation_maps: &'a [EntityId],
127    /// `Tag`, slot 7. Rejected below arity 8.
128    pub tag: Option<&'a str>,
129    /// `ElementType`, slot 8. Rejected below arity 9.
130    pub element_type: Option<&'a str>,
131}
132
133impl<'a> SupertypeDraft<'a> {
134    /// Starts an empty draft with every attribute unset.
135    #[must_use]
136    pub fn new() -> Self {
137        Self::default()
138    }
139
140    /// Sets `Description`.
141    #[must_use]
142    pub fn description(mut self, value: &'a str) -> Self {
143        self.description = Some(value);
144        self
145    }
146
147    /// Sets `ApplicableOccurrence`.
148    #[must_use]
149    pub fn applicable_occurrence(mut self, value: &'a str) -> Self {
150        self.applicable_occurrence = Some(value);
151        self
152    }
153
154    /// Sets `HasPropertySets`: `(Name, id)` per set.
155    #[must_use]
156    pub fn property_sets(mut self, value: &'a [(&'a str, EntityId)]) -> Self {
157        self.property_sets = value;
158        self
159    }
160
161    /// Sets `RepresentationMaps`.
162    #[must_use]
163    pub fn representation_maps(mut self, value: &'a [EntityId]) -> Self {
164        self.representation_maps = value;
165        self
166    }
167
168    /// Sets `Tag`.
169    #[must_use]
170    pub fn tag(mut self, value: &'a str) -> Self {
171        self.tag = Some(value);
172        self
173    }
174
175    /// Sets `ElementType`.
176    #[must_use]
177    pub fn element_type(mut self, value: &'a str) -> Self {
178        self.element_type = Some(value);
179        self
180    }
181}
182
183fn text(value: Option<&str>) -> Value {
184    value.map_or(Value::Null, |v| Value::Text(v.into()))
185}
186
187/// Stage a type definition that carries no `PredefinedType`.
188///
189/// `name` is taken by value, not as an `Option`: `NameRequired` makes it
190/// mandatory for every type object, so there is no legal way to omit it
191/// and the signature says so.
192///
193/// # Release
194///
195/// Takes no model, so it writes the IFC4X3 layout with `OwnerHistory` `$`
196/// and cannot refuse a model that declares another release; that record is
197/// never valid IFC2X3, which requires `OwnerHistory`. Use
198/// [`create_supertype_in`] or [`create_supertype_with_owner_history`] to
199/// write the model's declared release.
200///
201/// # Errors
202///
203/// Refuses a malformed GlobalId, a blank name (NameRequired), an empty
204/// but present property-set list, duplicate property-set names
205/// (UniquePropertySetNames), an empty representation-map list, and any
206/// attribute the entity's arity does not reach.
207pub fn create_supertype(
208    tx: &mut Transaction,
209    kind: SupertypeKind,
210    global_id: &str,
211    name: &str,
212    draft: SupertypeDraft<'_>,
213) -> ElementTypeResult<EntityId> {
214    let request = Request {
215        kind,
216        global_id,
217        name,
218        draft,
219    };
220    author(tx, Layout::catalogue()?, request, None)
221}
222
223/// [`create_supertype`] in the model's declared release (#202).
224///
225/// The record is laid out by attribute name from that release's table.
226/// `OwnerHistory` is left `$`, which IFC4 and IFC4X3 allow and IFC2X3 does
227/// not, so an IFC2X3 model is refused with
228/// [`AuthoringRequired`](crate::ElementTypeError::AuthoringRequired); use
229/// [`create_supertype_with_owner_history`] there. A header without
230/// `FILE_SCHEMA` binds IFC4.
231///
232/// # Errors
233///
234/// Those of [`create_supertype`], where "not declared" means not declared
235/// by the bound release, and: [`MultipleSchemas`](crate::ElementTypeError::MultipleSchemas) or
236/// [`UnsupportedSchema`](crate::ElementTypeError::UnsupportedSchema) for a model that binds no single
237/// known release; [`EntityNotInSchema`](crate::ElementTypeError::EntityNotInSchema) for a type the
238/// release does not declare or declares abstract (`IfcBuiltElementType` is
239/// IFC4X3 only). Nothing is staged on an error.
240pub fn create_supertype_in(
241    tx: &mut Transaction,
242    model: &Model,
243    kind: SupertypeKind,
244    global_id: &str,
245    name: &str,
246    draft: SupertypeDraft<'_>,
247) -> ElementTypeResult<EntityId> {
248    let request = Request {
249        kind,
250        global_id,
251        name,
252        draft,
253    };
254    author(tx, bind(model)?, request, None)
255}
256
257/// [`create_supertype_in`] with a caller-supplied `IfcOwnerHistory`, which
258/// IFC2X3 requires on every `IfcRoot`.
259///
260/// `owner_history` must be in the model or staged earlier on `tx`, and must
261/// be an `IfcOwnerHistory`; one is never invented here.
262///
263/// # Errors
264///
265/// Those of [`create_supertype_in`] except the IFC2X3 `OwnerHistory`
266/// refusal, and [`MissingEntity`](crate::ElementTypeError::MissingEntity) or
267/// [`Invalid`](crate::ElementTypeError::Invalid) on `OwnerHistory` for an `owner_history`
268/// that does not resolve or is another entity. Nothing is staged on an
269/// error.
270pub fn create_supertype_with_owner_history(
271    tx: &mut Transaction,
272    model: &Model,
273    kind: SupertypeKind,
274    global_id: &str,
275    name: &str,
276    draft: SupertypeDraft<'_>,
277    owner_history: EntityId,
278) -> ElementTypeResult<EntityId> {
279    let layout = bind(model)?;
280    require_owner_history(tx, model, kind.type_name, owner_history)?;
281    let request = Request {
282        kind,
283        global_id,
284        name,
285        draft,
286    };
287    author(tx, layout, request, Some(owner_history))
288}
289
290/// The caller's arguments, bundled.
291struct Request<'a> {
292    kind: SupertypeKind,
293    global_id: &'a str,
294    name: &'a str,
295    draft: SupertypeDraft<'a>,
296}
297
298/// Stage a supertype in `layout`; `None` leaves `OwnerHistory` `$`.
299fn author(
300    tx: &mut Transaction,
301    layout: Layout,
302    request: Request<'_>,
303    owner_history: Option<EntityId>,
304) -> ElementTypeResult<EntityId> {
305    let Request {
306        kind,
307        global_id,
308        name,
309        draft,
310    } = request;
311    let entity = kind.type_name;
312    if Guid::parse(global_id).is_none() {
313        return Err(invalid(entity, "GlobalId", global_id));
314    }
315    if name.trim().is_empty() {
316        return Err(invalid(entity, "Name", "NameRequired"));
317    }
318    layout.require_entity(entity)?;
319
320    let mut values = vec![
321        ("GlobalId", Value::Text(global_id.into())),
322        (
323            "OwnerHistory",
324            owner_history.map_or(Value::Null, Value::Ref),
325        ),
326        ("Name", Value::Text(name.into())),
327        ("Description", text(draft.description)),
328        ("ApplicableOccurrence", text(draft.applicable_occurrence)),
329    ];
330
331    if !draft.property_sets.is_empty() {
332        for (index, (set_name, _)) in draft.property_sets.iter().enumerate() {
333            // A blank name defeats the uniqueness rule rather than
334            // satisfying it: two unnamed sets are not distinguishable
335            // by the name the rule compares.
336            if set_name.trim().is_empty() {
337                return Err(invalid(entity, "HasPropertySets", "blank name"));
338            }
339            if draft.property_sets[..index]
340                .iter()
341                .any(|(seen, _)| seen == set_name)
342            {
343                return Err(invalid(entity, "HasPropertySets", (*set_name).to_owned()));
344            }
345        }
346        let sets = draft.property_sets.iter().map(|(_, id)| Value::Ref(*id));
347        values.push(("HasPropertySets", Value::List(sets.collect())));
348    }
349
350    // `RepresentationMaps`, `Tag` and `ElementType` exist only on the
351    // deeper types. An attribute the entity does not declare is refused,
352    // not dropped: silently discarding a caller's Tag writes a file
353    // missing data they believe they supplied.
354    let declared = |attribute| layout.attribute(entity, attribute).is_some();
355    if !draft.representation_maps.is_empty() {
356        if !declared("RepresentationMaps") {
357            return Err(invalid(entity, "RepresentationMaps", "not declared"));
358        }
359        let maps = draft.representation_maps.iter().copied().map(Value::Ref);
360        values.push(("RepresentationMaps", Value::List(maps.collect())));
361    }
362    if draft.tag.is_some() {
363        if !declared("Tag") {
364            return Err(invalid(entity, "Tag", "not declared"));
365        }
366        values.push(("Tag", text(draft.tag)));
367    }
368    if draft.element_type.is_some() {
369        if !declared("ElementType") {
370            return Err(invalid(entity, "ElementType", "not declared"));
371        }
372        values.push(("ElementType", text(draft.element_type)));
373    }
374
375    Ok(tx.create(layout.named_record(entity, values)?))
376}