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//! This module is stricter than `EXISTS`: a blank fallback string satisfies
24//! EXPRESS but names nothing, so it is refused too. And the writer takes an
25//! [`ElementType`] from the catalogue rather than a type-name string, so an
26//! entity the catalogue does not know cannot be written at all.
27//!
28//! # Which release is written
29//!
30//! [`create_type`] takes no model and writes the catalogue's IFC4X3
31//! layout. [`create_type_in`] and [`create_type_with_owner_history`] write
32//! the model's declared release (#202): see `release.rs`.
33
34use ifc_model::guid::Guid;
35use ifc_model::{EntityId, Model, Transaction, Value};
36
37use crate::error::{ElementTypeError, ElementTypeResult};
38use crate::release::{bind, require_owner_history, Layout};
39use crate::table::{ElementType, Family};
40
41pub(crate) fn invalid(
42 entity: &'static str,
43 attribute: &'static str,
44 value: impl Into<String>,
45) -> ElementTypeError {
46 ElementTypeError::Invalid {
47 entity,
48 attribute,
49 value: value.into(),
50 }
51}
52
53/// Attributes shared by every type definition.
54///
55/// `tag_or_long_description` and `maps_or_identification` occupy slots
56/// 7 and 6, whose meaning depends on [`Family`]. Naming them for both
57/// readings keeps a caller from assuming the element-type reading on a
58/// resource type, where it would file a tag as a description.
59#[derive(Debug, Clone, Copy, Default)]
60pub struct TypeDraft<'a> {
61 /// `Name`.
62 pub name: Option<&'a str>,
63 /// `Description`.
64 pub description: Option<&'a str>,
65 /// `ApplicableOccurrence`, slot 4.
66 pub applicable_occurrence: Option<&'a str>,
67 /// Slot 6: `RepresentationMaps` refs, or `Identification` text.
68 pub maps_or_identification: Option<Slot6<'a>>,
69 /// Slot 7: `Tag` on element types, `LongDescription` otherwise.
70 pub tag_or_long_description: Option<&'a str>,
71 /// Slot 8: the `USERDEFINED` fallback. Required when the
72 /// predefined type is `USERDEFINED`.
73 pub fallback: Option<&'a str>,
74}
75
76/// What slot 6 holds, which differs by [`Family`].
77#[derive(Debug, Clone, Copy)]
78pub enum Slot6<'a> {
79 /// `RepresentationMaps`: shape definitions the occurrences map.
80 RepresentationMaps(&'a [EntityId]),
81 /// `Identification`: a catalogue or article number.
82 Identification(&'a str),
83}
84
85/// Stage one type definition.
86///
87/// `predefined_type` must be a token the entity's own enum declares.
88/// A token borrowed from a sibling enum is refused: `IfcPumpTypeEnum`
89/// has no `SUBMERSIBLEPUMP` member merely because some other pump-like
90/// enum does.
91///
92/// # Release
93///
94/// Takes no model, so it writes the catalogue's IFC4X3 layout with
95/// `OwnerHistory` `$`, and cannot refuse a model that declares another
96/// release. That record is valid in IFC4X3 and, where IFC4 declares the
97/// type with the same layout and token, in IFC4; it is never valid IFC2X3,
98/// which requires `OwnerHistory`. Use [`create_type_in`] or
99/// [`create_type_with_owner_history`] to write the model's declared
100/// release.
101///
102/// # Errors
103///
104/// Refuses a malformed GlobalId, a token outside the entity's enum, a
105/// missing predefined type where the schema requires one, `USERDEFINED`
106/// without the fallback attribute, and a slot-6 value of the wrong
107/// shape for the entity's family.
108pub fn create_type(
109 tx: &mut Transaction,
110 kind: ElementType,
111 global_id: &str,
112 predefined_type: Option<&str>,
113 draft: TypeDraft<'_>,
114) -> ElementTypeResult<EntityId> {
115 let request = Request {
116 kind,
117 global_id,
118 predefined_type,
119 draft,
120 };
121 author(tx, Layout::catalogue()?, request, None)
122}
123
124/// [`create_type`] in the model's declared release (#202).
125///
126/// The record is laid out by attribute name from that release's table, and
127/// `predefined_type` is checked against that release's enumeration.
128/// `OwnerHistory` is left `$`, which IFC4 and IFC4X3 allow and IFC2X3 does
129/// not, so an IFC2X3 model is refused with
130/// [`ElementTypeError::AuthoringRequired`]; use
131/// [`create_type_with_owner_history`] there. A header without
132/// `FILE_SCHEMA` binds IFC4.
133///
134/// # Errors
135///
136/// Those of [`create_type`], checked against the bound release, and:
137/// [`ElementTypeError::MultipleSchemas`] or
138/// [`ElementTypeError::UnsupportedSchema`] for a model that binds no single
139/// known release; [`ElementTypeError::EntityNotInSchema`] for a type the
140/// release does not declare (IFC2X3 has no `IfcDoorType`, IFC4 no
141/// `IfcBearingType`); [`ElementTypeError::AuthoringNotInSchema`] for a
142/// token where the release declares no `PredefinedType`, and
143/// [`ElementTypeError::AuthoringRequired`] for any other attribute the
144/// release requires that the draft cannot carry. Nothing is staged on an
145/// error.
146pub fn create_type_in(
147 tx: &mut Transaction,
148 model: &Model,
149 kind: ElementType,
150 global_id: &str,
151 predefined_type: Option<&str>,
152 draft: TypeDraft<'_>,
153) -> ElementTypeResult<EntityId> {
154 let request = Request {
155 kind,
156 global_id,
157 predefined_type,
158 draft,
159 };
160 author(tx, bind(model)?, request, None)
161}
162
163/// [`create_type_in`] with a caller-supplied `IfcOwnerHistory`, which
164/// IFC2X3 requires on every `IfcRoot`.
165///
166/// `owner_history` must be in the model or staged earlier on `tx`, and must
167/// be an `IfcOwnerHistory`; one is never invented here (build it with
168/// `ifc-author`). In IFC4 and IFC4X3 the reference fills the optional slot.
169///
170/// # Errors
171///
172/// Those of [`create_type_in`] except the IFC2X3 `OwnerHistory` refusal,
173/// and [`ElementTypeError::MissingEntity`] for an `owner_history` that does
174/// not resolve or [`ElementTypeError::Invalid`] on `OwnerHistory` for one
175/// that is another entity. Nothing is staged on an error.
176pub fn create_type_with_owner_history(
177 tx: &mut Transaction,
178 model: &Model,
179 kind: ElementType,
180 global_id: &str,
181 predefined_type: Option<&str>,
182 draft: TypeDraft<'_>,
183 owner_history: EntityId,
184) -> ElementTypeResult<EntityId> {
185 let layout = bind(model)?;
186 // Checked before the draft so a wrong reference is reported as such.
187 require_owner_history(tx, model, kind.type_name, owner_history)?;
188 let request = Request {
189 kind,
190 global_id,
191 predefined_type,
192 draft,
193 };
194 author(tx, layout, request, Some(owner_history))
195}
196
197/// The caller's arguments, bundled.
198struct Request<'a> {
199 kind: ElementType,
200 global_id: &'a str,
201 predefined_type: Option<&'a str>,
202 draft: TypeDraft<'a>,
203}
204
205/// Stage one type definition in `layout`; `None` leaves `OwnerHistory` `$`.
206/// The owner history, if any, has been checked by the caller.
207fn author(
208 tx: &mut Transaction,
209 layout: Layout,
210 request: Request<'_>,
211 owner_history: Option<EntityId>,
212) -> ElementTypeResult<EntityId> {
213 let Request {
214 kind,
215 global_id,
216 predefined_type,
217 draft,
218 } = request;
219 let entity = kind.type_name;
220 if Guid::parse(global_id).is_none() {
221 return Err(invalid(entity, "GlobalId", global_id));
222 }
223 // `IfcTypeObject.NameRequired` is inherited by all 132 catalogue
224 // types. `Name` is OPTIONAL in the slot table and mandatory by
225 // rule, so a writer trusting the slot table alone files a nameless
226 // type that parses and cannot be referred to.
227 if blank(draft.name) {
228 return Err(invalid(entity, "Name", "NameRequired"));
229 }
230 layout.require_entity(entity)?;
231
232 // The release's own `PredefinedType`: whether it is required, and its
233 // tokens. For the catalogue's release these are the row's own.
234 match layout.attribute(entity, "PredefinedType") {
235 None if predefined_type.is_some() => {
236 return Err(ElementTypeError::AuthoringNotInSchema {
237 entity,
238 attribute: "PredefinedType",
239 schema: layout.version(),
240 });
241 }
242 None => {}
243 Some(declared) => {
244 let members = layout.members(entity, "PredefinedType").unwrap_or_default();
245 match predefined_type {
246 None if !declared.optional => {
247 return Err(invalid(entity, "PredefinedType", "required"));
248 }
249 Some(token) if !members.contains(&token) => {
250 return Err(invalid(entity, "PredefinedType", token));
251 }
252 _ => {}
253 }
254 }
255 }
256 if predefined_type == Some("USERDEFINED") && blank(draft.fallback) {
257 return Err(invalid(
258 entity,
259 kind.fallback_attr,
260 "required by USERDEFINED",
261 ));
262 }
263
264 let (slot6_name, slot6) = match (draft.maps_or_identification, kind.family) {
265 (Some(Slot6::RepresentationMaps(maps)), Family::Element) => {
266 if maps.is_empty() {
267 return Err(invalid(entity, "RepresentationMaps", "empty"));
268 }
269 (
270 "RepresentationMaps",
271 Value::List(maps.iter().copied().map(Value::Ref).collect()),
272 )
273 }
274 (Some(Slot6::Identification(id)), Family::ResourceOrProcess) => {
275 ("Identification", Value::Text(id.into()))
276 }
277 (Some(Slot6::RepresentationMaps(_)), Family::ResourceOrProcess) => {
278 return Err(invalid(entity, "Identification", "expected text, got maps"));
279 }
280 (Some(Slot6::Identification(_)), Family::Element) => {
281 return Err(invalid(
282 entity,
283 "RepresentationMaps",
284 "expected maps, got text",
285 ));
286 }
287 (None, _) => ("RepresentationMaps", Value::Null),
288 };
289 let slot7_name = match kind.family {
290 Family::Element => "Tag",
291 Family::ResourceOrProcess => "LongDescription",
292 };
293
294 let record = layout.named_record(
295 entity,
296 vec![
297 ("GlobalId", Value::Text(global_id.into())),
298 (
299 "OwnerHistory",
300 owner_history.map_or(Value::Null, Value::Ref),
301 ),
302 ("Name", text(draft.name)),
303 ("Description", text(draft.description)),
304 ("ApplicableOccurrence", text(draft.applicable_occurrence)),
305 (slot6_name, slot6),
306 (slot7_name, text(draft.tag_or_long_description)),
307 (kind.fallback_attr, text(draft.fallback)),
308 (
309 "PredefinedType",
310 predefined_type.map_or(Value::Null, |t| Value::Enum(t.into())),
311 ),
312 ],
313 )?;
314 Ok(tx.create(record))
315}
316
317fn text(value: Option<&str>) -> Value {
318 value.map_or(Value::Null, |v| Value::Text(v.into()))
319}
320
321fn blank(value: Option<&str>) -> bool {
322 value.is_none_or(|v| v.trim().is_empty())
323}