openbim_step/schema/complex.rs
1//! Complex entity instances: the Part 21 external mapping.
2//!
3//! A complex instance (`#1=(A(..)B(..)C(..));`) combines several entity
4//! types that no single declared entity covers -- how AP203/214/242 write a
5//! rational B-spline curve, or a representation context with units and an
6//! uncertainty. ISO 10303-21:2016 §12.2.5.3 fixes its shape:
7//!
8//! - one partial record per entity type in the instance, **including every
9//! supertype**, even one with no attributes (NOTE 2);
10//! - partial records in ascending order of entity name, compared as
11//! upper-case octets (§5.2), so `_` (0x5F) sorts after every letter;
12//! - each partial record carries only the explicit attributes **its own**
13//! entity declares, in declaration order -- never the inherited ones.
14//!
15//! The last rule is why [`SchemaGraph::attributes`] is the wrong query here:
16//! it answers the internal mapping, where one record holds the whole
17//! inherited layout. [`SchemaGraph::resolve_complex`] answers this one.
18//!
19//! Problems are reported as [`ComplexIssue`]s rather than an error, and the
20//! parts that did resolve are still returned: a consumer reading a slightly
21//! non-conforming file can still interpret everything it recognises.
22
23use std::collections::{BTreeSet, HashSet};
24
25use super::SchemaGraph;
26use crate::express::{Attribute, EntityDef};
27
28/// A complex instance's partial records resolved against a schema.
29///
30/// Returned by [`SchemaGraph::resolve_complex`]. Parts are kept in the order
31/// they were given, one per input name, so `parts()[i]` always describes the
32/// `i`-th partial record of the instance.
33#[derive(Debug, Clone)]
34pub struct ComplexLayout<'s> {
35 parts: Vec<ComplexPart<'s>>,
36 types: Vec<&'s str>,
37 issues: Vec<ComplexIssue>,
38}
39
40impl<'s> ComplexLayout<'s> {
41 /// One entry per partial record, in input order.
42 #[must_use]
43 pub fn parts(&self) -> &[ComplexPart<'s>] {
44 &self.parts
45 }
46
47 /// Every entity type the instance is: each known part plus all of its
48 /// supertypes, once each, sorted by declared name.
49 ///
50 /// For a conforming instance this is exactly the set of parts. When a
51 /// supertype is missing it still appears here, so `types` always
52 /// describes the whole instance; the omission is reported as
53 /// [`ComplexIssue::MissingSupertype`].
54 #[must_use]
55 pub fn types(&self) -> &[&'s str] {
56 &self.types
57 }
58
59 /// Every way the instance departs from §12.2.5.3, in a stable order:
60 /// per-part issues by index, then missing supertypes by name.
61 #[must_use]
62 pub fn issues(&self) -> &[ComplexIssue] {
63 &self.issues
64 }
65
66 /// Whether the instance conforms: every part known, in order, no part
67 /// repeated, and every supertype present.
68 #[must_use]
69 pub fn is_valid(&self) -> bool {
70 self.issues.is_empty()
71 }
72}
73
74/// One partial record of a complex instance.
75#[derive(Debug, Clone)]
76pub struct ComplexPart<'s> {
77 name: String,
78 entity: Option<&'s EntityDef>,
79 slots: Vec<ComplexSlot<'s>>,
80}
81
82impl<'s> ComplexPart<'s> {
83 /// The partial record's name as given.
84 #[must_use]
85 pub fn name(&self) -> &str {
86 &self.name
87 }
88
89 /// The entity declaration it names, or `None` if the schema has none
90 /// (reported as [`ComplexIssue::UnknownType`]).
91 #[must_use]
92 pub fn entity(&self) -> Option<&'s EntityDef> {
93 self.entity
94 }
95
96 /// The partial record's positional slots: the explicit attributes the
97 /// entity itself declares, in declaration order. Empty for an unknown
98 /// part and for an entity that declares none.
99 #[must_use]
100 pub fn slots(&self) -> &[ComplexSlot<'s>] {
101 &self.slots
102 }
103}
104
105/// One positional slot of a partial record.
106#[derive(Debug, Clone, Copy)]
107pub struct ComplexSlot<'s> {
108 attribute: &'s Attribute,
109 derived: bool,
110}
111
112impl<'s> ComplexSlot<'s> {
113 /// The attribute declaration occupying this slot.
114 #[must_use]
115 pub fn attribute(&self) -> &'s Attribute {
116 self.attribute
117 }
118
119 /// Whether another type in the instance redeclares this attribute as
120 /// derived (`DERIVE SELF\X.a : T := ...;`).
121 ///
122 /// A conforming file writes such a slot as `*` (§12.2.6). This is a
123 /// property of the whole instance, not of the declaring entity alone:
124 /// `NAMED_UNIT(*)` in `(LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(..))` is `*`
125 /// because `si_unit` derives `named_unit.dimensions`.
126 #[must_use]
127 pub fn is_derived(&self) -> bool {
128 self.derived
129 }
130}
131
132/// A way a complex instance departs from ISO 10303-21:2016 §12.2.5.3.
133#[derive(Debug, Clone, PartialEq, Eq, Hash)]
134#[non_exhaustive]
135pub enum ComplexIssue {
136 /// Part `index` names no entity in the schema.
137 UnknownType {
138 /// Position of the part in the input.
139 index: usize,
140 },
141 /// Part `index` does not sort after the part before it. Names must be in
142 /// strictly ascending order of their upper-case octets.
143 OutOfOrder {
144 /// Position of the part in the input.
145 index: usize,
146 },
147 /// Part `index` repeats an earlier part (names compare case-insensitively).
148 Duplicate {
149 /// Position of the part in the input.
150 index: usize,
151 },
152 /// A supertype of some part has no partial record of its own. §12.2.5.3
153 /// NOTE 2 requires one even when it declares no attributes.
154 MissingSupertype {
155 /// The supertype's declared name.
156 name: String,
157 },
158}
159
160impl SchemaGraph {
161 /// Resolves a complex entity instance's partial records against this
162 /// schema (ISO 10303-21:2016 §12.2.5.3, the external mapping).
163 ///
164 /// `names` are the partial record names in the order the instance
165 /// writes them, e.g. `["BOUNDED_CURVE", "B_SPLINE_CURVE", ...]`. Matching
166 /// is case-insensitive. Each resolved part's slots are the explicit
167 /// attributes its entity *itself* declares -- compare them with the
168 /// partial record's parameters one to one.
169 ///
170 /// Never fails: unknown, repeated, misordered or missing types are
171 /// reported through [`ComplexLayout::issues`] and every part that could
172 /// be resolved still is.
173 #[must_use]
174 pub fn resolve_complex(&self, names: &[&str]) -> ComplexLayout<'_> {
175 let mut issues = Vec::new();
176 let mut seen: HashSet<String> = HashSet::new();
177 let mut previous: Option<String> = None;
178 let entities: Vec<Option<&EntityDef>> = names
179 .iter()
180 .enumerate()
181 .map(|(index, name)| {
182 let upper = name.to_ascii_uppercase();
183 let entity = self.entity(name);
184 if entity.is_none() {
185 issues.push(ComplexIssue::UnknownType { index });
186 }
187 if !seen.insert(upper.clone()) {
188 issues.push(ComplexIssue::Duplicate { index });
189 } else if previous.as_ref().is_some_and(|prev| upper <= *prev) {
190 issues.push(ComplexIssue::OutOfOrder { index });
191 }
192 previous = Some(upper);
193 entity
194 })
195 .collect();
196
197 // The supertype closure. Walking `supertypes` covers diamonds and
198 // multiple inheritance, and is already cycle-safe.
199 let mut types: BTreeSet<&str> = BTreeSet::new();
200 for entity in entities.iter().flatten() {
201 types.insert(entity.name.as_str());
202 types.extend(self.supertypes(&entity.name));
203 }
204 let mut missing: Vec<&str> = types
205 .iter()
206 .copied()
207 .filter(|name| !seen.contains(&name.to_ascii_uppercase()))
208 .collect();
209 missing.sort_unstable_by_key(|name| name.to_ascii_uppercase());
210 issues.extend(
211 missing
212 .into_iter()
213 .map(|name| ComplexIssue::MissingSupertype {
214 name: name.to_owned(),
215 }),
216 );
217
218 // An attribute is written `*` when any type in the instance derives
219 // it. Collected over the whole closure, not just the parts, so a
220 // derivation declared on a missing supertype still counts.
221 let derived: HashSet<(String, String)> = types
222 .iter()
223 .filter_map(|name| self.entity(name))
224 .flat_map(|entity| self.derivations(entity))
225 .collect();
226
227 let parts = names
228 .iter()
229 .zip(entities)
230 .map(|(name, entity)| ComplexPart {
231 name: (*name).to_owned(),
232 entity,
233 slots: entity.map_or_else(Vec::new, |entity| {
234 let owner = entity.name.to_ascii_uppercase();
235 entity
236 .attributes
237 .iter()
238 .map(|attribute| ComplexSlot {
239 attribute,
240 derived: derived
241 .contains(&(owner.clone(), attribute.name.to_ascii_uppercase())),
242 })
243 .collect()
244 }),
245 })
246 .collect();
247
248 ComplexLayout {
249 parts,
250 types: types.into_iter().collect(),
251 issues,
252 }
253 }
254
255 /// `(declaring entity, attribute)` pairs, upper-cased, that `entity`
256 /// redeclares as derived.
257 ///
258 /// `EntityDef::derived` stores names unqualified, so each is attributed
259 /// to the nearest supertype that declares an explicit attribute of that
260 /// name -- the one the `SELF\X.` prefix named. A derived name matching
261 /// no inherited attribute is a new derived attribute, not a
262 /// redeclaration, and occupies no slot.
263 fn derivations(&self, entity: &EntityDef) -> Vec<(String, String)> {
264 entity
265 .derived
266 .iter()
267 .filter_map(|name| {
268 self.supertypes(&entity.name)
269 .into_iter()
270 .filter_map(|ancestor| self.entity(ancestor))
271 .find(|ancestor| {
272 ancestor
273 .attributes
274 .iter()
275 .any(|attribute| attribute.name.eq_ignore_ascii_case(name))
276 })
277 .map(|ancestor| {
278 (
279 ancestor.name.to_ascii_uppercase(),
280 name.to_ascii_uppercase(),
281 )
282 })
283 })
284 .collect()
285 }
286}