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}