Skip to main content

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
23use ifc_model::guid::Guid;
24use ifc_model::{Entity, EntityId, Transaction, Value};
25
26use crate::table::{ElementType, Family};
27
28/// Why a type definition was refused.
29#[derive(Debug, Clone, PartialEq, Eq)]
30#[non_exhaustive]
31pub enum ElementTypeError {
32    /// An attribute value the schema does not permit.
33    Invalid {
34        /// STEP type name.
35        entity: &'static str,
36        /// Attribute that was rejected.
37        attribute: &'static str,
38        /// The offending value.
39        value: String,
40    },
41}
42
43impl std::fmt::Display for ElementTypeError {
44    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
45        let Self::Invalid {
46            entity,
47            attribute,
48            value,
49        } = self;
50        write!(f, "{entity}.{attribute}: {value}")
51    }
52}
53
54impl std::error::Error for ElementTypeError {}
55
56/// Result of staging a type definition.
57pub type ElementTypeResult<T> = Result<T, ElementTypeError>;
58
59fn invalid(
60    entity: &'static str,
61    attribute: &'static str,
62    value: impl Into<String>,
63) -> ElementTypeError {
64    ElementTypeError::Invalid {
65        entity,
66        attribute,
67        value: value.into(),
68    }
69}
70
71/// Attributes shared by every type definition.
72///
73/// `tag_or_long_description` and `maps_or_identification` occupy slots
74/// 7 and 6, whose meaning depends on [`Family`]. Naming them for both
75/// readings keeps a caller from assuming the element-type reading on a
76/// resource type, where it would file a tag as a description.
77#[derive(Debug, Clone, Copy, Default)]
78pub struct TypeDraft<'a> {
79    /// `Name`.
80    pub name: Option<&'a str>,
81    /// `Description`.
82    pub description: Option<&'a str>,
83    /// `ApplicableOccurrence`, slot 4.
84    pub applicable_occurrence: Option<&'a str>,
85    /// Slot 6: `RepresentationMaps` refs, or `Identification` text.
86    pub maps_or_identification: Option<Slot6<'a>>,
87    /// Slot 7: `Tag` on element types, `LongDescription` otherwise.
88    pub tag_or_long_description: Option<&'a str>,
89    /// Slot 8: the `USERDEFINED` fallback. Required when the
90    /// predefined type is `USERDEFINED`.
91    pub fallback: Option<&'a str>,
92}
93
94/// What slot 6 holds, which differs by [`Family`].
95#[derive(Debug, Clone, Copy)]
96pub enum Slot6<'a> {
97    /// `RepresentationMaps`: shape definitions the occurrences map.
98    RepresentationMaps(&'a [EntityId]),
99    /// `Identification`: a catalogue or article number.
100    Identification(&'a str),
101}
102
103/// Stage one type definition.
104///
105/// `predefined_type` must be a token the entity's own enum declares.
106/// A token borrowed from a sibling enum is refused: `IfcPumpTypeEnum`
107/// has no `SUBMERSIBLEPUMP` member merely because some other pump-like
108/// enum does.
109///
110/// # Errors
111///
112/// Refuses a malformed GlobalId, a token outside the entity's enum, a
113/// missing predefined type where the schema requires one, `USERDEFINED`
114/// without the fallback attribute, and a slot-6 value of the wrong
115/// shape for the entity's family.
116pub fn create_type(
117    tx: &mut Transaction,
118    kind: ElementType,
119    global_id: &str,
120    predefined_type: Option<&str>,
121    draft: TypeDraft<'_>,
122) -> ElementTypeResult<EntityId> {
123    let entity = kind.type_name;
124    if Guid::parse(global_id).is_none() {
125        return Err(invalid(entity, "GlobalId", global_id));
126    }
127    // `IfcTypeObject.NameRequired` is inherited by all 132 catalogue
128    // types. `Name` is OPTIONAL in the slot table and mandatory by
129    // rule, so a writer trusting the slot table alone files a nameless
130    // type that parses and cannot be referred to.
131    if blank(draft.name) {
132        return Err(invalid(entity, "Name", "NameRequired"));
133    }
134
135    match predefined_type {
136        None if !kind.predefined_optional => {
137            return Err(invalid(entity, "PredefinedType", "required"));
138        }
139        Some(token) if !kind.members.contains(&token) => {
140            return Err(invalid(entity, "PredefinedType", token));
141        }
142        Some(token) if token == "USERDEFINED" && blank(draft.fallback) => {
143            return Err(invalid(
144                entity,
145                kind.fallback_attr,
146                "required by USERDEFINED",
147            ));
148        }
149        _ => {}
150    }
151
152    let mut attrs = vec![Value::Null; kind.arity];
153    attrs[0] = Value::Text(global_id.into());
154    attrs[2] = text(draft.name);
155    attrs[3] = text(draft.description);
156    attrs[4] = text(draft.applicable_occurrence);
157
158    match (draft.maps_or_identification, kind.family) {
159        (Some(Slot6::RepresentationMaps(maps)), Family::Element) => {
160            if maps.is_empty() {
161                return Err(invalid(entity, "RepresentationMaps", "empty"));
162            }
163            attrs[6] = Value::List(maps.iter().copied().map(Value::Ref).collect());
164        }
165        (Some(Slot6::Identification(id)), Family::ResourceOrProcess) => {
166            attrs[6] = Value::Text(id.into());
167        }
168        (Some(Slot6::RepresentationMaps(_)), Family::ResourceOrProcess) => {
169            return Err(invalid(entity, "Identification", "expected text, got maps"));
170        }
171        (Some(Slot6::Identification(_)), Family::Element) => {
172            return Err(invalid(
173                entity,
174                "RepresentationMaps",
175                "expected maps, got text",
176            ));
177        }
178        (None, _) => {}
179    }
180
181    attrs[7] = text(draft.tag_or_long_description);
182    attrs[kind.fallback_slot] = text(draft.fallback);
183    attrs[kind.predefined_slot] = predefined_type.map_or(Value::Null, |t| Value::Enum(t.into()));
184
185    Ok(tx.create(Entity::new(entity, attrs)))
186}
187
188fn text(value: Option<&str>) -> Value {
189    value.map_or(Value::Null, |v| Value::Text(v.into()))
190}
191
192fn blank(value: Option<&str>) -> bool {
193    value.is_none_or(|v| v.trim().is_empty())
194}