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