Skip to main content

ifc_lite_core/generated/
schema_registry.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
5//! Generated EXPRESS schema registry selection.
6//!
7//! The three registry modules are generated independently from the committed
8//! IFC2X3, IFC4, and IFC4X3 EXPRESS inputs.  The canonical [`super::IfcType`]
9//! remains the IFC4X3 type used by parsing and geometry; this module exists
10//! only for questions whose answer is defined by the source file's schema,
11//! such as positional attribute names.
12
13/// The EXPRESS schema families bundled in the Rust registry.
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
15pub enum SchemaVersion {
16    Ifc2x3,
17    Ifc4,
18    Ifc4x1,
19    Ifc4x2,
20    Ifc4x3,
21}
22
23/// Schema-local facts about one declared entity.
24///
25/// These facts come from the independently generated registry for the source
26/// schema, not from the public merged type universe (#4203).
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub struct SchemaEntityInfo {
29    pub name: &'static str,
30    pub parent: Option<&'static str>,
31    pub is_abstract: bool,
32}
33
34impl SchemaVersion {
35    /// Resolve a STEP `FILE_SCHEMA` label to a bundled schema family.
36    ///
37    /// Addendum, corrigendum, and release-candidate suffixes share their
38    /// family's bundled EXPRESS registry. Unknown labels deliberately return
39    /// `None`: guessing a nearby schema could relabel a positional value
40    /// incorrectly.
41    pub fn from_file_schema(label: &str) -> Option<Self> {
42        let upper = label.trim().to_ascii_uppercase();
43        if is_schema_family(&upper, "IFC4X3") {
44            Some(Self::Ifc4x3)
45        } else if is_schema_family(&upper, "IFC4X2") {
46            Some(Self::Ifc4x2)
47        } else if is_schema_family(&upper, "IFC4X1") {
48            Some(Self::Ifc4x1)
49        } else if is_schema_family(&upper, "IFC4") {
50            Some(Self::Ifc4)
51        } else if is_schema_family(&upper, "IFC2X3") {
52            Some(Self::Ifc2x3)
53        } else {
54            None
55        }
56    }
57
58    /// Attribute names for `entity_name` in this schema, or `None` when the
59    /// entity is not declared by this schema. A declared entity with no
60    /// attributes returns `Some(&[])` so callers never confuse it with absent.
61    pub fn attribute_names(self, entity_name: &str) -> Option<&'static [&'static str]> {
62        match self {
63            Self::Ifc2x3 => attribute_names_ifc2x3(entity_name),
64            Self::Ifc4 => attribute_names_ifc4(entity_name),
65            Self::Ifc4x1 => attribute_names_ifc4x1(entity_name),
66            Self::Ifc4x2 => attribute_names_ifc4x2(entity_name),
67            Self::Ifc4x3 => attribute_names_ifc4x3(entity_name),
68        }
69    }
70
71    /// Membership, direct parent, and abstractness from this schema's EXPRESS
72    /// declaration. Unknown names fail closed.
73    pub fn entity_info(self, entity_name: &str) -> Option<SchemaEntityInfo> {
74        match self {
75            Self::Ifc2x3 => entity_info_ifc2x3(entity_name),
76            Self::Ifc4 => entity_info_ifc4(entity_name),
77            Self::Ifc4x1 => entity_info_ifc4x1(entity_name),
78            Self::Ifc4x2 => entity_info_ifc4x2(entity_name),
79            Self::Ifc4x3 => entity_info_ifc4x3(entity_name),
80        }
81    }
82
83    /// Whether `child` inherits from `parent` in this schema's own graph.
84    pub fn is_subtype_of(self, child: &str, parent: &str) -> bool {
85        let mut current = self.entity_info(child);
86        while let Some(info) = current {
87            if info.name.eq_ignore_ascii_case(parent) {
88                return true;
89            }
90            current = info.parent.and_then(|name| self.entity_info(name));
91        }
92        false
93    }
94}
95
96/// Exact family name, optionally followed by standard revision components
97/// such as `_ADD2`, `_TC1`, or `_RC4`.
98fn is_schema_family(label: &str, family: &str) -> bool {
99    let Some(suffix) = label.strip_prefix(family) else {
100        return false;
101    };
102    if suffix.is_empty() {
103        return true;
104    }
105    let Some(revision) = suffix.strip_prefix('_') else {
106        return false;
107    };
108    revision.split('_').all(|component| {
109        ["ADD", "TC", "RC"].iter().any(|prefix| {
110            component.strip_prefix(prefix).is_some_and(|number| {
111                !number.is_empty() && number.bytes().all(|b| b.is_ascii_digit())
112            })
113        })
114    })
115}
116
117fn attribute_names_ifc2x3(name: &str) -> Option<&'static [&'static str]> {
118    let ty = super::ifc2x3::IfcType::from_str(name);
119    if matches!(ty, super::ifc2x3::IfcType::Unknown(_)) {
120        None
121    } else {
122        Some(ty.attribute_names())
123    }
124}
125
126fn entity_info_ifc2x3(name: &str) -> Option<SchemaEntityInfo> {
127    let ty = super::ifc2x3::IfcType::from_str(name);
128    if matches!(ty, super::ifc2x3::IfcType::Unknown(_)) {
129        None
130    } else {
131        Some(SchemaEntityInfo {
132            name: ty.known_as_str()?,
133            parent: ty.parent().and_then(|parent| parent.known_as_str()),
134            is_abstract: ty.is_abstract(),
135        })
136    }
137}
138
139fn attribute_names_ifc4(name: &str) -> Option<&'static [&'static str]> {
140    let ty = super::ifc4::IfcType::from_str(name);
141    if matches!(ty, super::ifc4::IfcType::Unknown(_)) {
142        None
143    } else {
144        Some(ty.attribute_names())
145    }
146}
147
148fn entity_info_ifc4(name: &str) -> Option<SchemaEntityInfo> {
149    let ty = super::ifc4::IfcType::from_str(name);
150    if matches!(ty, super::ifc4::IfcType::Unknown(_)) {
151        None
152    } else {
153        Some(SchemaEntityInfo {
154            name: ty.known_as_str()?,
155            parent: ty.parent().and_then(|parent| parent.known_as_str()),
156            is_abstract: ty.is_abstract(),
157        })
158    }
159}
160
161fn attribute_names_ifc4x1(name: &str) -> Option<&'static [&'static str]> {
162    let ty = super::ifc4x1::IfcType::from_str(name);
163    if matches!(ty, super::ifc4x1::IfcType::Unknown(_)) {
164        None
165    } else {
166        Some(ty.attribute_names())
167    }
168}
169
170fn entity_info_ifc4x1(name: &str) -> Option<SchemaEntityInfo> {
171    let ty = super::ifc4x1::IfcType::from_str(name);
172    if matches!(ty, super::ifc4x1::IfcType::Unknown(_)) {
173        None
174    } else {
175        Some(SchemaEntityInfo {
176            name: ty.known_as_str()?,
177            parent: ty.parent().and_then(|parent| parent.known_as_str()),
178            is_abstract: ty.is_abstract(),
179        })
180    }
181}
182
183fn attribute_names_ifc4x2(name: &str) -> Option<&'static [&'static str]> {
184    let ty = super::ifc4x2::IfcType::from_str(name);
185    if matches!(ty, super::ifc4x2::IfcType::Unknown(_)) {
186        None
187    } else {
188        Some(ty.attribute_names())
189    }
190}
191
192fn entity_info_ifc4x2(name: &str) -> Option<SchemaEntityInfo> {
193    let ty = super::ifc4x2::IfcType::from_str(name);
194    if matches!(ty, super::ifc4x2::IfcType::Unknown(_)) {
195        None
196    } else {
197        Some(SchemaEntityInfo {
198            name: ty.known_as_str()?,
199            parent: ty.parent().and_then(|parent| parent.known_as_str()),
200            is_abstract: ty.is_abstract(),
201        })
202    }
203}
204
205fn attribute_names_ifc4x3(name: &str) -> Option<&'static [&'static str]> {
206    // The canonical enum is the whole supported-schema universe (#4203): a
207    // legacy class such as IfcDoorStyle resolves to an exact variant there,
208    // but IFC4X3 does not declare it, so this registry must still answer
209    // `None` rather than lend it an empty (or borrowed) slot layout.
210    let ty = super::schema::IfcType::from_str(name);
211    if ty.declared_by_canonical_schema() {
212        Some(ty.attribute_names())
213    } else {
214        None
215    }
216}
217
218fn entity_info_ifc4x3(name: &str) -> Option<SchemaEntityInfo> {
219    let ty = super::schema::IfcType::from_str(name);
220    if !ty.declared_by_canonical_schema() {
221        return None;
222    }
223    Some(SchemaEntityInfo {
224        name: ty.known_as_str()?,
225        parent: ty.parent().and_then(|parent| parent.known_as_str()),
226        is_abstract: ty.is_abstract(),
227    })
228}
229
230/// Attribute names for an entity in the source schema named by `FILE_SCHEMA`.
231pub fn attribute_names_for_schema(
232    file_schema: &str,
233    entity_name: &str,
234) -> Option<&'static [&'static str]> {
235    SchemaVersion::from_file_schema(file_schema)?.attribute_names(entity_name)
236}
237
238/// Schema-local facts for an entity in the source schema named by
239/// `FILE_SCHEMA`.
240pub fn entity_info_for_schema(file_schema: &str, entity_name: &str) -> Option<SchemaEntityInfo> {
241    SchemaVersion::from_file_schema(file_schema)?.entity_info(entity_name)
242}
243
244/// Schema-local inheritance test for a source `FILE_SCHEMA`.
245pub fn is_subtype_of_for_schema(file_schema: &str, child: &str, parent: &str) -> bool {
246    SchemaVersion::from_file_schema(file_schema).is_some_and(|schema| schema.is_subtype_of(child, parent))
247}