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//! The same holds for `IfcEventType.CorrectEventTriggerType`, stated over
24//! `EventTriggerType` and `UserDefinedEventTriggerType`.
25//!
26//! This module is stricter than `EXISTS`: a blank fallback string satisfies
27//! EXPRESS but names nothing, so it is refused too. And the writer takes an
28//! [`ElementType`] from the catalogue rather than a type-name string, so an
29//! entity the catalogue does not know cannot be written at all.
30//!
31//! # Which release is written
32//!
33//! [`create_type`] takes no model and writes the catalogue's IFC4X3
34//! layout. [`create_type_in`] and [`create_type_with_owner_history`] write
35//! the model's declared release (#202): see `release.rs`.
36
37use ifc_model::guid::Guid;
38use ifc_model::{EntityId, Model, Transaction, Value};
39
40use crate::error::{ElementTypeError, ElementTypeResult};
41use crate::release::{bind, require_owner_history, Layout};
42use crate::table::{ElementType, Family};
43
44pub(crate) fn invalid(
45 entity: &'static str,
46 attribute: &'static str,
47 value: impl Into<String>,
48) -> ElementTypeError {
49 ElementTypeError::Invalid {
50 entity,
51 attribute,
52 value: value.into(),
53 }
54}
55
56/// Attributes of a type definition: those every type shares, and the few
57/// one type requires on top.
58///
59/// `tag_or_long_description` and `maps_or_identification` occupy slots
60/// 7 and 6, whose meaning depends on [`Family`]. Naming them for both
61/// readings keeps a caller from assuming the element-type reading on a
62/// resource type, where it would file a tag as a description.
63///
64/// The type-specific fields (#214) carry the attributes IFC4 and IFC4X3
65/// require on four types, as `references/ifc-spec` declares them:
66///
67/// ```text
68/// IfcDoorType OperationType : IfcDoorTypeOperationEnum;
69/// IfcWindowType PartitioningType : IfcWindowTypePartitioningEnum;
70/// IfcEventType EventTriggerType : IfcEventTriggerTypeEnum;
71/// IfcFurnitureType AssemblyPlace : IfcAssemblyPlaceEnum; (IFC2X3 too)
72/// ```
73///
74/// A value for an attribute the type does not declare in the bound release
75/// is refused, never dropped.
76///
77/// The struct is `#[non_exhaustive]`: build it with [`TypeDraft::new`] and
78/// the setters, so a field a later release needs can be added without
79/// breaking callers.
80#[derive(Debug, Clone, Copy, Default)]
81#[non_exhaustive]
82pub struct TypeDraft<'a> {
83 /// `Name`.
84 pub name: Option<&'a str>,
85 /// `Description`.
86 pub description: Option<&'a str>,
87 /// `ApplicableOccurrence`, slot 4.
88 pub applicable_occurrence: Option<&'a str>,
89 /// Slot 6: `RepresentationMaps` refs, or `Identification` text.
90 pub maps_or_identification: Option<Slot6<'a>>,
91 /// Slot 7: `Tag` on element types, `LongDescription` otherwise.
92 pub tag_or_long_description: Option<&'a str>,
93 /// Slot 8: the `USERDEFINED` fallback. Required when the
94 /// predefined type is `USERDEFINED`.
95 pub fallback: Option<&'a str>,
96 /// `IfcDoorType.OperationType`, an `IfcDoorTypeOperationEnum` token;
97 /// required on `IfcDoorType`.
98 pub operation_type: Option<&'a str>,
99 /// `IfcDoorType.UserDefinedOperationType`.
100 pub user_defined_operation_type: Option<&'a str>,
101 /// `IfcWindowType.PartitioningType`, an `IfcWindowTypePartitioningEnum`
102 /// token; required on `IfcWindowType`.
103 pub partitioning_type: Option<&'a str>,
104 /// `IfcWindowType.UserDefinedPartitioningType`.
105 pub user_defined_partitioning_type: Option<&'a str>,
106 /// `ParameterTakesPrecedence` on `IfcDoorType` and `IfcWindowType`.
107 pub parameter_takes_precedence: Option<bool>,
108 /// `IfcEventType.EventTriggerType`, an `IfcEventTriggerTypeEnum` token;
109 /// required on `IfcEventType`.
110 pub event_trigger_type: Option<&'a str>,
111 /// `IfcEventType.UserDefinedEventTriggerType`. Required when
112 /// `event_trigger_type` is `USERDEFINED` (`CorrectEventTriggerType`).
113 pub user_defined_event_trigger_type: Option<&'a str>,
114 /// `IfcFurnitureType.AssemblyPlace`, an `IfcAssemblyPlaceEnum` token;
115 /// required on `IfcFurnitureType`.
116 pub assembly_place: Option<&'a str>,
117}
118
119impl<'a> TypeDraft<'a> {
120 /// Starts an empty draft with every attribute unset.
121 #[must_use]
122 pub fn new() -> Self {
123 Self::default()
124 }
125
126 /// Sets `Name`, which `NameRequired` makes mandatory.
127 #[must_use]
128 pub fn name(mut self, value: &'a str) -> Self {
129 self.name = Some(value);
130 self
131 }
132
133 /// Sets `Description`.
134 #[must_use]
135 pub fn description(mut self, value: &'a str) -> Self {
136 self.description = Some(value);
137 self
138 }
139
140 /// Sets `ApplicableOccurrence`.
141 #[must_use]
142 pub fn applicable_occurrence(mut self, value: &'a str) -> Self {
143 self.applicable_occurrence = Some(value);
144 self
145 }
146
147 /// Sets slot 6: `RepresentationMaps` or `Identification`.
148 #[must_use]
149 pub fn maps_or_identification(mut self, value: Slot6<'a>) -> Self {
150 self.maps_or_identification = Some(value);
151 self
152 }
153
154 /// Sets slot 7: `Tag` or `LongDescription`.
155 #[must_use]
156 pub fn tag_or_long_description(mut self, value: &'a str) -> Self {
157 self.tag_or_long_description = Some(value);
158 self
159 }
160
161 /// Sets the slot-8 `USERDEFINED` fallback.
162 #[must_use]
163 pub fn fallback(mut self, value: &'a str) -> Self {
164 self.fallback = Some(value);
165 self
166 }
167
168 /// Sets `IfcDoorType.OperationType`.
169 #[must_use]
170 pub fn operation_type(mut self, value: &'a str) -> Self {
171 self.operation_type = Some(value);
172 self
173 }
174
175 /// Sets `IfcDoorType.UserDefinedOperationType`.
176 #[must_use]
177 pub fn user_defined_operation_type(mut self, value: &'a str) -> Self {
178 self.user_defined_operation_type = Some(value);
179 self
180 }
181
182 /// Sets `IfcWindowType.PartitioningType`.
183 #[must_use]
184 pub fn partitioning_type(mut self, value: &'a str) -> Self {
185 self.partitioning_type = Some(value);
186 self
187 }
188
189 /// Sets `IfcWindowType.UserDefinedPartitioningType`.
190 #[must_use]
191 pub fn user_defined_partitioning_type(mut self, value: &'a str) -> Self {
192 self.user_defined_partitioning_type = Some(value);
193 self
194 }
195
196 /// Sets `ParameterTakesPrecedence` (`IfcDoorType`, `IfcWindowType`).
197 #[must_use]
198 pub fn parameter_takes_precedence(mut self, value: bool) -> Self {
199 self.parameter_takes_precedence = Some(value);
200 self
201 }
202
203 /// Sets `IfcEventType.EventTriggerType`.
204 #[must_use]
205 pub fn event_trigger_type(mut self, value: &'a str) -> Self {
206 self.event_trigger_type = Some(value);
207 self
208 }
209
210 /// Sets `IfcEventType.UserDefinedEventTriggerType`.
211 #[must_use]
212 pub fn user_defined_event_trigger_type(mut self, value: &'a str) -> Self {
213 self.user_defined_event_trigger_type = Some(value);
214 self
215 }
216
217 /// Sets `IfcFurnitureType.AssemblyPlace`.
218 #[must_use]
219 pub fn assembly_place(mut self, value: &'a str) -> Self {
220 self.assembly_place = Some(value);
221 self
222 }
223}
224
225/// What slot 6 holds, which differs by [`Family`].
226#[derive(Debug, Clone, Copy)]
227#[non_exhaustive]
228pub enum Slot6<'a> {
229 /// `RepresentationMaps`: shape definitions the occurrences map.
230 RepresentationMaps(&'a [EntityId]),
231 /// `Identification`: a catalogue or article number.
232 Identification(&'a str),
233}
234
235/// Stage one type definition.
236///
237/// `predefined_type` must be a token the entity's own enum declares.
238/// A token borrowed from a sibling enum is refused: `IfcPumpTypeEnum`
239/// has no `SUBMERSIBLEPUMP` member merely because some other pump-like
240/// enum does.
241///
242/// # Release
243///
244/// Takes no model, so it writes the catalogue's IFC4X3 layout with
245/// `OwnerHistory` `$`, and cannot refuse a model that declares another
246/// release. That record is valid in IFC4X3 and, where IFC4 declares the
247/// type with the same layout and token, in IFC4; it is never valid IFC2X3,
248/// which requires `OwnerHistory`. Use [`create_type_in`] or
249/// [`create_type_with_owner_history`] to write the model's declared
250/// release.
251///
252/// # Errors
253///
254/// Refuses a malformed GlobalId, a token outside the entity's enum, a
255/// missing predefined type where the schema requires one, `USERDEFINED`
256/// without the fallback attribute, and a slot-6 value of the wrong
257/// shape for the entity's family. A type-specific token outside its
258/// enumeration is [`ElementTypeError::Invalid`], one for an attribute the
259/// type does not declare [`ElementTypeError::AuthoringNotInSchema`], and
260/// `USERDEFINED` `event_trigger_type` without
261/// `user_defined_event_trigger_type` is refused (`CorrectEventTriggerType`).
262/// A required attribute left unset, such as `IfcDoorType.OperationType` or
263/// `IfcFurnitureType.AssemblyPlace`, is
264/// [`ElementTypeError::AuthoringRequired`], never written `$` (#214).
265/// Nothing is staged on an error.
266pub fn create_type(
267 tx: &mut Transaction,
268 kind: ElementType,
269 global_id: &str,
270 predefined_type: Option<&str>,
271 draft: TypeDraft<'_>,
272) -> ElementTypeResult<EntityId> {
273 let request = Request {
274 kind,
275 global_id,
276 predefined_type,
277 draft,
278 };
279 author(tx, Layout::catalogue()?, request, None)
280}
281
282/// [`create_type`] in the model's declared release (#202).
283///
284/// The record is laid out by attribute name from that release's table, and
285/// `predefined_type` is checked against that release's enumeration.
286/// `OwnerHistory` is left `$`, which IFC4 and IFC4X3 allow and IFC2X3 does
287/// not, so an IFC2X3 model is refused with
288/// [`ElementTypeError::AuthoringRequired`]; use
289/// [`create_type_with_owner_history`] there. A header without
290/// `FILE_SCHEMA` binds IFC4.
291///
292/// # Errors
293///
294/// Those of [`create_type`], checked against the bound release, and:
295/// [`ElementTypeError::MultipleSchemas`] or
296/// [`ElementTypeError::UnsupportedSchema`] for a model that binds no single
297/// known release; [`ElementTypeError::EntityNotInSchema`] for a type the
298/// release does not declare (IFC2X3 has no `IfcDoorType`, IFC4 no
299/// `IfcBearingType`); [`ElementTypeError::AuthoringNotInSchema`] for a
300/// token where the release declares no `PredefinedType`, and
301/// [`ElementTypeError::AuthoringRequired`] for any other attribute the
302/// release requires that the draft leaves unset. Nothing is staged on an
303/// error.
304pub fn create_type_in(
305 tx: &mut Transaction,
306 model: &Model,
307 kind: ElementType,
308 global_id: &str,
309 predefined_type: Option<&str>,
310 draft: TypeDraft<'_>,
311) -> ElementTypeResult<EntityId> {
312 let request = Request {
313 kind,
314 global_id,
315 predefined_type,
316 draft,
317 };
318 author(tx, bind(model)?, request, None)
319}
320
321/// [`create_type_in`] with a caller-supplied `IfcOwnerHistory`, which
322/// IFC2X3 requires on every `IfcRoot`.
323///
324/// `owner_history` must be in the model or staged earlier on `tx`, and must
325/// be an `IfcOwnerHistory`; one is never invented here (build it with
326/// `ifc-author`). In IFC4 and IFC4X3 the reference fills the optional slot.
327///
328/// # Errors
329///
330/// Those of [`create_type_in`] except the IFC2X3 `OwnerHistory` refusal,
331/// and [`ElementTypeError::MissingEntity`] for an `owner_history` that does
332/// not resolve or [`ElementTypeError::Invalid`] on `OwnerHistory` for one
333/// that is another entity. Nothing is staged on an error.
334pub fn create_type_with_owner_history(
335 tx: &mut Transaction,
336 model: &Model,
337 kind: ElementType,
338 global_id: &str,
339 predefined_type: Option<&str>,
340 draft: TypeDraft<'_>,
341 owner_history: EntityId,
342) -> ElementTypeResult<EntityId> {
343 let layout = bind(model)?;
344 // Checked before the draft so a wrong reference is reported as such.
345 require_owner_history(tx, model, kind.type_name, owner_history)?;
346 let request = Request {
347 kind,
348 global_id,
349 predefined_type,
350 draft,
351 };
352 author(tx, layout, request, Some(owner_history))
353}
354
355/// The caller's arguments, bundled.
356struct Request<'a> {
357 kind: ElementType,
358 global_id: &'a str,
359 predefined_type: Option<&'a str>,
360 draft: TypeDraft<'a>,
361}
362
363/// Stage one type definition in `layout`; `None` leaves `OwnerHistory` `$`.
364/// The owner history, if any, has been checked by the caller.
365fn author(
366 tx: &mut Transaction,
367 layout: Layout,
368 request: Request<'_>,
369 owner_history: Option<EntityId>,
370) -> ElementTypeResult<EntityId> {
371 let Request {
372 kind,
373 global_id,
374 predefined_type,
375 draft,
376 } = request;
377 let entity = kind.type_name;
378 if Guid::parse(global_id).is_none() {
379 return Err(invalid(entity, "GlobalId", global_id));
380 }
381 // `IfcTypeObject.NameRequired` is inherited by all 132 catalogue
382 // types. `Name` is OPTIONAL in the slot table and mandatory by
383 // rule, so a writer trusting the slot table alone files a nameless
384 // type that parses and cannot be referred to.
385 if blank(draft.name) {
386 return Err(invalid(entity, "Name", "NameRequired"));
387 }
388 layout.require_entity(entity)?;
389
390 // The release's own `PredefinedType`: whether it is required, and its
391 // tokens. For the catalogue's release these are the row's own.
392 match layout.attribute(entity, "PredefinedType") {
393 None if predefined_type.is_some() => {
394 return Err(ElementTypeError::AuthoringNotInSchema {
395 entity,
396 attribute: "PredefinedType",
397 schema: layout.version(),
398 });
399 }
400 None => {}
401 Some(declared) => {
402 let members = layout.members(entity, "PredefinedType").unwrap_or_default();
403 match predefined_type {
404 None if !declared.optional => {
405 return Err(invalid(entity, "PredefinedType", "required"));
406 }
407 Some(token) if !members.contains(&token) => {
408 return Err(invalid(entity, "PredefinedType", token));
409 }
410 _ => {}
411 }
412 }
413 }
414 if predefined_type == Some("USERDEFINED") && blank(draft.fallback) {
415 return Err(invalid(
416 entity,
417 kind.fallback_attr,
418 "required by USERDEFINED",
419 ));
420 }
421
422 let (slot6_name, slot6) = match (draft.maps_or_identification, kind.family) {
423 (Some(Slot6::RepresentationMaps(maps)), Family::Element) => {
424 if maps.is_empty() {
425 return Err(invalid(entity, "RepresentationMaps", "empty"));
426 }
427 (
428 "RepresentationMaps",
429 Value::List(maps.iter().copied().map(Value::Ref).collect()),
430 )
431 }
432 (Some(Slot6::Identification(id)), Family::ResourceOrProcess) => {
433 ("Identification", Value::Text(id.into()))
434 }
435 (Some(Slot6::RepresentationMaps(_)), Family::ResourceOrProcess) => {
436 return Err(invalid(entity, "Identification", "expected text, got maps"));
437 }
438 (Some(Slot6::Identification(_)), Family::Element) => {
439 return Err(invalid(
440 entity,
441 "RepresentationMaps",
442 "expected maps, got text",
443 ));
444 }
445 (None, _) => ("RepresentationMaps", Value::Null),
446 };
447 let slot7_name = match kind.family {
448 Family::Element => "Tag",
449 Family::ResourceOrProcess => "LongDescription",
450 };
451
452 let specific = specific_values(layout, entity, &draft)?;
453 let mut values = vec![
454 ("GlobalId", Value::Text(global_id.into())),
455 (
456 "OwnerHistory",
457 owner_history.map_or(Value::Null, Value::Ref),
458 ),
459 ("Name", text(draft.name)),
460 ("Description", text(draft.description)),
461 ("ApplicableOccurrence", text(draft.applicable_occurrence)),
462 (slot6_name, slot6),
463 (slot7_name, text(draft.tag_or_long_description)),
464 (kind.fallback_attr, text(draft.fallback)),
465 (
466 "PredefinedType",
467 predefined_type.map_or(Value::Null, |t| Value::Enum(t.into())),
468 ),
469 ];
470 values.extend(specific);
471 let record = layout.named_record(entity, values)?;
472 Ok(tx.create(record))
473}
474
475/// The type-specific attributes of `draft` (#214), checked against the
476/// bound release: a token must be a member of the enumeration the release
477/// declares for that attribute on `entity`, and a value for an attribute
478/// `entity` does not declare there is
479/// [`ElementTypeError::AuthoringNotInSchema`]. Unset fields yield `$`,
480/// which [`Layout::named_record`] drops for an undeclared attribute and
481/// refuses for a required one.
482fn specific_values(
483 layout: Layout,
484 entity: &'static str,
485 draft: &TypeDraft<'_>,
486) -> ElementTypeResult<Vec<(&'static str, Value)>> {
487 let enums = [
488 ("OperationType", draft.operation_type),
489 ("PartitioningType", draft.partitioning_type),
490 ("EventTriggerType", draft.event_trigger_type),
491 ("AssemblyPlace", draft.assembly_place),
492 ];
493 let mut values = Vec::new();
494 for (attribute, token) in enums {
495 let Some(token) = token else {
496 continue;
497 };
498 let Some(members) = layout.members(entity, attribute) else {
499 return Err(ElementTypeError::AuthoringNotInSchema {
500 entity,
501 attribute,
502 schema: layout.version(),
503 });
504 };
505 if !members.contains(&token) {
506 return Err(invalid(entity, attribute, token));
507 }
508 values.push((attribute, Value::Enum(token.into())));
509 }
510 // `IfcEventType.CorrectEventTriggerType`: USERDEFINED names its trigger
511 // in `UserDefinedEventTriggerType`. Blank is refused as for `fallback`.
512 if draft.event_trigger_type == Some("USERDEFINED")
513 && blank(draft.user_defined_event_trigger_type)
514 {
515 return Err(invalid(
516 entity,
517 "UserDefinedEventTriggerType",
518 "required by USERDEFINED",
519 ));
520 }
521 values.extend([
522 (
523 "UserDefinedOperationType",
524 text(draft.user_defined_operation_type),
525 ),
526 (
527 "UserDefinedPartitioningType",
528 text(draft.user_defined_partitioning_type),
529 ),
530 (
531 "UserDefinedEventTriggerType",
532 text(draft.user_defined_event_trigger_type),
533 ),
534 (
535 "ParameterTakesPrecedence",
536 draft
537 .parameter_takes_precedence
538 .map_or(Value::Null, Value::Bool),
539 ),
540 ]);
541 Ok(values)
542}
543
544fn text(value: Option<&str>) -> Value {
545 value.map_or(Value::Null, |v| Value::Text(v.into()))
546}
547
548fn blank(value: Option<&str>) -> bool {
549 value.is_none_or(|v| v.trim().is_empty())
550}