Skip to main content

ifc_schema/
registry.rs

1//! The assembled, queryable IFC schema.
2//!
3//! # What this owns
4//!
5//! Supertype chains, Part 21 positional attribute order, case-insensitive
6//! lookup and defined-type alias resolution. They are not IFC concepts --
7//! every EXPRESS schema serialized as Part 21 shares them -- but this crate
8//! answers them over its own declaration types, so decoding and querying a
9//! bundled table needs no parser crate. The walks follow the same rules as
10//! `openbim_step::SchemaGraph` (ISO 10303-21 §12.2.5.2 slot order,
11//! case-folded lookup, bounded chains).
12//!
13//! What is genuinely IFC here: which schema *version* a file declares (the
14//! `FILE_SCHEMA` tokens), and each version's independently bundled tables.
15//!
16//! Parsing EXPRESS text (`Schema::from_express`) needs the opt-in `express`
17//! feature. A schema can always be assembled from declarations:
18//!
19//! ```
20//! use ifc_schema::{Attribute, EntityDef, Schema};
21//!
22//! let schema = Schema::new(
23//!     "IFC4",
24//!     vec![
25//!         EntityDef::new("IfcRoot")
26//!             .with_attribute(Attribute::new("GlobalId", "IfcGloballyUniqueId")),
27//!         EntityDef::new("IfcWall")
28//!             .with_supertype("IfcRoot")
29//!             .with_attribute(Attribute::new("Name", "IfcLabel")),
30//!     ],
31//!     Vec::new(),
32//! );
33//!
34//! assert!(schema.is_a("IFCWALL", "IfcRoot"));
35//! assert_eq!(schema.attribute_names("IfcWall"), ["GlobalId", "Name"]);
36//! ```
37
38use std::collections::{HashMap, HashSet};
39
40use crate::attribute::Attribute;
41use crate::entity::EntityDef;
42use crate::types::{TypeDef, TypeKind};
43use crate::version::SchemaVersion;
44
45/// Longest supertype or alias chain walked before giving up.
46///
47/// A cyclic `SUBTYPE OF` is not legal EXPRESS, but a malformed source must
48/// not hang a consumer. Real schemas nest around a dozen levels.
49const MAX_CHAIN_DEPTH: usize = 64;
50
51/// An IFC schema: the entity and type tables, queryable.
52///
53/// Every lookup folds ASCII case: Part 21 spells `IFCWALL`, the schema
54/// `IfcWall`, and either may be passed.
55#[derive(Debug, Clone)]
56pub struct Schema {
57    name: String,
58    /// Declarations in source order. A name declared twice keeps its first
59    /// position and its last declaration.
60    entities: Vec<EntityDef>,
61    types: Vec<TypeDef>,
62    /// Upper-cased name to index into `entities` / `types`.
63    entity_index: HashMap<String, usize>,
64    type_index: HashMap<String, usize>,
65    /// Upper-cased supertype name to the declared names of its direct
66    /// subtypes, sorted. Derived once: `EntityDef` records only the upward
67    /// edge, and scanning every declaration per query is quadratic.
68    children: HashMap<String, Vec<String>>,
69}
70
71/// Equal when the name and the declarations, in order, are equal.
72impl PartialEq for Schema {
73    fn eq(&self, other: &Self) -> bool {
74        self.name == other.name && self.entities == other.entities && self.types == other.types
75    }
76}
77
78impl Eq for Schema {}
79
80impl Schema {
81    /// Assembles a schema from declarations in source order.
82    ///
83    /// A name declared more than once keeps its first position and its last
84    /// declaration, so counts are of distinct names.
85    #[must_use]
86    pub fn new(name: impl Into<String>, entities: Vec<EntityDef>, types: Vec<TypeDef>) -> Self {
87        let (entities, entity_index) = index_by_name(entities, |entity| &entity.name);
88        let (types, type_index) = index_by_name(types, |type_def| &type_def.name);
89        let mut children: HashMap<String, Vec<String>> = HashMap::new();
90        for entity in &entities {
91            for supertype in &entity.supertypes {
92                children
93                    .entry(supertype.to_ascii_uppercase())
94                    .or_default()
95                    .push(entity.name.clone());
96            }
97        }
98        for names in children.values_mut() {
99            names.sort_unstable_by_key(|name| name.to_ascii_uppercase());
100            // `SUBTYPE OF (a, a)` is illegal but must not double-report.
101            names.dedup_by(|a, b| a.eq_ignore_ascii_case(b));
102        }
103        Self {
104            name: name.into(),
105            entities,
106            types,
107            entity_index,
108            type_index,
109            children,
110        }
111    }
112
113    /// Parses EXPRESS source into a schema.
114    ///
115    /// Requires the `express` feature, which links the `openbim-step`
116    /// EXPRESS extractor. Its result is converted into this crate's own
117    /// types; explicit attribute redeclarations are not recorded.
118    #[cfg(feature = "express")]
119    #[must_use]
120    pub fn from_express(source: &str) -> Self {
121        crate::express::parse(source)
122    }
123
124    /// Parses EXPRESS source that is not valid UTF-8.
125    ///
126    /// The normative `IFC4.exp` is Latin-1: it contains `°` and similar in
127    /// comments. Decoding byte-per-char is correct for the ASCII structure
128    /// this parser reads and cannot fail. Requires the `express` feature.
129    #[cfg(feature = "express")]
130    #[must_use]
131    pub fn from_express_bytes(bytes: &[u8]) -> Self {
132        let text: String = bytes.iter().map(|&byte| byte as char).collect();
133        Self::from_express(&text)
134    }
135
136    /// The declared schema name, e.g. `IFC4`.
137    #[must_use]
138    pub fn name(&self) -> &str {
139        &self.name
140    }
141
142    /// The IFC schema version this table describes, when recognized.
143    ///
144    /// This is the one genuinely IFC-specific query on this type: it maps a
145    /// declared schema name onto the versions this crate knows about.
146    #[must_use]
147    pub fn version(&self) -> Option<SchemaVersion> {
148        SchemaVersion::from_header_token(&self.name)
149    }
150
151    /// How many entity declarations the schema holds.
152    #[must_use]
153    pub fn entity_count(&self) -> usize {
154        self.entities.len()
155    }
156
157    /// How many type declarations the schema holds.
158    #[must_use]
159    pub fn type_count(&self) -> usize {
160        self.types.len()
161    }
162
163    /// The entity declaration for `name`, if the schema declares one.
164    #[must_use]
165    pub fn entity(&self, name: &str) -> Option<&EntityDef> {
166        self.entity_index
167            .get(&name.to_ascii_uppercase())
168            .map(|&index| &self.entities[index])
169    }
170
171    /// The type declaration for `name`, if the schema declares one.
172    #[must_use]
173    pub fn type_def(&self, name: &str) -> Option<&TypeDef> {
174        self.type_index
175            .get(&name.to_ascii_uppercase())
176            .map(|&index| &self.types[index])
177    }
178
179    /// Every entity declaration, in source order.
180    pub fn entities(&self) -> impl Iterator<Item = &EntityDef> {
181        self.entities.iter()
182    }
183
184    /// Every type declaration, in source order.
185    pub fn types(&self) -> impl Iterator<Item = &TypeDef> {
186        self.types.iter()
187    }
188
189    /// Every entity name the schema declares.
190    ///
191    /// Source order today; callers needing a particular order must sort.
192    pub fn entity_names(&self) -> impl Iterator<Item = &str> {
193        self.entities.iter().map(|entity| entity.name.as_str())
194    }
195
196    /// Whether a candidate entity or defined type satisfies a declared type.
197    ///
198    /// This walks entity inheritance, defined-type aliases, and nested SELECTs.
199    /// Unknown declarations and cyclic aliases fail closed.
200    #[must_use]
201    pub fn accepts_type(&self, declared: &str, candidate: &str) -> bool {
202        self.accepts_type_inner(declared, candidate, &mut HashSet::new(), 32)
203    }
204
205    fn accepts_type_inner(
206        &self,
207        declared: &str,
208        candidate: &str,
209        seen: &mut HashSet<(String, String)>,
210        depth: usize,
211    ) -> bool {
212        if declared.eq_ignore_ascii_case(candidate) {
213            return self.entity(declared).is_some() || self.type_def(declared).is_some();
214        }
215        if depth == 0
216            || !seen.insert((
217                declared.to_ascii_uppercase(),
218                candidate.to_ascii_uppercase(),
219            ))
220        {
221            return false;
222        }
223        if self.entity(declared).is_some() && self.entity(candidate).is_some() {
224            return self.is_a(candidate, declared);
225        }
226        if let Some(definition) = self.type_def(declared) {
227            match &definition.kind {
228                TypeKind::Defined(alias) => {
229                    if self.accepts_type_inner(alias, candidate, seen, depth - 1) {
230                        return true;
231                    }
232                }
233                TypeKind::Select(members) => {
234                    if members
235                        .iter()
236                        .any(|member| self.accepts_type_inner(member, candidate, seen, depth - 1))
237                    {
238                        return true;
239                    }
240                }
241                TypeKind::Enumeration(_) => {}
242            }
243        }
244        if let Some(definition) = self.type_def(candidate) {
245            if let TypeKind::Defined(alias) = &definition.kind {
246                return self.accepts_type_inner(declared, alias, seen, depth - 1);
247            }
248        }
249        false
250    }
251
252    /// Whether `name` is `ancestor`, or inherits from it.
253    ///
254    /// Reflexive for declared entities. An entity the schema never declares
255    /// is not a subtype of anything, including itself -- otherwise a typo
256    /// would silently satisfy every check made against it.
257    #[must_use]
258    pub fn is_a(&self, name: &str, ancestor: &str) -> bool {
259        if name.eq_ignore_ascii_case(ancestor) {
260            return self.entity(name).is_some();
261        }
262        self.supertypes(name)
263            .iter()
264            .any(|super_name| super_name.eq_ignore_ascii_case(ancestor))
265    }
266
267    /// The supertype chain above `name`, nearest parent first.
268    ///
269    /// Depth-first in `SUBTYPE OF` order, each ancestor once, excluding
270    /// `name`. A supertype the schema names but never declares is reported
271    /// but cannot be walked past. Terminates on a malformed cyclic schema.
272    #[must_use]
273    pub fn supertypes(&self, name: &str) -> Vec<&str> {
274        let mut seen = HashSet::new();
275        seen.insert(name.to_ascii_uppercase());
276        let mut out = Vec::new();
277        self.collect_supertypes(name, 0, &mut seen, &mut out);
278        out
279    }
280
281    fn collect_supertypes<'s>(
282        &'s self,
283        name: &str,
284        depth: usize,
285        seen: &mut HashSet<String>,
286        out: &mut Vec<&'s str>,
287    ) {
288        if depth >= MAX_CHAIN_DEPTH {
289            return;
290        }
291        let Some(def) = self.entity(name) else {
292            return;
293        };
294        for supertype in &def.supertypes {
295            if !seen.insert(supertype.to_ascii_uppercase()) {
296                continue;
297            }
298            match self.entity(supertype) {
299                Some(parent) => {
300                    out.push(parent.name.as_str());
301                    self.collect_supertypes(&parent.name, depth + 1, seen, out);
302                }
303                None => out.push(supertype.as_str()),
304            }
305        }
306    }
307
308    /// Entities declaring `SUBTYPE OF (name)` directly, sorted by name.
309    #[must_use]
310    pub fn direct_subtypes(&self, name: &str) -> Vec<&str> {
311        self.children
312            .get(&name.to_ascii_uppercase())
313            .map(|names| names.iter().map(String::as_str).collect())
314            .unwrap_or_default()
315    }
316
317    /// Every entity inheriting from `name` at any depth, excluding `name`.
318    ///
319    /// Sorted depth-first pre-order. The inverse of [`Self::is_a`].
320    #[must_use]
321    pub fn subtypes(&self, name: &str) -> Vec<&str> {
322        let mut seen = HashSet::new();
323        seen.insert(name.to_ascii_uppercase());
324        let mut out = Vec::new();
325        // Children are pushed in reverse so the sorted first sibling pops
326        // first, which is what makes this a pre-order and not a post-order.
327        let mut stack: Vec<&str> = self.direct_subtypes(name).into_iter().rev().collect();
328        while let Some(current) = stack.pop() {
329            if !seen.insert(current.to_ascii_uppercase()) {
330                continue;
331            }
332            out.push(current);
333            stack.extend(self.direct_subtypes(current).into_iter().rev());
334        }
335        out
336    }
337
338    /// Every attribute slot in Part 21 positional order, inherited first.
339    ///
340    /// ISO 10303-21:2016 §12.2.5.2: each supertype is laid out in
341    /// `SUBTYPE OF` order, its own supertypes first, and a supertype reached
342    /// a second time contributes nothing more. Derived redeclarations are
343    /// included (they keep their slot and are written `*`); use
344    /// [`EntityDef::is_derived`] on the owning entity to tell them apart.
345    #[must_use]
346    pub fn attributes(&self, name: &str) -> Vec<&Attribute> {
347        let mut seen = HashSet::new();
348        let mut out = Vec::new();
349        self.collect_attributes(name, 0, &mut seen, &mut out);
350        out
351    }
352
353    fn collect_attributes<'s>(
354        &'s self,
355        name: &str,
356        depth: usize,
357        seen: &mut HashSet<String>,
358        out: &mut Vec<&'s Attribute>,
359    ) {
360        if depth > MAX_CHAIN_DEPTH {
361            return;
362        }
363        let Some(def) = self.entity(name) else {
364            return;
365        };
366        if !seen.insert(def.name.to_ascii_uppercase()) {
367            return;
368        }
369        for supertype in &def.supertypes {
370            self.collect_attributes(supertype, depth + 1, seen, out);
371        }
372        out.extend(def.attributes.iter());
373    }
374
375    /// Attribute names in positional order.
376    #[must_use]
377    pub fn attribute_names(&self, name: &str) -> Vec<&str> {
378        self.attributes(name)
379            .into_iter()
380            .map(|attribute| attribute.name.as_str())
381            .collect()
382    }
383
384    /// Resolves a defined type to the base it ultimately aliases.
385    ///
386    /// `IfcPositiveLengthMeasure` -> `IfcLengthMeasure` -> `REAL`. Returns
387    /// the final right-hand side verbatim (including any aggregate syntax);
388    /// for a declaration that is not an alias, the type's own name. Bounded
389    /// so a cyclic alias cannot hang.
390    #[must_use]
391    pub fn resolve_defined(&self, name: &str) -> String {
392        let mut current = name.to_string();
393        for _ in 0..MAX_CHAIN_DEPTH {
394            let Some(def) = self.type_def(&current) else {
395                return current;
396            };
397            let TypeKind::Defined(target) = &def.kind else {
398                return current;
399            };
400            let next = target.trim().to_string();
401            if next.eq_ignore_ascii_case(&current) {
402                return current;
403            }
404            current = next;
405        }
406        current
407    }
408}
409
410/// Deduplicates declarations by case-folded name (first position, last
411/// declaration wins) and builds the lookup index.
412fn index_by_name<T>(
413    declarations: Vec<T>,
414    name: impl Fn(&T) -> &String,
415) -> (Vec<T>, HashMap<String, usize>) {
416    let mut kept: Vec<T> = Vec::with_capacity(declarations.len());
417    let mut index = HashMap::with_capacity(declarations.len());
418    for declaration in declarations {
419        let key = name(&declaration).to_ascii_uppercase();
420        match index.get(&key) {
421            Some(&position) => kept[position] = declaration,
422            None => {
423                index.insert(key, kept.len());
424                kept.push(declaration);
425            }
426        }
427    }
428    (kept, index)
429}
430
431#[cfg(test)]
432mod tests {
433    use super::*;
434
435    /// A real three-level chain: two levels cannot catch ordering bugs.
436    fn chain() -> Schema {
437        Schema::new(
438            "IFC4",
439            vec![
440                EntityDef::new("IfcRoot")
441                    .abstract_entity()
442                    .with_attribute(Attribute::new("GlobalId", "IfcGloballyUniqueId"))
443                    .with_attribute(Attribute::new("OwnerHistory", "IfcOwnerHistory").optional())
444                    .with_attribute(Attribute::new("Name", "IfcLabel").optional())
445                    .with_attribute(Attribute::new("Description", "IfcText").optional()),
446                EntityDef::new("IfcObjectDefinition")
447                    .abstract_entity()
448                    .with_supertype("IfcRoot"),
449                EntityDef::new("IfcObject")
450                    .with_supertype("IfcObjectDefinition")
451                    .with_attribute(Attribute::new("ObjectType", "IfcLabel").optional()),
452            ],
453            vec![
454                TypeDef::new("IfcLengthMeasure", TypeKind::Defined("REAL".into())),
455                TypeDef::new(
456                    "IfcPositiveLengthMeasure",
457                    TypeKind::Defined("IfcLengthMeasure".into()),
458                ),
459            ],
460        )
461    }
462
463    #[test]
464    fn the_declared_schema_name_maps_onto_a_known_ifc_version() {
465        let schema = chain();
466        assert_eq!(schema.name(), "IFC4");
467        assert_eq!(schema.version(), Some(SchemaVersion::Ifc4));
468    }
469
470    /// A schema this crate does not recognize still assembles and queries.
471    #[test]
472    fn an_unrecognized_schema_name_has_no_version_but_still_works() {
473        let schema = Schema::new(
474            "AP242",
475            vec![EntityDef::new("Product").with_attribute(Attribute::new("Id", "Identifier"))],
476            Vec::new(),
477        );
478        assert_eq!(schema.version(), None, "not an IFC schema");
479        assert_eq!(schema.attribute_names("Product"), ["Id"]);
480    }
481
482    #[test]
483    fn inherited_attributes_come_first_in_positional_order() {
484        assert_eq!(
485            chain().attribute_names("IFCOBJECT"),
486            [
487                "GlobalId",
488                "OwnerHistory",
489                "Name",
490                "Description",
491                "ObjectType"
492            ],
493        );
494    }
495
496    #[test]
497    fn defined_types_resolve_through_the_alias_chain() {
498        assert_eq!(chain().resolve_defined("IfcPositiveLengthMeasure"), "REAL");
499    }
500
501    #[test]
502    fn subtype_tests_cross_intermediate_levels_and_are_reflexive_only_when_declared() {
503        let schema = chain();
504        assert!(schema.is_a("IFCOBJECT", "IfcRoot"));
505        assert!(schema.is_a("IfcObject", "IfcObject"));
506        assert!(!schema.is_a("IfcRoot", "IfcObject"));
507        assert!(!schema.is_a("NotAThing", "NotAThing"));
508        assert_eq!(
509            schema.subtypes("ifcroot"),
510            ["IfcObjectDefinition", "IfcObject"]
511        );
512        assert_eq!(
513            schema.supertypes("IfcObject"),
514            ["IfcObjectDefinition", "IfcRoot"]
515        );
516    }
517
518    #[test]
519    fn cyclic_chains_terminate() {
520        let schema = Schema::new(
521            "S",
522            vec![
523                EntityDef::new("A").with_supertype("B"),
524                EntityDef::new("B").with_supertype("A"),
525            ],
526            vec![
527                TypeDef::new("X", TypeKind::Defined("Y".into())),
528                TypeDef::new("Y", TypeKind::Defined("X".into())),
529            ],
530        );
531        assert_eq!(schema.supertypes("A"), ["B"]);
532        assert_eq!(schema.subtypes("A"), ["B"]);
533        let resolved = schema.resolve_defined("X");
534        assert!(resolved == "X" || resolved == "Y");
535    }
536
537    #[test]
538    fn an_undeclared_supertype_is_still_named_in_both_directions() {
539        let schema = Schema::new(
540            "S",
541            vec![EntityDef::new("A").with_supertype("Missing")],
542            Vec::new(),
543        );
544        assert_eq!(schema.supertypes("A"), ["Missing"]);
545        assert!(schema.is_a("A", "Missing"));
546        assert_eq!(schema.subtypes("Missing"), ["A"]);
547    }
548
549    #[test]
550    fn a_repeated_name_keeps_its_first_position_and_last_declaration() {
551        let schema = Schema::new(
552            "S",
553            vec![
554                EntityDef::new("A"),
555                EntityDef::new("B"),
556                EntityDef::new("a").with_attribute(Attribute::new("X", "INTEGER")),
557            ],
558            Vec::new(),
559        );
560        assert_eq!(schema.entity_count(), 2);
561        assert_eq!(schema.entity_names().collect::<Vec<_>>(), ["a", "B"]);
562        assert_eq!(schema.attribute_names("A"), ["X"]);
563    }
564}
565
566/// The owned walks must answer exactly as `openbim_step::SchemaGraph` did,
567/// which this crate delegated to before owning its types.
568#[cfg(all(test, feature = "express"))]
569mod parity_with_step {
570    use super::*;
571
572    const TREE: &str = "\
573SCHEMA TREE;
574ENTITY Root; Id : INTEGER; END_ENTITY;
575ENTITY Wall SUBTYPE OF (Root); Name : OPTIONAL STRING; END_ENTITY;
576ENTITY Door SUBTYPE OF (Root); END_ENTITY;
577ENTITY WallStandardCase SUBTYPE OF (Wall); Tag : LIST [1:?] OF STRING; END_ENTITY;
578ENTITY WallElementedCase SUBTYPE OF (Wall); END_ENTITY;
579ENTITY Orphan SUBTYPE OF (Missing); END_ENTITY;
580ENTITY Unrelated; END_ENTITY;
581TYPE Count = INTEGER; END_TYPE;
582TYPE PositiveCount = Count; END_TYPE;
583TYPE Colour = ENUMERATION OF (RED, GREEN); END_TYPE;
584END_SCHEMA;";
585
586    #[test]
587    fn every_query_matches_the_step_schema_graph() {
588        let owned = Schema::from_express(TREE);
589        let graph = openbim_step::SchemaGraph::from_express(TREE);
590        assert_eq!(owned.name(), graph.name());
591        assert_eq!(owned.entity_count(), graph.entity_count());
592        assert_eq!(owned.type_count(), graph.type_count());
593        let mut names: Vec<&str> = graph.entity_names().collect();
594        names.extend(["Missing", "NotAThing"]);
595        for &name in &names {
596            assert_eq!(owned.supertypes(name), graph.supertypes(name), "{name}");
597            assert_eq!(owned.subtypes(name), graph.subtypes(name), "{name}");
598            assert_eq!(
599                owned.direct_subtypes(name),
600                graph.direct_subtypes(name),
601                "{name}"
602            );
603            assert_eq!(
604                owned.attribute_names(name),
605                graph.attribute_names(name),
606                "{name}"
607            );
608            for &ancestor in &names {
609                assert_eq!(
610                    owned.is_a(name, ancestor),
611                    graph.is_a(name, ancestor),
612                    "{name} / {ancestor}"
613                );
614            }
615        }
616        for name in ["PositiveCount", "Colour", "Count", "Unknown"] {
617            assert_eq!(owned.resolve_defined(name), graph.resolve_defined(name));
618        }
619    }
620}