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}