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(¤t) 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(¤t) {
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}