Skip to main content

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}