Skip to main content

openbim_step/schema/
mod.rs

1//! The parsed schema as a queryable graph.
2//!
3//! # Why this is not in `express`
4//!
5//! [`express::parse`](crate::express::parse) answers "what does this source
6//! text declare?". This module answers "given those declarations, what is
7//! true of entity X?" -- supertype chains, positional attribute order, type
8//! resolution. Those are questions about ISO 10303-11 semantics rather than
9//! syntax, and every schema-aware consumer needs them before it can interpret
10//! a positional Part 21 record.
11//!
12//! # Why it is not in a downstream crate
13//!
14//! It was in one. An application-schema crate carried this logic, but nothing
15//! here is specific to any single schema: AP203, AP214, AP242 and IFC are all
16//! EXPRESS schemas serialized as Part 21 records with identical inheritance
17//! and positional rules. Every consumer would otherwise reimplement it.
18//! Application layers keep what is genuinely theirs -- which schema version a
19//! file declares, and any bundled copy of their own tables.
20//!
21//! # The positional rule this exists to enforce
22//!
23//! A Part 21 record lists attributes **supertype-first, most general first**:
24//!
25//! ```text
26//! ENTITY Base;                    Id, Owner, Name
27//! ENTITY Derived SUBTYPE OF (Base);   ... then Derived's own
28//!
29//! #1=DERIVED('id',$,'Name',$, ...);
30//!             ^0   ^1  ^2    ^3     inherited slots come first
31//! ```
32//!
33//! Reversing that order misreads every attribute of every inheriting entity,
34//! silently, and the values still look plausible. That is why
35//! [`SchemaGraph::attributes`] is tested against a three-level chain rather
36//! than a synthetic two-level one -- two levels cannot distinguish
37//! "inherited first" from "declaring entity first".
38
39use std::collections::HashMap;
40
41mod complex;
42
43pub use complex::{ComplexIssue, ComplexLayout, ComplexPart, ComplexSlot};
44
45use crate::express::{Attribute, EntityDef, ParsedSchema, TypeDef, TypeKind};
46
47/// Longest supertype or alias chain this will walk before giving up.
48///
49/// A cyclic `SUBTYPE OF` is not legal EXPRESS, but this parser is deliberately
50/// tolerant and a malformed source must not hang a consumer. Real schemas nest
51/// around a dozen levels; 64 leaves generous room while still terminating.
52const MAX_CHAIN_DEPTH: usize = 64;
53
54/// Case-insensitive lookup over a parsed schema's declarations.
55///
56/// EXPRESS identifiers are case-insensitive, and Part 21 records conventionally
57/// spell entity names in upper case (`MYENTITY`) while schema sources spell
58/// them in mixed case (`MyEntity`). Every lookup here folds case so callers can
59/// pass whichever spelling they hold.
60#[derive(Debug, Clone)]
61pub struct SchemaGraph {
62    name: String,
63    entities: HashMap<String, EntityDef>,
64    types: HashMap<String, TypeDef>,
65    /// Upper-cased supertype name to the declared names of its direct
66    /// subtypes, sorted. Derived once at construction: `EntityDef` records
67    /// only the upward edge, and answering "what inherits from X" by scanning
68    /// every declaration per call is quadratic over a whole-model query.
69    children: HashMap<String, Vec<String>>,
70}
71
72impl SchemaGraph {
73    /// Indexes a parsed schema for querying.
74    #[must_use]
75    pub fn new(parsed: ParsedSchema) -> Self {
76        let entities: HashMap<String, EntityDef> = parsed
77            .entities
78            .into_iter()
79            .map(|entity| (entity.name.to_ascii_uppercase(), entity))
80            .collect();
81        let mut children: HashMap<String, Vec<String>> = HashMap::new();
82        for entity in entities.values() {
83            for supertype in &entity.supertypes {
84                children
85                    .entry(supertype.to_ascii_uppercase())
86                    .or_default()
87                    .push(entity.name.clone());
88            }
89        }
90        for names in children.values_mut() {
91            names.sort_unstable_by_key(|name| name.to_ascii_uppercase());
92            // `SUBTYPE OF (a, a)` is illegal but must not double-report.
93            names.dedup_by(|a, b| a.eq_ignore_ascii_case(b));
94        }
95        let types = parsed
96            .types
97            .into_iter()
98            .map(|type_def| (type_def.name.to_ascii_uppercase(), type_def))
99            .collect();
100        Self {
101            name: parsed.name,
102            entities,
103            types,
104            children,
105        }
106    }
107
108    /// Parses EXPRESS source and indexes it in one step.
109    #[must_use]
110    pub fn from_express(source: &str) -> Self {
111        Self::new(crate::express::parse(source))
112    }
113
114    /// The declared schema name.
115    #[must_use]
116    pub fn name(&self) -> &str {
117        &self.name
118    }
119
120    /// How many entity declarations the schema holds.
121    #[must_use]
122    pub fn entity_count(&self) -> usize {
123        self.entities.len()
124    }
125
126    /// How many type declarations the schema holds.
127    #[must_use]
128    pub fn type_count(&self) -> usize {
129        self.types.len()
130    }
131
132    /// The entity declaration for `name`, if the schema declares one.
133    #[must_use]
134    pub fn entity(&self, name: &str) -> Option<&EntityDef> {
135        self.entities.get(&name.to_ascii_uppercase())
136    }
137
138    /// The type declaration for `name`, if the schema declares one.
139    #[must_use]
140    pub fn type_def(&self, name: &str) -> Option<&TypeDef> {
141        self.types.get(&name.to_ascii_uppercase())
142    }
143
144    /// Every entity name the schema declares, in unspecified order.
145    ///
146    /// Callers needing determinism must sort: the underlying map order is
147    /// deliberately not promised.
148    pub fn entity_names(&self) -> impl Iterator<Item = &str> {
149        self.entities.values().map(|entity| entity.name.as_str())
150    }
151
152    /// Whether `name` is `ancestor`, or inherits from it.
153    ///
154    /// Reflexive for declared entities, matching EXPRESS subtype semantics
155    /// where a type belongs to its own subtype set. An entity the schema never
156    /// declares is not a subtype of anything, including itself -- otherwise a
157    /// typo would silently satisfy every check made against it.
158    #[must_use]
159    pub fn is_a(&self, name: &str, ancestor: &str) -> bool {
160        if name.eq_ignore_ascii_case(ancestor) {
161            return self.entities.contains_key(&name.to_ascii_uppercase());
162        }
163        self.supertypes(name)
164            .iter()
165            .any(|super_name| super_name.eq_ignore_ascii_case(ancestor))
166    }
167
168    /// The direct supertypes of `name`, in `SUBTYPE OF` order.
169    ///
170    /// Empty for a root entity and for an entity the schema never declares.
171    #[must_use]
172    pub fn direct_supertypes(&self, name: &str) -> Vec<&str> {
173        self.entity(name)
174            .map(|def| def.supertypes.iter().map(String::as_str).collect())
175            .unwrap_or_default()
176    }
177
178    /// Every ancestor of `name`, each once.
179    ///
180    /// Depth-first in `SUBTYPE OF` order: a parent, then that parent's own
181    /// ancestors, then the next parent. For single inheritance this is the
182    /// chain nearest parent first. Excludes `name` itself.
183    ///
184    /// A supertype the schema names but never declares is still reported --
185    /// a consumer checking `is_a` against a partial schema should see the
186    /// declared relationship -- but cannot be walked past. Terminates on a
187    /// malformed cyclic schema: each entity is visited once, and the walk is
188    /// bounded in depth.
189    #[must_use]
190    pub fn supertypes(&self, name: &str) -> Vec<&str> {
191        let mut seen = std::collections::HashSet::new();
192        seen.insert(name.to_ascii_uppercase());
193        let mut out = Vec::new();
194        self.collect_supertypes(name, 0, &mut seen, &mut out);
195        out
196    }
197
198    fn collect_supertypes<'s>(
199        &'s self,
200        name: &str,
201        depth: usize,
202        seen: &mut std::collections::HashSet<String>,
203        out: &mut Vec<&'s str>,
204    ) {
205        if depth >= MAX_CHAIN_DEPTH {
206            return;
207        }
208        let Some(def) = self.entity(name) else {
209            return;
210        };
211        for supertype in &def.supertypes {
212            if !seen.insert(supertype.to_ascii_uppercase()) {
213                continue;
214            }
215            match self.entity(supertype) {
216                Some(parent) => {
217                    out.push(parent.name.as_str());
218                    self.collect_supertypes(&parent.name, depth + 1, seen, out);
219                }
220                None => out.push(supertype.as_str()),
221            }
222        }
223    }
224
225    /// Entities declaring `SUBTYPE OF (name)` directly, sorted by name.
226    ///
227    /// Excludes `name` itself. Names a supertype the schema never declares
228    /// the same way [`Self::supertypes`] does: if `A SUBTYPE OF (Missing)`,
229    /// then `direct_subtypes("Missing")` is `["A"]`, keeping the two
230    /// directions consistent on a partial schema.
231    #[must_use]
232    pub fn direct_subtypes(&self, name: &str) -> Vec<&str> {
233        self.children
234            .get(&name.to_ascii_uppercase())
235            .map(|names| names.iter().map(String::as_str).collect())
236            .unwrap_or_default()
237    }
238
239    /// Every entity that inherits from `name`, at any depth.
240    ///
241    /// The downward counterpart of [`Self::supertypes`]: excludes `name`
242    /// itself, and `y` is in `subtypes(x)` exactly when `is_a(y, x)` holds
243    /// for `y != x`. Order is a depth-first pre-order with siblings sorted by
244    /// name, so the result is deterministic across runs and platforms.
245    ///
246    /// Terminates on a malformed cyclic schema: each entity is visited once.
247    #[must_use]
248    pub fn subtypes(&self, name: &str) -> Vec<&str> {
249        let mut seen = std::collections::HashSet::new();
250        seen.insert(name.to_ascii_uppercase());
251        let mut out = Vec::new();
252        // Children are pushed in reverse so the sorted first sibling pops
253        // first, which is what makes this a pre-order and not a post-order.
254        let mut stack: Vec<&str> = self.direct_subtypes(name).into_iter().rev().collect();
255        while let Some(current) = stack.pop() {
256            if !seen.insert(current.to_ascii_uppercase()) {
257                continue;
258            }
259            out.push(current);
260            stack.extend(self.direct_subtypes(current).into_iter().rev());
261        }
262        out
263    }
264
265    /// Every attribute slot in **Part 21 positional order**, inherited first.
266    ///
267    /// See the module documentation for why this ordering is load-bearing.
268    ///
269    /// ISO 10303-21:2016 §12.2.5.2: each supertype is laid out in
270    /// `SUBTYPE OF` order, its own supertypes first, and a supertype reached
271    /// a second time (the diamond of multiple inheritance) contributes
272    /// nothing more -- "all references after the first one shall be ignored".
273    ///
274    /// Derived redeclarations are *included*: they keep their inherited
275    /// position and are written `*` in a Part 21 record. Use
276    /// [`EntityDef::is_derived`] on the owning entity to tell them apart.
277    /// Explicit redeclarations (`SELF\X.a : T;`) add no slot (§12.2.8); the
278    /// parser keeps them out of [`EntityDef::attributes`].
279    #[must_use]
280    pub fn attributes(&self, name: &str) -> Vec<&Attribute> {
281        let mut seen = std::collections::HashSet::new();
282        let mut out = Vec::new();
283        self.collect_attributes(name, 0, &mut seen, &mut out);
284        out
285    }
286
287    fn collect_attributes<'s>(
288        &'s self,
289        name: &str,
290        depth: usize,
291        seen: &mut std::collections::HashSet<String>,
292        out: &mut Vec<&'s Attribute>,
293    ) {
294        if depth > MAX_CHAIN_DEPTH {
295            return;
296        }
297        let Some(def) = self.entity(name) else {
298            return;
299        };
300        if !seen.insert(def.name.to_ascii_uppercase()) {
301            return;
302        }
303        for supertype in &def.supertypes {
304            self.collect_attributes(supertype, depth + 1, seen, out);
305        }
306        out.extend(def.attributes.iter());
307    }
308
309    /// Attribute names in positional order.
310    ///
311    /// The bridge a positional-to-named mapping needs: slot `i` is called
312    /// `names[i]`.
313    #[must_use]
314    pub fn attribute_names(&self, name: &str) -> Vec<&str> {
315        self.attributes(name)
316            .into_iter()
317            .map(|attribute| attribute.name.as_str())
318            .collect()
319    }
320
321    /// Resolves a defined type to the base it ultimately aliases.
322    ///
323    /// A chain such as `PositiveCount -> Count -> INTEGER` resolves to
324    /// `INTEGER`. Returns the final right-hand side; for a declaration that is
325    /// not an alias, that is the type's own name. Bounded like the supertype
326    /// walk so a cyclic alias cannot hang.
327    ///
328    /// The right-hand side is returned verbatim, including any aggregate
329    /// syntax (`LIST [1:?] OF X`): discarding it here would throw away the
330    /// aggregate fact entirely, and callers that want the base scalar can
331    /// match on the text they receive.
332    #[must_use]
333    pub fn resolve_defined(&self, name: &str) -> String {
334        let mut current = name.to_string();
335        for _ in 0..MAX_CHAIN_DEPTH {
336            let Some(def) = self.type_def(&current) else {
337                return current;
338            };
339            let TypeKind::Defined(target) = &def.kind else {
340                return current;
341            };
342            let next = target.trim().to_string();
343            if next.eq_ignore_ascii_case(&current) {
344                return current;
345            }
346            current = next;
347        }
348        current
349    }
350}
351
352#[cfg(test)]
353mod tests {
354    use super::*;
355
356    /// A real three-level chain: two levels cannot catch ordering bugs.
357    const CHAIN: &str = "\
358SCHEMA DEMO;
359ENTITY Base
360 ABSTRACT SUPERTYPE OF (ONEOF(Middle));
361  Id : Identifier;
362  Owner : OPTIONAL Party;
363  Name : OPTIONAL Label;
364  Description : OPTIONAL Text;
365END_ENTITY;
366ENTITY Middle
367 ABSTRACT SUPERTYPE OF (ONEOF(Leaf))
368 SUBTYPE OF (Base);
369END_ENTITY;
370ENTITY Leaf
371 SUBTYPE OF (Middle);
372  Kind : OPTIONAL Label;
373END_ENTITY;
374TYPE Count = INTEGER; END_TYPE;
375TYPE PositiveCount = Count; END_TYPE;
376TYPE Colour = ENUMERATION OF (RED, GREEN, NOTDEFINED); END_TYPE;
377END_SCHEMA;";
378
379    fn graph() -> SchemaGraph {
380        SchemaGraph::from_express(CHAIN)
381    }
382
383    #[test]
384    fn inherited_attributes_come_first_in_positional_order() {
385        assert_eq!(
386            graph().attribute_names("LEAF"),
387            ["Id", "Owner", "Name", "Description", "Kind"],
388            "Base's slots must precede Leaf's own"
389        );
390    }
391
392    #[test]
393    fn subtype_tests_cross_intermediate_levels() {
394        let schema = graph();
395        assert!(schema.is_a("LEAF", "Base"), "grandparent");
396        assert!(schema.is_a("Leaf", "Middle"), "parent");
397        assert!(schema.is_a("Leaf", "Leaf"), "reflexive");
398        assert!(!schema.is_a("Base", "Leaf"), "not upward");
399    }
400
401    /// An entity the schema never declares is not a subtype even of itself.
402    #[test]
403    fn an_undeclared_entity_is_not_a_subtype_even_of_itself() {
404        assert!(!graph().is_a("NotAThing", "NotAThing"));
405    }
406
407    #[test]
408    fn defined_types_resolve_through_the_alias_chain() {
409        assert_eq!(graph().resolve_defined("PositiveCount"), "INTEGER");
410    }
411
412    /// A non-alias declaration resolves to itself.
413    #[test]
414    fn an_enumeration_resolves_to_its_own_name() {
415        assert_eq!(graph().resolve_defined("Colour"), "Colour");
416    }
417
418    /// A cyclic supertype must terminate rather than hang.
419    #[test]
420    fn a_cyclic_supertype_chain_terminates() {
421        let schema = SchemaGraph::from_express(
422            "SCHEMA S;\
423             ENTITY A SUBTYPE OF (B); END_ENTITY;\
424             ENTITY B SUBTYPE OF (A); END_ENTITY;\
425             END_SCHEMA;",
426        );
427        assert!(schema.supertypes("A").len() <= MAX_CHAIN_DEPTH);
428    }
429
430    /// A cyclic alias must terminate rather than hang.
431    #[test]
432    fn a_cyclic_alias_chain_terminates() {
433        let schema = SchemaGraph::from_express(
434            "SCHEMA S; TYPE A = B; END_TYPE; TYPE B = A; END_TYPE; END_SCHEMA;",
435        );
436        let resolved = schema.resolve_defined("A");
437        assert!(resolved == "A" || resolved == "B");
438    }
439
440    /// A supertype the schema never declares is still reported.
441    #[test]
442    fn an_undeclared_supertype_is_still_named() {
443        let schema = SchemaGraph::from_express(
444            "SCHEMA S; ENTITY A SUBTYPE OF (Missing); END_ENTITY; END_SCHEMA;",
445        );
446        assert_eq!(schema.supertypes("A"), ["Missing"]);
447        assert!(schema.is_a("A", "Missing"));
448        assert_eq!(
449            schema.subtypes("Missing"),
450            ["A"],
451            "the downward walk must agree with is_a on a partial schema"
452        );
453    }
454
455    /// Two branches under one root, declared out of alphabetical order, so
456    /// both sibling sorting and pre-order nesting are observable.
457    const TREE: &str = "\
458SCHEMA TREE;
459ENTITY Root; END_ENTITY;
460ENTITY Wall SUBTYPE OF (Root); END_ENTITY;
461ENTITY Door SUBTYPE OF (Root); END_ENTITY;
462ENTITY WallStandardCase SUBTYPE OF (Wall); END_ENTITY;
463ENTITY WallElementedCase SUBTYPE OF (Wall); END_ENTITY;
464ENTITY Unrelated; END_ENTITY;
465END_SCHEMA;";
466
467    #[test]
468    fn direct_subtypes_are_one_level_and_sorted() {
469        let schema = SchemaGraph::from_express(TREE);
470        assert_eq!(schema.direct_subtypes("root"), ["Door", "Wall"]);
471        assert!(schema.direct_subtypes("Door").is_empty(), "a leaf");
472        assert!(schema.direct_subtypes("NotAThing").is_empty());
473    }
474
475    #[test]
476    fn subtypes_walk_every_depth_in_sorted_pre_order() {
477        let schema = SchemaGraph::from_express(TREE);
478        assert_eq!(
479            schema.subtypes("ROOT"),
480            ["Door", "Wall", "WallElementedCase", "WallStandardCase"],
481            "a grandchild must follow its own parent, not trail the whole level"
482        );
483        assert!(!schema.subtypes("Root").contains(&"Root"), "not reflexive");
484        assert!(!schema.subtypes("Root").contains(&"Unrelated"));
485    }
486
487    /// `subtypes` is defined as the inverse of `is_a`; check that for every
488    /// ordered pair instead of trusting the two walks to agree by design.
489    #[test]
490    fn subtypes_is_exactly_the_inverse_of_is_a() {
491        for source in [CHAIN, TREE] {
492            let schema = SchemaGraph::from_express(source);
493            let names: Vec<&str> = schema.entity_names().collect();
494            for &ancestor in &names {
495                let down = schema.subtypes(ancestor);
496                for &candidate in &names {
497                    let expected = candidate != ancestor && schema.is_a(candidate, ancestor);
498                    assert_eq!(
499                        down.contains(&candidate),
500                        expected,
501                        "{candidate} vs {ancestor}"
502                    );
503                }
504            }
505        }
506    }
507
508    /// A cyclic supertype must not make the downward walk loop.
509    #[test]
510    fn a_cyclic_subtype_walk_terminates() {
511        let schema = SchemaGraph::from_express(
512            "SCHEMA S;\
513             ENTITY A SUBTYPE OF (B); END_ENTITY;\
514             ENTITY B SUBTYPE OF (A); END_ENTITY;\
515             END_SCHEMA;",
516        );
517        assert_eq!(schema.subtypes("A"), ["B"]);
518    }
519}