openbim_ifcx/layers/mod.rs
1//! Layer stacks: loading a main layer and, recursively, its `imports`.
2//!
3//! A [`LayerStackBuilder`] asks a caller-supplied [`LayerResolver`] for each
4//! layer, loads every layer once, checks `integrity` where an import carries
5//! it, and returns the layers as a [`LayerStack`] in the order upstream's
6//! `IfcxLayerStackBuilder` (`buildingSMART/IFC5-development`,
7//! `src/ifcx-core/layers/layer-stack.ts`, commit `1a63082`) uses. The crate
8//! performs no network access. A filesystem resolver, [`FsResolver`], is
9//! available behind the `fs` feature, and [`MemoryResolver`] serves layers
10//! held in memory.
11//!
12//! # Layer order
13//!
14//! The main layer comes first. Then, for each layer, all of its imports that
15//! are not loaded yet are claimed in the order written, and each claimed
16//! import is followed by its own new imports before the next claimed one. For
17//! `main` importing `a` and `b`, and `a` importing `c`:
18//!
19//! ```text
20//! main, a, c, b
21//! ```
22//!
23//! An import of a layer that is already in the stack is not loaded again and
24//! does not move it.
25//!
26//! [`LayerStack::federate`] concatenates schemas and data in this order, as
27//! upstream's `Federate` does, and [`flatten`](crate::flatten) lets later
28//! opinions win. So, in
29//! upstream's effective behaviour, **an imported layer overrides the layer
30//! that imports it**, later imports override earlier ones, and nested imports
31//! override their importer too. The main layer has the lowest priority for
32//! every path and attribute an import also sets. Nothing in the `ifcx_alpha`
33//! draft text states a priority between a layer and its imports; this crate
34//! reproduces the reference implementation and records the finding in
35//! `docs/capabilities.md`.
36//!
37//! Upstream's builder appends a nested layer's subtree more than once (for
38//! `main → a → c` it yields `main, c, a, c`). Because later opinions win, the
39//! composed result is the same as for the list above, where each layer keeps
40//! the position of its last occurrence; only the first-seen position of a
41//! path or schema key can differ. This crate loads and lists each layer once.
42//!
43//! # Cycles
44//!
45//! Upstream silently skips an import of a layer that is already placed, so it
46//! accepts import cycles. This crate rejects them with
47//! [`LayerError::Cycle`] by default; [`LayerStackBuilder::allow_cycles`]
48//! restores upstream's behaviour.
49//!
50//! ```
51//! use openbim_ifcx::layers::{LayerStackBuilder, MemoryResolver};
52//!
53//! let layer = |id: &str, imports: &str, value: &str| format!(r#"{{
54//! "header": {{"id": "{id}", "ifcxVersion": "ifcx_alpha", "dataVersion": "1.0.0",
55//! "author": "someone", "timestamp": "2026-10-03"}},
56//! "imports": [{imports}],
57//! "schemas": {{}},
58//! "data": [{{"path": "p1", "attributes": {{"demo::value": "{value}"}}}}]
59//! }}"#);
60//!
61//! let mut resolver = MemoryResolver::new();
62//! resolver.insert("main", layer("main", r#"{"uri": "base"}"#, "main"));
63//! resolver.insert("base", layer("base", "", "base"));
64//!
65//! let stack = LayerStackBuilder::new(resolver).build("main")?;
66//! assert_eq!(stack.keys().collect::<Vec<_>>(), ["main", "base"]);
67//!
68//! // Data stays in stack order; the import's opinion comes last and wins.
69//! let federated = stack.federate();
70//! assert_eq!(federated.data.len(), 2);
71//! assert_eq!(federated.data[1].attributes.as_ref().unwrap()["demo::value"], "base");
72//! # Ok::<(), Box<dyn std::error::Error>>(())
73//! ```
74
75#[cfg(feature = "fs")]
76mod fs;
77mod integrity;
78mod memory;
79
80use std::collections::HashMap;
81use std::fmt;
82
83use indexmap::IndexMap;
84
85use crate::json::ReadError;
86use crate::model::{IfcxFile, ImportNode};
87use crate::validate::ValidationReport;
88
89#[cfg(feature = "fs")]
90pub use fs::{FsError, FsResolver};
91pub use integrity::{check_integrity, IntegrityError};
92pub use memory::MemoryResolver;
93
94/// Finds and loads layers for a [`LayerStackBuilder`].
95///
96/// Resolution happens in two steps so that a layer imported from several
97/// places loads once: [`key`](Self::key) turns an import `uri` into a key that
98/// identifies the layer, and [`load`](Self::load) returns the bytes for a key
99/// that is not loaded yet. Integrity is checked against exactly these bytes.
100pub trait LayerResolver {
101 /// Failure other than a missing layer, such as an I/O error.
102 type Error;
103
104 /// Identifies the layer that `uri` names. `importer` is the key of the
105 /// layer whose `imports` list `uri`, or `None` for the main layer, so
106 /// relative references can resolve against it.
107 ///
108 /// The default uses `uri` unchanged, as upstream's providers do.
109 fn key(&mut self, uri: &str, importer: Option<&str>) -> Result<String, Self::Error> {
110 let _ = importer;
111 Ok(uri.to_owned())
112 }
113
114 /// Returns the bytes of the layer with this key, or `Ok(None)` when there
115 /// is no such layer.
116 fn load(&mut self, key: &str) -> Result<Option<Vec<u8>>, Self::Error>;
117}
118
119impl<R: LayerResolver + ?Sized> LayerResolver for &mut R {
120 type Error = R::Error;
121
122 fn key(&mut self, uri: &str, importer: Option<&str>) -> Result<String, Self::Error> {
123 (**self).key(uri, importer)
124 }
125
126 fn load(&mut self, key: &str) -> Result<Option<Vec<u8>>, Self::Error> {
127 (**self).load(key)
128 }
129}
130
131/// Why a layer stack could not be built. `E` is the resolver's error type.
132#[derive(Debug)]
133pub enum LayerError<E> {
134 /// The resolver has no layer for this import.
135 Missing {
136 uri: String,
137 /// Key of the importing layer; `None` for the main layer.
138 importer: Option<String>,
139 },
140 /// The resolver failed.
141 Resolver {
142 uri: String,
143 importer: Option<String>,
144 source: E,
145 },
146 /// A loaded layer is not a valid IFCX file.
147 Read { key: String, source: ReadError },
148 /// The bytes of an imported layer do not satisfy the import's `integrity`,
149 /// or the value cannot be checked.
150 Integrity {
151 uri: String,
152 importer: String,
153 source: IntegrityError,
154 },
155 /// Layers import each other in a loop. `chain` lists the keys along the
156 /// loop and ends with the key it starts with.
157 Cycle { chain: Vec<String> },
158}
159
160impl<E: fmt::Display> fmt::Display for LayerError<E> {
161 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
162 let from = |importer: &Option<String>| match importer {
163 Some(key) => format!(" imported by {key:?}"),
164 None => String::new(),
165 };
166 match self {
167 Self::Missing { uri, importer } => {
168 write!(f, "layer {uri:?}{} not found", from(importer))
169 }
170 Self::Resolver {
171 uri,
172 importer,
173 source,
174 } => write!(
175 f,
176 "could not resolve layer {uri:?}{}: {source}",
177 from(importer)
178 ),
179 Self::Read { key, source } => write!(f, "layer {key:?}: {source}"),
180 Self::Integrity {
181 uri,
182 importer,
183 source,
184 } => write!(f, "layer {uri:?} imported by {importer:?}: {source}"),
185 Self::Cycle { chain } => write!(f, "import cycle: {}", chain.join(" -> ")),
186 }
187 }
188}
189
190impl<E> std::error::Error for LayerError<E>
191where
192 E: std::error::Error + 'static,
193{
194 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
195 match self {
196 Self::Resolver { source, .. } => Some(source),
197 Self::Read { source, .. } => Some(source),
198 Self::Integrity { source, .. } => Some(source),
199 Self::Missing { .. } | Self::Cycle { .. } => None,
200 }
201 }
202}
203
204/// One loaded layer of a [`LayerStack`].
205#[derive(Debug, Clone, PartialEq)]
206pub struct Layer {
207 key: String,
208 file: IfcxFile,
209}
210
211impl Layer {
212 /// The key the resolver gave this layer.
213 pub fn key(&self) -> &str {
214 &self.key
215 }
216
217 pub fn file(&self) -> &IfcxFile {
218 &self.file
219 }
220
221 pub fn into_file(self) -> IfcxFile {
222 self.file
223 }
224}
225
226/// A main layer and everything it imports, each once, in upstream order.
227#[derive(Debug, Clone, PartialEq)]
228pub struct LayerStack {
229 layers: Vec<Layer>,
230}
231
232impl LayerStack {
233 /// The layer the stack was built from.
234 pub fn main(&self) -> &Layer {
235 &self.layers[0]
236 }
237
238 /// All layers, main layer first. See the [module docs](self) for the order.
239 pub fn layers(&self) -> &[Layer] {
240 &self.layers
241 }
242
243 /// Layer keys in stack order.
244 pub fn keys(&self) -> impl Iterator<Item = &str> {
245 self.layers.iter().map(Layer::key)
246 }
247
248 pub fn into_layers(self) -> Vec<Layer> {
249 self.layers
250 }
251
252 /// Merges the stack into one file, as upstream's `Federate` does before
253 /// composing: the main layer's header, no imports, schemas from every
254 /// layer, and the data of every layer in stack order.
255 ///
256 /// A schema id defined by several layers keeps the position where it first
257 /// appears and takes the value of the last layer. Data nodes are not
258 /// merged; several nodes may share a path, and [`flatten`](crate::flatten)
259 /// applies them in order so that later opinions win.
260 pub fn federate(&self) -> IfcxFile {
261 federate(self.layers.iter().map(Layer::file)).expect("a layer stack is never empty")
262 }
263
264 /// [`federate`](Self::federate) for a stack that is not needed
265 /// afterwards: the layers' schemas and data move into the result instead
266 /// of being copied. Pass its `data` to [`flatten_owned`] to keep
267 /// composition free of copies too.
268 ///
269 /// [`flatten_owned`]: crate::flatten_owned
270 pub fn into_federated(self) -> IfcxFile {
271 federate_owned(self.layers.into_iter().map(Layer::into_file))
272 .expect("a layer stack is never empty")
273 }
274
275 /// Checks the attributes of the whole stack: the schemas of every layer,
276 /// merged as [`federate`](Self::federate) does, against the attributes of
277 /// every layer's data, merged per path as [`flatten`](crate::flatten)
278 /// does. This is [`IfcxFile::validate`] of the federated file, without
279 /// copying it.
280 ///
281 /// So an attribute whose schema lives only in an imported file validates,
282 /// and only the value that wins for a path is checked.
283 ///
284 /// ```
285 /// use openbim_ifcx::layers::{LayerStackBuilder, MemoryResolver};
286 ///
287 /// let header = r#""header": {"id": "x", "ifcxVersion": "ifcx_alpha", "dataVersion": "1.0.0",
288 /// "author": "someone", "timestamp": "2026-10-03"}"#;
289 /// let mut resolver = MemoryResolver::new();
290 /// resolver.insert("main", format!(r#"{{{header}, "imports": [{{"uri": "schemas"}}],
291 /// "schemas": {{}}, "data": [{{"path": "w", "attributes": {{"demo::height": 2.5}}}}]}}"#));
292 /// resolver.insert("schemas", format!(r#"{{{header}, "imports": [], "data": [],
293 /// "schemas": {{"demo::height": {{"value": {{"dataType": "Real"}}}}}}}}"#));
294 ///
295 /// let stack = LayerStackBuilder::new(resolver).build("main")?;
296 /// // The main layer alone does not know `demo::height`; its stack does.
297 /// assert!(stack.main().file().validate().is_err());
298 /// assert!(stack.validate().is_ok());
299 /// # Ok::<(), Box<dyn std::error::Error>>(())
300 /// ```
301 pub fn validate(&self) -> Result<(), ValidationReport> {
302 crate::validate::validate_files(self.layers.iter().map(Layer::file)).into_result()
303 }
304}
305
306/// Merges `files` in the order given, as upstream's `Federate` does: the first
307/// file's header, no imports, schemas from every file (a repeated id keeps its
308/// first position and takes the last value), and every data node in order.
309/// Top-level fields this crate does not model are not carried over.
310///
311/// Returns `None` for no files. [`federate_owned`] does the same for files
312/// that are not needed afterwards, without copying them.
313pub fn federate<'a>(files: impl IntoIterator<Item = &'a IfcxFile>) -> Option<IfcxFile> {
314 let mut files = files.into_iter().peekable();
315 let header = files.peek()?.header.clone();
316 let mut schemas = IndexMap::new();
317 let mut data = Vec::new();
318 for file in files {
319 for (id, schema) in &file.schemas {
320 schemas.insert(id.clone(), schema.clone());
321 }
322 data.extend(file.data.iter().cloned());
323 }
324 Some(IfcxFile {
325 header,
326 imports: Vec::new(),
327 schemas,
328 data,
329 extra: Default::default(),
330 })
331}
332
333/// [`federate`] for files that are not needed afterwards: their schemas and
334/// data move into the result instead of being copied. The result is the same.
335///
336/// Returns `None` for no files.
337pub fn federate_owned(files: impl IntoIterator<Item = IfcxFile>) -> Option<IfcxFile> {
338 let mut files = files.into_iter();
339 let first = files.next()?;
340 let header = first.header;
341 let mut schemas = first.schemas;
342 let mut data = first.data;
343 for file in files {
344 schemas.extend(file.schemas);
345 data.extend(file.data);
346 }
347 Some(IfcxFile {
348 header,
349 imports: Vec::new(),
350 schemas,
351 data,
352 extra: Default::default(),
353 })
354}
355
356/// Builds a [`LayerStack`] from a main layer through a [`LayerResolver`].
357#[derive(Debug)]
358pub struct LayerStackBuilder<R> {
359 resolver: R,
360 allow_cycles: bool,
361}
362
363impl<R: LayerResolver> LayerStackBuilder<R> {
364 pub fn new(resolver: R) -> Self {
365 Self {
366 resolver,
367 allow_cycles: false,
368 }
369 }
370
371 /// Accept import cycles and skip the import that closes one, as upstream
372 /// does. Off by default, so cycles are a [`LayerError::Cycle`].
373 pub fn allow_cycles(mut self, allow: bool) -> Self {
374 self.allow_cycles = allow;
375 self
376 }
377
378 pub fn resolver(&self) -> &R {
379 &self.resolver
380 }
381
382 pub fn resolver_mut(&mut self) -> &mut R {
383 &mut self.resolver
384 }
385
386 pub fn into_resolver(self) -> R {
387 self.resolver
388 }
389
390 /// Loads the layer `main` names and, recursively, its imports.
391 pub fn build(&mut self, main: &str) -> Result<LayerStack, LayerError<R::Error>> {
392 let key = self
393 .resolver
394 .key(main, None)
395 .map_err(|source| LayerError::Resolver {
396 uri: main.to_owned(),
397 importer: None,
398 source,
399 })?;
400 let mut build = Build::new(&mut self.resolver);
401 let root = build.load(main, None, key)?;
402 build.order.push(root);
403 build.satisfy(root)?;
404 Self::finish(self.allow_cycles, build)
405 }
406
407 /// Loads several layers given weakest first, and recursively their
408 /// imports, as if they were the `imports` of a main layer without data.
409 /// This is how upstream's `ifcx compose` and viewer stack the files a
410 /// user names, so the last one wins.
411 ///
412 /// The stack holds only the named layers and their imports, in the order
413 /// [`build`](Self::build) would give them below that main layer: each
414 /// named layer, followed by its own new imports, which override it. A
415 /// layer named twice, or also imported, loads once. `uris` resolve with
416 /// no importer, as `build`'s `main` does. At least one is needed; with
417 /// none, the result is [`LayerError::Missing`] for an empty URI.
418 ///
419 /// ```
420 /// use openbim_ifcx::layers::{LayerStackBuilder, MemoryResolver};
421 ///
422 /// let layer = |id: &str| format!(r#"{{
423 /// "header": {{"id": "{id}", "ifcxVersion": "ifcx_alpha", "dataVersion": "1.0.0",
424 /// "author": "someone", "timestamp": "2026-10-03"}},
425 /// "imports": [], "schemas": {{}},
426 /// "data": [{{"path": "p1", "attributes": {{"demo::value": "{id}"}}}}]
427 /// }}"#);
428 /// let mut resolver = MemoryResolver::new();
429 /// resolver.insert("base", layer("base"));
430 /// resolver.insert("overlay", layer("overlay"));
431 ///
432 /// let stack = LayerStackBuilder::new(resolver).build_all(["base", "overlay"])?;
433 /// assert_eq!(stack.keys().collect::<Vec<_>>(), ["base", "overlay"]);
434 /// assert_eq!(stack.main().key(), "base");
435 /// # Ok::<(), Box<dyn std::error::Error>>(())
436 /// ```
437 pub fn build_all<I>(&mut self, uris: I) -> Result<LayerStack, LayerError<R::Error>>
438 where
439 I: IntoIterator,
440 I::Item: AsRef<str>,
441 {
442 let mut build = Build::new(&mut self.resolver);
443 let mut pending = Vec::new();
444 for uri in uris {
445 let uri = uri.as_ref();
446 let key = build
447 .resolver
448 .key(uri, None)
449 .map_err(|source| LayerError::Resolver {
450 uri: uri.to_owned(),
451 importer: None,
452 source,
453 })?;
454 if !build.index.contains_key(&key) {
455 pending.push(build.load(uri, None, key)?);
456 }
457 }
458 if pending.is_empty() {
459 return Err(LayerError::Missing {
460 uri: String::new(),
461 importer: None,
462 });
463 }
464 for i in pending {
465 build.order.push(i);
466 build.satisfy(i)?;
467 }
468 Self::finish(self.allow_cycles, build)
469 }
470
471 fn finish(allow_cycles: bool, build: Build<'_, R>) -> Result<LayerStack, LayerError<R::Error>> {
472 if !allow_cycles {
473 if let Some(chain) = find_cycle(&build.edges) {
474 let chain = chain
475 .into_iter()
476 .map(|i| build.layers[i].key.clone())
477 .collect();
478 return Err(LayerError::Cycle { chain });
479 }
480 }
481 let mut layers: Vec<Option<Layer>> = build.layers.into_iter().map(Some).collect();
482 let layers = build
483 .order
484 .iter()
485 .map(|&i| layers[i].take().expect("each layer is placed once"))
486 .collect();
487 Ok(LayerStack { layers })
488 }
489}
490
491struct Build<'r, R> {
492 resolver: &'r mut R,
493 /// Layers in load order; indices below refer to this list.
494 layers: Vec<Layer>,
495 /// Layer indices in stack order.
496 order: Vec<usize>,
497 /// Raw bytes per layer index, kept to check integrity of later imports.
498 bytes: HashMap<usize, Vec<u8>>,
499 index: HashMap<String, usize>,
500 /// Import edges by layer index, for cycle detection.
501 edges: Vec<Vec<usize>>,
502}
503
504impl<'r, R: LayerResolver> Build<'r, R> {
505 fn new(resolver: &'r mut R) -> Self {
506 Self {
507 resolver,
508 layers: Vec::new(),
509 bytes: HashMap::new(),
510 index: HashMap::new(),
511 edges: Vec::new(),
512 order: Vec::new(),
513 }
514 }
515
516 fn load(
517 &mut self,
518 uri: &str,
519 importer: Option<&str>,
520 key: String,
521 ) -> Result<usize, LayerError<R::Error>> {
522 let bytes = match self.resolver.load(&key) {
523 Ok(Some(bytes)) => bytes,
524 Ok(None) => {
525 return Err(LayerError::Missing {
526 uri: uri.to_owned(),
527 importer: importer.map(str::to_owned),
528 })
529 }
530 Err(source) => {
531 return Err(LayerError::Resolver {
532 uri: uri.to_owned(),
533 importer: importer.map(str::to_owned),
534 source,
535 })
536 }
537 };
538 let file = IfcxFile::from_json_slice(&bytes).map_err(|source| LayerError::Read {
539 key: key.clone(),
540 source,
541 })?;
542 let i = self.layers.len();
543 self.index.insert(key.clone(), i);
544 self.bytes.insert(i, bytes);
545 self.layers.push(Layer { key, file });
546 self.edges.push(Vec::new());
547 Ok(i)
548 }
549
550 /// Upstream's `SatisfyDependencies`: claim every new import of `layer` in
551 /// order, then descend into each claimed import in turn.
552 fn satisfy(&mut self, layer: usize) -> Result<(), LayerError<R::Error>> {
553 let imports: Vec<ImportNode> = self.layers[layer].file.imports.clone();
554 let importer = self.layers[layer].key.clone();
555 let mut pending = Vec::new();
556 for import in &imports {
557 let key = self
558 .resolver
559 .key(&import.uri, Some(&importer))
560 .map_err(|source| LayerError::Resolver {
561 uri: import.uri.clone(),
562 importer: Some(importer.clone()),
563 source,
564 })?;
565 let target = match self.index.get(&key) {
566 Some(&i) => i,
567 None => {
568 let i = self.load(&import.uri, Some(&importer), key)?;
569 pending.push(i);
570 i
571 }
572 };
573 self.edges[layer].push(target);
574 if let Some(expected) = &import.integrity {
575 check_integrity(expected, &self.bytes[&target]).map_err(|source| {
576 LayerError::Integrity {
577 uri: import.uri.clone(),
578 importer: importer.clone(),
579 source,
580 }
581 })?;
582 }
583 }
584 for i in pending {
585 self.order.push(i);
586 self.satisfy(i)?;
587 }
588 Ok(())
589 }
590}
591
592/// Returns a loop in the import graph as layer indices, first index repeated
593/// at the end, or `None`.
594fn find_cycle(edges: &[Vec<usize>]) -> Option<Vec<usize>> {
595 #[derive(Clone, Copy, PartialEq)]
596 enum State {
597 New,
598 Open,
599 Done,
600 }
601 let mut state = vec![State::New; edges.len()];
602 for start in 0..edges.len() {
603 if state[start] != State::New {
604 continue;
605 }
606 // Iterative depth-first search; `path` holds open nodes and the next
607 // edge to follow from each.
608 let mut path: Vec<(usize, usize)> = vec![(start, 0)];
609 state[start] = State::Open;
610 while let Some(&mut (node, ref mut next)) = path.last_mut() {
611 if let Some(&target) = edges[node].get(*next) {
612 *next += 1;
613 match state[target] {
614 State::Open => {
615 let from = path.iter().position(|&(n, _)| n == target).unwrap();
616 let mut chain: Vec<usize> = path[from..].iter().map(|&(n, _)| n).collect();
617 chain.push(target);
618 return Some(chain);
619 }
620 State::New => {
621 state[target] = State::Open;
622 path.push((target, 0));
623 }
624 State::Done => {}
625 }
626 } else {
627 state[node] = State::Done;
628 path.pop();
629 }
630 }
631 }
632 None
633}
634
635#[cfg(test)]
636mod tests {
637 use super::find_cycle;
638
639 #[test]
640 fn finds_cycles() {
641 assert_eq!(find_cycle(&[vec![1], vec![2], vec![]]), None);
642 assert_eq!(find_cycle(&[vec![1, 2], vec![2], vec![]]), None);
643 assert_eq!(find_cycle(&[vec![0]]), Some(vec![0, 0]));
644 assert_eq!(
645 find_cycle(&[vec![1], vec![2], vec![1]]),
646 Some(vec![1, 2, 1])
647 );
648 }
649}