Skip to main content

ifc_schema/
registry.rs

1//! The assembled, queryable IFC schema.
2//!
3//! # What this owns, and what it delegates
4//!
5//! Supertype chains, Part 21 positional attribute order, case-insensitive
6//! lookup and defined-type alias resolution are not IFC concepts: every
7//! EXPRESS schema serialized as Part 21 shares them. They live in
8//! [`openbim_step::SchemaGraph`], and this type delegates to it.
9//!
10//! What stays here is genuinely IFC: which schema *version* a file declares
11//! (the `IFC2X3`/`IFC4`/`IFC4X3` tokens), and each version's independently
12//! bundled tables.
13//!
14//! ```
15//! use ifc_schema::Schema;
16//!
17//! let schema = Schema::from_express(
18//!     "SCHEMA IFC4;\n\
19//!      ENTITY IfcRoot; GlobalId : IfcGloballyUniqueId; END_ENTITY;\n\
20//!      ENTITY IfcWall SUBTYPE OF (IfcRoot); Name : IfcLabel; END_ENTITY;\n\
21//!      END_SCHEMA;",
22//! );
23//!
24//! assert!(schema.is_a("IFCWALL", "IfcRoot"));
25//! assert_eq!(schema.attribute_names("IfcWall"), ["GlobalId", "Name"]);
26//! ```
27
28use std::collections::HashSet;
29
30use openbim_step::express::{Attribute, EntityDef, ParsedSchema, TypeDef, TypeKind};
31use openbim_step::SchemaGraph;
32
33use crate::version::SchemaVersion;
34
35/// An IFC schema: the entity and type tables, queryable.
36#[derive(Debug, Clone)]
37pub struct Schema {
38    graph: SchemaGraph,
39}
40
41impl Schema {
42    /// Wraps an already-parsed schema.
43    #[must_use]
44    pub fn from_parsed(parsed: ParsedSchema) -> Self {
45        Self {
46            graph: SchemaGraph::new(parsed),
47        }
48    }
49
50    /// Parses EXPRESS source into a schema.
51    #[must_use]
52    pub fn from_express(source: &str) -> Self {
53        Self {
54            graph: SchemaGraph::from_express(source),
55        }
56    }
57
58    /// Parses EXPRESS source that is not valid UTF-8.
59    ///
60    /// The normative `IFC4.exp` is Latin-1: it contains `°` and similar in
61    /// comments. Decoding byte-per-char is correct for the ASCII structure
62    /// this parser reads and cannot fail.
63    #[must_use]
64    pub fn from_express_bytes(bytes: &[u8]) -> Self {
65        let text: String = bytes.iter().map(|&byte| byte as char).collect();
66        Self::from_express(&text)
67    }
68
69    /// The declared schema name, e.g. `IFC4`.
70    #[must_use]
71    pub fn name(&self) -> &str {
72        self.graph.name()
73    }
74
75    /// The IFC schema version this table describes, when recognized.
76    ///
77    /// This is the one genuinely IFC-specific query on this type: it maps a
78    /// declared schema name onto the versions this crate knows about.
79    #[must_use]
80    pub fn version(&self) -> Option<SchemaVersion> {
81        SchemaVersion::from_header_token(self.graph.name())
82    }
83
84    /// How many entity declarations the schema holds.
85    #[must_use]
86    pub fn entity_count(&self) -> usize {
87        self.graph.entity_count()
88    }
89
90    /// How many type declarations the schema holds.
91    #[must_use]
92    pub fn type_count(&self) -> usize {
93        self.graph.type_count()
94    }
95
96    /// The entity declaration for `name`, if the schema declares one.
97    #[must_use]
98    pub fn entity(&self, name: &str) -> Option<&EntityDef> {
99        self.graph.entity(name)
100    }
101
102    /// The type declaration for `name`, if the schema declares one.
103    #[must_use]
104    pub fn type_def(&self, name: &str) -> Option<&TypeDef> {
105        self.graph.type_def(name)
106    }
107
108    /// Every entity name the schema declares, in unspecified order.
109    pub fn entity_names(&self) -> impl Iterator<Item = &str> {
110        self.graph.entity_names()
111    }
112
113    /// Whether a candidate entity or defined type satisfies a declared type.
114    ///
115    /// This walks entity inheritance, defined-type aliases, and nested SELECTs.
116    /// Unknown declarations and cyclic aliases fail closed.
117    #[must_use]
118    pub fn accepts_type(&self, declared: &str, candidate: &str) -> bool {
119        self.accepts_type_inner(declared, candidate, &mut HashSet::new(), 32)
120    }
121
122    fn accepts_type_inner(
123        &self,
124        declared: &str,
125        candidate: &str,
126        seen: &mut HashSet<(String, String)>,
127        depth: usize,
128    ) -> bool {
129        if declared.eq_ignore_ascii_case(candidate) {
130            return self.entity(declared).is_some() || self.type_def(declared).is_some();
131        }
132        if depth == 0
133            || !seen.insert((
134                declared.to_ascii_uppercase(),
135                candidate.to_ascii_uppercase(),
136            ))
137        {
138            return false;
139        }
140        if self.entity(declared).is_some() && self.entity(candidate).is_some() {
141            return self.is_a(candidate, declared);
142        }
143        if let Some(definition) = self.type_def(declared) {
144            match &definition.kind {
145                TypeKind::Defined(alias) => {
146                    if self.accepts_type_inner(alias, candidate, seen, depth - 1) {
147                        return true;
148                    }
149                }
150                TypeKind::Select(members) => {
151                    if members
152                        .iter()
153                        .any(|member| self.accepts_type_inner(member, candidate, seen, depth - 1))
154                    {
155                        return true;
156                    }
157                }
158                TypeKind::Enumeration(_) => {}
159            }
160        }
161        if let Some(definition) = self.type_def(candidate) {
162            if let TypeKind::Defined(alias) = &definition.kind {
163                return self.accepts_type_inner(declared, alias, seen, depth - 1);
164            }
165        }
166        false
167    }
168
169    /// Whether `name` is `ancestor`, or inherits from it.
170    #[must_use]
171    pub fn is_a(&self, name: &str, ancestor: &str) -> bool {
172        self.graph.is_a(name, ancestor)
173    }
174
175    /// The supertype chain above `name`, nearest parent first.
176    #[must_use]
177    pub fn supertypes(&self, name: &str) -> Vec<&str> {
178        self.graph.supertypes(name)
179    }
180
181    /// Entities declaring `SUBTYPE OF (name)` directly, sorted by name.
182    #[must_use]
183    pub fn direct_subtypes(&self, name: &str) -> Vec<&str> {
184        self.graph.direct_subtypes(name)
185    }
186
187    /// Every entity inheriting from `name` at any depth, excluding `name`.
188    ///
189    /// Sorted depth-first pre-order. The inverse of [`Self::is_a`].
190    #[must_use]
191    pub fn subtypes(&self, name: &str) -> Vec<&str> {
192        self.graph.subtypes(name)
193    }
194
195    /// Every attribute slot in Part 21 positional order, inherited first.
196    #[must_use]
197    pub fn attributes(&self, name: &str) -> Vec<&Attribute> {
198        self.graph.attributes(name)
199    }
200
201    /// Attribute names in positional order.
202    #[must_use]
203    pub fn attribute_names(&self, name: &str) -> Vec<&str> {
204        self.graph.attribute_names(name)
205    }
206
207    /// Resolves a defined type to the base it ultimately aliases.
208    ///
209    /// `IfcPositiveLengthMeasure` -> `IfcLengthMeasure` -> `REAL`.
210    #[must_use]
211    pub fn resolve_defined(&self, name: &str) -> String {
212        self.graph.resolve_defined(name)
213    }
214
215    /// The underlying schema graph.
216    ///
217    /// Exposed so schema-neutral consumers can work against the generic type
218    /// rather than this IFC-flavoured wrapper.
219    #[must_use]
220    pub fn graph(&self) -> &SchemaGraph {
221        &self.graph
222    }
223}
224
225#[cfg(test)]
226mod tests {
227    use super::*;
228
229    const CHAIN: &str = "\
230SCHEMA IFC4;
231ENTITY IfcRoot
232 ABSTRACT SUPERTYPE OF (ONEOF(IfcObjectDefinition));
233  GlobalId : IfcGloballyUniqueId;
234  OwnerHistory : OPTIONAL IfcOwnerHistory;
235  Name : OPTIONAL IfcLabel;
236  Description : OPTIONAL IfcText;
237END_ENTITY;
238ENTITY IfcObjectDefinition
239 ABSTRACT SUPERTYPE OF (ONEOF(IfcObject))
240 SUBTYPE OF (IfcRoot);
241END_ENTITY;
242ENTITY IfcObject
243 SUBTYPE OF (IfcObjectDefinition);
244  ObjectType : OPTIONAL IfcLabel;
245END_ENTITY;
246TYPE IfcLengthMeasure = REAL; END_TYPE;
247TYPE IfcPositiveLengthMeasure = IfcLengthMeasure; END_TYPE;
248END_SCHEMA;";
249
250    #[test]
251    fn the_declared_schema_name_maps_onto_a_known_ifc_version() {
252        let schema = Schema::from_express(CHAIN);
253        assert_eq!(schema.name(), "IFC4");
254        assert_eq!(schema.version(), Some(SchemaVersion::Ifc4));
255    }
256
257    /// A schema this crate does not recognize still parses and queries.
258    #[test]
259    fn an_unrecognized_schema_name_has_no_version_but_still_works() {
260        let schema = Schema::from_express(
261            "SCHEMA AP242; ENTITY Product; Id : Identifier; END_ENTITY; END_SCHEMA;",
262        );
263        assert_eq!(schema.version(), None, "not an IFC schema");
264        assert_eq!(schema.attribute_names("Product"), ["Id"]);
265    }
266
267    #[test]
268    fn inherited_attributes_come_first_in_positional_order() {
269        assert_eq!(
270            Schema::from_express(CHAIN).attribute_names("IFCOBJECT"),
271            [
272                "GlobalId",
273                "OwnerHistory",
274                "Name",
275                "Description",
276                "ObjectType"
277            ],
278        );
279    }
280
281    #[test]
282    fn defined_types_resolve_through_the_alias_chain() {
283        assert_eq!(
284            Schema::from_express(CHAIN).resolve_defined("IfcPositiveLengthMeasure"),
285            "REAL"
286        );
287    }
288}