Skip to main content

openbim_step/
schema.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
41use crate::express::{Attribute, EntityDef, ParsedSchema, TypeDef, TypeKind};
42
43/// Longest supertype or alias chain this will walk before giving up.
44///
45/// A cyclic `SUBTYPE OF` is not legal EXPRESS, but this parser is deliberately
46/// tolerant and a malformed source must not hang a consumer. Real schemas nest
47/// around a dozen levels; 64 leaves generous room while still terminating.
48const MAX_CHAIN_DEPTH: usize = 64;
49
50/// Case-insensitive lookup over a parsed schema's declarations.
51///
52/// EXPRESS identifiers are case-insensitive, and Part 21 records conventionally
53/// spell entity names in upper case (`MYENTITY`) while schema sources spell
54/// them in mixed case (`MyEntity`). Every lookup here folds case so callers can
55/// pass whichever spelling they hold.
56#[derive(Debug, Clone)]
57pub struct SchemaGraph {
58    name: String,
59    entities: HashMap<String, EntityDef>,
60    types: HashMap<String, TypeDef>,
61}
62
63impl SchemaGraph {
64    /// Indexes a parsed schema for querying.
65    #[must_use]
66    pub fn new(parsed: ParsedSchema) -> Self {
67        let entities = parsed
68            .entities
69            .into_iter()
70            .map(|entity| (entity.name.to_ascii_uppercase(), entity))
71            .collect();
72        let types = parsed
73            .types
74            .into_iter()
75            .map(|type_def| (type_def.name.to_ascii_uppercase(), type_def))
76            .collect();
77        Self {
78            name: parsed.name,
79            entities,
80            types,
81        }
82    }
83
84    /// Parses EXPRESS source and indexes it in one step.
85    #[must_use]
86    pub fn from_express(source: &str) -> Self {
87        Self::new(crate::express::parse(source))
88    }
89
90    /// The declared schema name.
91    #[must_use]
92    pub fn name(&self) -> &str {
93        &self.name
94    }
95
96    /// How many entity declarations the schema holds.
97    #[must_use]
98    pub fn entity_count(&self) -> usize {
99        self.entities.len()
100    }
101
102    /// How many type declarations the schema holds.
103    #[must_use]
104    pub fn type_count(&self) -> usize {
105        self.types.len()
106    }
107
108    /// The entity declaration for `name`, if the schema declares one.
109    #[must_use]
110    pub fn entity(&self, name: &str) -> Option<&EntityDef> {
111        self.entities.get(&name.to_ascii_uppercase())
112    }
113
114    /// The type declaration for `name`, if the schema declares one.
115    #[must_use]
116    pub fn type_def(&self, name: &str) -> Option<&TypeDef> {
117        self.types.get(&name.to_ascii_uppercase())
118    }
119
120    /// Every entity name the schema declares, in unspecified order.
121    ///
122    /// Callers needing determinism must sort: the underlying map order is
123    /// deliberately not promised.
124    pub fn entity_names(&self) -> impl Iterator<Item = &str> {
125        self.entities.values().map(|entity| entity.name.as_str())
126    }
127
128    /// Whether `name` is `ancestor`, or inherits from it.
129    ///
130    /// Reflexive for declared entities, matching EXPRESS subtype semantics
131    /// where a type belongs to its own subtype set. An entity the schema never
132    /// declares is not a subtype of anything, including itself -- otherwise a
133    /// typo would silently satisfy every check made against it.
134    #[must_use]
135    pub fn is_a(&self, name: &str, ancestor: &str) -> bool {
136        if name.eq_ignore_ascii_case(ancestor) {
137            return self.entities.contains_key(&name.to_ascii_uppercase());
138        }
139        self.supertypes(name)
140            .iter()
141            .any(|super_name| super_name.eq_ignore_ascii_case(ancestor))
142    }
143
144    /// The supertype chain above `name`, nearest parent first.
145    ///
146    /// Excludes `name` itself. Bounded by a fixed depth limit so a malformed
147    /// cyclic schema terminates instead of hanging.
148    #[must_use]
149    pub fn supertypes(&self, name: &str) -> Vec<&str> {
150        let mut chain = Vec::new();
151        let mut current = self.entities.get(&name.to_ascii_uppercase());
152        for _ in 0..MAX_CHAIN_DEPTH {
153            let Some(def) = current else { break };
154            let Some(supertype) = def.supertype.as_ref() else {
155                break;
156            };
157            let Some(parent) = self.entities.get(&supertype.to_ascii_uppercase()) else {
158                // The source names a supertype it never declares. Report the
159                // name anyway: a consumer checking `is_a` against a partial
160                // schema should still see the declared relationship.
161                chain.push(supertype.as_str());
162                break;
163            };
164            chain.push(parent.name.as_str());
165            current = Some(parent);
166        }
167        chain
168    }
169
170    /// Every attribute slot in **Part 21 positional order**, inherited first.
171    ///
172    /// See the module documentation for why this ordering is load-bearing.
173    ///
174    /// Derived redeclarations are *included*: they keep their inherited
175    /// position and are written `*` in a Part 21 record. Use
176    /// [`EntityDef::is_derived`] on the owning entity to tell them apart.
177    #[must_use]
178    pub fn attributes(&self, name: &str) -> Vec<&Attribute> {
179        let mut chain: Vec<&EntityDef> = Vec::new();
180        let mut current = self.entities.get(&name.to_ascii_uppercase());
181        for _ in 0..MAX_CHAIN_DEPTH {
182            let Some(def) = current else { break };
183            chain.push(def);
184            let Some(supertype) = def.supertype.as_ref() else {
185                break;
186            };
187            current = self.entities.get(&supertype.to_ascii_uppercase());
188        }
189        chain.reverse();
190        chain.iter().flat_map(|def| def.attributes.iter()).collect()
191    }
192
193    /// Attribute names in positional order.
194    ///
195    /// The bridge a positional-to-named mapping needs: slot `i` is called
196    /// `names[i]`.
197    #[must_use]
198    pub fn attribute_names(&self, name: &str) -> Vec<&str> {
199        self.attributes(name)
200            .into_iter()
201            .map(|attribute| attribute.name.as_str())
202            .collect()
203    }
204
205    /// Resolves a defined type to the base it ultimately aliases.
206    ///
207    /// A chain such as `PositiveCount -> Count -> INTEGER` resolves to
208    /// `INTEGER`. Returns the final right-hand side; for a declaration that is
209    /// not an alias, that is the type's own name. Bounded like the supertype
210    /// walk so a cyclic alias cannot hang.
211    ///
212    /// The right-hand side is returned verbatim, including any aggregate
213    /// syntax (`LIST [1:?] OF X`): discarding it here would throw away the
214    /// aggregate fact entirely, and callers that want the base scalar can
215    /// match on the text they receive.
216    #[must_use]
217    pub fn resolve_defined(&self, name: &str) -> String {
218        let mut current = name.to_string();
219        for _ in 0..MAX_CHAIN_DEPTH {
220            let Some(def) = self.type_def(&current) else {
221                return current;
222            };
223            let TypeKind::Defined(target) = &def.kind else {
224                return current;
225            };
226            let next = target.trim().to_string();
227            if next.eq_ignore_ascii_case(&current) {
228                return current;
229            }
230            current = next;
231        }
232        current
233    }
234}
235
236#[cfg(test)]
237mod tests {
238    use super::*;
239
240    /// A real three-level chain: two levels cannot catch ordering bugs.
241    const CHAIN: &str = "\
242SCHEMA DEMO;
243ENTITY Base
244 ABSTRACT SUPERTYPE OF (ONEOF(Middle));
245  Id : Identifier;
246  Owner : OPTIONAL Party;
247  Name : OPTIONAL Label;
248  Description : OPTIONAL Text;
249END_ENTITY;
250ENTITY Middle
251 ABSTRACT SUPERTYPE OF (ONEOF(Leaf))
252 SUBTYPE OF (Base);
253END_ENTITY;
254ENTITY Leaf
255 SUBTYPE OF (Middle);
256  Kind : OPTIONAL Label;
257END_ENTITY;
258TYPE Count = INTEGER; END_TYPE;
259TYPE PositiveCount = Count; END_TYPE;
260TYPE Colour = ENUMERATION OF (RED, GREEN, NOTDEFINED); END_TYPE;
261END_SCHEMA;";
262
263    fn graph() -> SchemaGraph {
264        SchemaGraph::from_express(CHAIN)
265    }
266
267    #[test]
268    fn inherited_attributes_come_first_in_positional_order() {
269        assert_eq!(
270            graph().attribute_names("LEAF"),
271            ["Id", "Owner", "Name", "Description", "Kind"],
272            "Base's slots must precede Leaf's own"
273        );
274    }
275
276    #[test]
277    fn subtype_tests_cross_intermediate_levels() {
278        let schema = graph();
279        assert!(schema.is_a("LEAF", "Base"), "grandparent");
280        assert!(schema.is_a("Leaf", "Middle"), "parent");
281        assert!(schema.is_a("Leaf", "Leaf"), "reflexive");
282        assert!(!schema.is_a("Base", "Leaf"), "not upward");
283    }
284
285    /// An entity the schema never declares is not a subtype even of itself.
286    #[test]
287    fn an_undeclared_entity_is_not_a_subtype_even_of_itself() {
288        assert!(!graph().is_a("NotAThing", "NotAThing"));
289    }
290
291    #[test]
292    fn defined_types_resolve_through_the_alias_chain() {
293        assert_eq!(graph().resolve_defined("PositiveCount"), "INTEGER");
294    }
295
296    /// A non-alias declaration resolves to itself.
297    #[test]
298    fn an_enumeration_resolves_to_its_own_name() {
299        assert_eq!(graph().resolve_defined("Colour"), "Colour");
300    }
301
302    /// A cyclic supertype must terminate rather than hang.
303    #[test]
304    fn a_cyclic_supertype_chain_terminates() {
305        let schema = SchemaGraph::from_express(
306            "SCHEMA S;\
307             ENTITY A SUBTYPE OF (B); END_ENTITY;\
308             ENTITY B SUBTYPE OF (A); END_ENTITY;\
309             END_SCHEMA;",
310        );
311        assert!(schema.supertypes("A").len() <= MAX_CHAIN_DEPTH);
312    }
313
314    /// A cyclic alias must terminate rather than hang.
315    #[test]
316    fn a_cyclic_alias_chain_terminates() {
317        let schema = SchemaGraph::from_express(
318            "SCHEMA S; TYPE A = B; END_TYPE; TYPE B = A; END_TYPE; END_SCHEMA;",
319        );
320        let resolved = schema.resolve_defined("A");
321        assert!(resolved == "A" || resolved == "B");
322    }
323
324    /// A supertype the schema never declares is still reported.
325    #[test]
326    fn an_undeclared_supertype_is_still_named() {
327        let schema = SchemaGraph::from_express(
328            "SCHEMA S; ENTITY A SUBTYPE OF (Missing); END_ENTITY; END_SCHEMA;",
329        );
330        assert_eq!(schema.supertypes("A"), ["Missing"]);
331        assert!(schema.is_a("A", "Missing"));
332    }
333}