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