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}