ipfrs_core/dag.rs
1//! DAG (Directed Acyclic Graph) traversal and analysis utilities.
2//!
3//! This module provides utilities for working with IPLD Merkle DAGs:
4//! - Extracting CID links from IPLD data
5//! - Calculating DAG statistics
6//! - Validating DAG structures
7//! - Collecting all CIDs in a DAG
8//!
9//! # Examples
10//!
11//! ```rust
12//! use ipfrs_core::{Ipld, Cid, CidBuilder};
13//! use ipfrs_core::dag::{extract_links, DagStats};
14//! use std::collections::BTreeMap;
15//!
16//! // Create IPLD with links
17//! let cid1 = CidBuilder::new().build(b"data1").unwrap();
18//! let cid2 = CidBuilder::new().build(b"data2").unwrap();
19//!
20//! let mut map = BTreeMap::new();
21//! map.insert("link1".to_string(), Ipld::link(cid1));
22//! map.insert("link2".to_string(), Ipld::link(cid2));
23//! let ipld = Ipld::Map(map);
24//!
25//! // Extract all CID links
26//! let links = extract_links(&ipld);
27//! assert_eq!(links.len(), 2);
28//! ```
29
30use crate::cid::{Cid, SerializableCid};
31use crate::ipld::Ipld;
32use std::collections::{HashMap, HashSet, VecDeque};
33
34/// Extract all CID links from an IPLD structure (non-recursive).
35///
36/// This function finds all direct `Ipld::Link` values in the given IPLD data,
37/// but does not recursively traverse nested links. Use `collect_all_links` for
38/// recursive traversal.
39///
40/// # Arguments
41///
42/// * `ipld` - The IPLD data to extract links from
43///
44/// # Returns
45///
46/// A vector of all CID links found at the top level
47///
48/// # Examples
49///
50/// ```rust
51/// use ipfrs_core::{Ipld, CidBuilder};
52/// use ipfrs_core::dag::extract_links;
53/// use std::collections::BTreeMap;
54///
55/// let cid = CidBuilder::new().build(b"test").unwrap();
56/// let mut map = BTreeMap::new();
57/// map.insert("file".to_string(), Ipld::link(cid.clone()));
58/// let ipld = Ipld::Map(map);
59///
60/// let links = extract_links(&ipld);
61/// assert_eq!(links.len(), 1);
62/// assert_eq!(links[0], cid);
63/// ```
64pub fn extract_links(ipld: &Ipld) -> Vec<Cid> {
65 let mut links = Vec::new();
66 extract_links_recursive(ipld, &mut links, false);
67 links
68}
69
70/// Extract all CID links from an IPLD structure recursively.
71///
72/// This function traverses the entire IPLD tree and collects all `Ipld::Link`
73/// values found at any depth.
74///
75/// # Arguments
76///
77/// * `ipld` - The IPLD data to extract links from
78///
79/// # Returns
80///
81/// A vector of all CID links found (may contain duplicates)
82///
83/// # Examples
84///
85/// ```rust
86/// use ipfrs_core::{Ipld, CidBuilder};
87/// use ipfrs_core::dag::collect_all_links;
88/// use std::collections::BTreeMap;
89///
90/// let cid1 = CidBuilder::new().build(b"test1").unwrap();
91/// let cid2 = CidBuilder::new().build(b"test2").unwrap();
92///
93/// // Nested structure
94/// let mut inner = BTreeMap::new();
95/// inner.insert("link".to_string(), Ipld::link(cid2.clone()));
96///
97/// let mut outer = BTreeMap::new();
98/// outer.insert("file".to_string(), Ipld::link(cid1.clone()));
99/// outer.insert("nested".to_string(), Ipld::Map(inner));
100///
101/// let ipld = Ipld::Map(outer);
102/// let links = collect_all_links(&ipld);
103/// assert_eq!(links.len(), 2);
104/// ```
105pub fn collect_all_links(ipld: &Ipld) -> Vec<Cid> {
106 let mut links = Vec::new();
107 extract_links_recursive(ipld, &mut links, true);
108 links
109}
110
111/// Extract unique CID links from an IPLD structure (no duplicates).
112///
113/// This function is similar to `collect_all_links` but returns a set of unique
114/// CIDs, removing any duplicates.
115///
116/// # Arguments
117///
118/// * `ipld` - The IPLD data to extract links from
119///
120/// # Returns
121///
122/// A set of unique CID links
123pub fn collect_unique_links(ipld: &Ipld) -> HashSet<Cid> {
124 collect_all_links(ipld).into_iter().collect()
125}
126
127/// Internal recursive link extraction
128fn extract_links_recursive(ipld: &Ipld, links: &mut Vec<Cid>, recursive: bool) {
129 match ipld {
130 Ipld::Link(SerializableCid(cid)) => {
131 links.push(*cid);
132 }
133 Ipld::List(items) => {
134 if recursive {
135 for item in items {
136 extract_links_recursive(item, links, true);
137 }
138 } else {
139 for item in items {
140 if let Ipld::Link(SerializableCid(cid)) = item {
141 links.push(*cid);
142 }
143 }
144 }
145 }
146 Ipld::Map(map) => {
147 if recursive {
148 for value in map.values() {
149 extract_links_recursive(value, links, true);
150 }
151 } else {
152 for value in map.values() {
153 if let Ipld::Link(SerializableCid(cid)) = value {
154 links.push(*cid);
155 }
156 }
157 }
158 }
159 _ => {}
160 }
161}
162
163/// Statistics about a DAG structure
164#[derive(Debug, Clone, Default, PartialEq, Eq)]
165pub struct DagStats {
166 /// Total number of unique CIDs in the DAG
167 pub unique_cids: usize,
168 /// Total number of links (including duplicates)
169 pub total_links: usize,
170 /// Maximum depth of the DAG
171 pub max_depth: usize,
172 /// Number of leaf nodes (nodes with no outgoing links)
173 pub leaf_count: usize,
174 /// Number of intermediate nodes (nodes with outgoing links)
175 pub intermediate_count: usize,
176}
177
178impl DagStats {
179 /// Create empty DAG statistics
180 pub fn new() -> Self {
181 Self::default()
182 }
183
184 /// Calculate statistics for an IPLD structure
185 ///
186 /// Note: This only analyzes the structure of the IPLD data provided,
187 /// it does not follow CID links to fetch additional blocks.
188 ///
189 /// # Arguments
190 ///
191 /// * `ipld` - The IPLD data to analyze
192 ///
193 /// # Returns
194 ///
195 /// Statistics about the DAG structure
196 ///
197 /// # Examples
198 ///
199 /// ```rust
200 /// use ipfrs_core::{Ipld, CidBuilder};
201 /// use ipfrs_core::dag::DagStats;
202 /// use std::collections::BTreeMap;
203 ///
204 /// let cid = CidBuilder::new().build(b"data").unwrap();
205 /// let mut map = BTreeMap::new();
206 /// map.insert("link".to_string(), Ipld::link(cid));
207 /// let ipld = Ipld::Map(map);
208 ///
209 /// let stats = DagStats::from_ipld(&ipld);
210 /// assert_eq!(stats.total_links, 1);
211 /// assert_eq!(stats.unique_cids, 1);
212 /// ```
213 pub fn from_ipld(ipld: &Ipld) -> Self {
214 let all_links = collect_all_links(ipld);
215 let unique_links: HashSet<_> = all_links.iter().collect();
216
217 let depth = calculate_depth(ipld, 0);
218 let (leaves, intermediates) = count_node_types(ipld);
219
220 Self {
221 unique_cids: unique_links.len(),
222 total_links: all_links.len(),
223 max_depth: depth,
224 leaf_count: leaves,
225 intermediate_count: intermediates,
226 }
227 }
228
229 /// Calculate the deduplication ratio
230 ///
231 /// Returns the ratio of duplicate links to total links.
232 /// A value of 0.0 means no duplication, 1.0 means all links are duplicates.
233 pub fn deduplication_ratio(&self) -> f64 {
234 if self.total_links == 0 {
235 return 0.0;
236 }
237 let duplicates = self.total_links.saturating_sub(self.unique_cids);
238 duplicates as f64 / self.total_links as f64
239 }
240}
241
242/// Calculate the maximum depth of an IPLD tree
243fn calculate_depth(ipld: &Ipld, current_depth: usize) -> usize {
244 match ipld {
245 Ipld::List(items) => {
246 let max_child_depth = items
247 .iter()
248 .map(|item| calculate_depth(item, current_depth + 1))
249 .max()
250 .unwrap_or(current_depth);
251 max_child_depth
252 }
253 Ipld::Map(map) => {
254 let max_child_depth = map
255 .values()
256 .map(|value| calculate_depth(value, current_depth + 1))
257 .max()
258 .unwrap_or(current_depth);
259 max_child_depth
260 }
261 _ => current_depth,
262 }
263}
264
265/// Count leaf and intermediate nodes
266fn count_node_types(ipld: &Ipld) -> (usize, usize) {
267 match ipld {
268 Ipld::List(items) => {
269 if items.is_empty() {
270 return (1, 0); // Empty list is a leaf
271 }
272 let mut leaves = 0;
273 let mut intermediates = 1; // This node is intermediate
274 for item in items {
275 let (l, i) = count_node_types(item);
276 leaves += l;
277 intermediates += i;
278 }
279 (leaves, intermediates)
280 }
281 Ipld::Map(map) => {
282 if map.is_empty() {
283 return (1, 0); // Empty map is a leaf
284 }
285 let mut leaves = 0;
286 let mut intermediates = 1; // This node is intermediate
287 for value in map.values() {
288 let (l, i) = count_node_types(value);
289 leaves += l;
290 intermediates += i;
291 }
292 (leaves, intermediates)
293 }
294 _ => (1, 0), // Scalar values are leaves
295 }
296}
297
298/// Validate that an IPLD structure forms a proper DAG (no cycles).
299///
300/// This function checks that there are no circular references in the CID links.
301/// Note: This only validates the structure of the provided IPLD data, it does
302/// not fetch and validate linked blocks.
303///
304/// # Arguments
305///
306/// * `ipld` - The IPLD data to validate
307///
308/// # Returns
309///
310/// `true` if the structure is acyclic, `false` if cycles are detected
311///
312/// # Examples
313///
314/// ```rust
315/// use ipfrs_core::{Ipld, CidBuilder};
316/// use ipfrs_core::dag::is_dag;
317/// use std::collections::BTreeMap;
318///
319/// let cid = CidBuilder::new().build(b"test").unwrap();
320/// let mut map = BTreeMap::new();
321/// map.insert("link".to_string(), Ipld::link(cid));
322/// let ipld = Ipld::Map(map);
323///
324/// assert!(is_dag(&ipld));
325/// ```
326pub fn is_dag(ipld: &Ipld) -> bool {
327 let mut visited = HashSet::new();
328 let mut stack = HashSet::new();
329 has_cycle_dfs(ipld, &mut visited, &mut stack)
330}
331
332/// DFS cycle detection
333fn has_cycle_dfs(ipld: &Ipld, visited: &mut HashSet<String>, stack: &mut HashSet<String>) -> bool {
334 match ipld {
335 Ipld::Link(SerializableCid(cid)) => {
336 let cid_str = cid.to_string();
337 if stack.contains(&cid_str) {
338 return false; // Cycle detected
339 }
340 if visited.contains(&cid_str) {
341 return true; // Already validated this path
342 }
343 visited.insert(cid_str.clone());
344 stack.insert(cid_str.clone());
345 // Note: We can't follow the link without a BlockFetcher
346 // So we just mark it as visited
347 stack.remove(&cid_str);
348 true
349 }
350 Ipld::List(items) => {
351 for item in items {
352 if !has_cycle_dfs(item, visited, stack) {
353 return false;
354 }
355 }
356 true
357 }
358 Ipld::Map(map) => {
359 for value in map.values() {
360 if !has_cycle_dfs(value, visited, stack) {
361 return false;
362 }
363 }
364 true
365 }
366 _ => true,
367 }
368}
369
370/// Find all paths from root to a specific CID in an IPLD structure.
371///
372/// Returns a list of paths (as lists of keys) that lead to the target CID.
373///
374/// # Arguments
375///
376/// * `ipld` - The IPLD data to search
377/// * `target_cid` - The CID to find paths to
378///
379/// # Returns
380///
381/// A vector of paths, where each path is a vector of keys leading to the CID
382pub fn find_paths_to_cid(ipld: &Ipld, target_cid: &Cid) -> Vec<Vec<String>> {
383 let mut paths = Vec::new();
384 let mut current_path = Vec::new();
385 find_paths_recursive(ipld, target_cid, &mut current_path, &mut paths);
386 paths
387}
388
389/// Recursive path finding helper
390fn find_paths_recursive(
391 ipld: &Ipld,
392 target_cid: &Cid,
393 current_path: &mut Vec<String>,
394 paths: &mut Vec<Vec<String>>,
395) {
396 match ipld {
397 Ipld::Link(SerializableCid(cid)) if cid == target_cid => {
398 paths.push(current_path.clone());
399 }
400 Ipld::List(items) => {
401 for (i, item) in items.iter().enumerate() {
402 current_path.push(format!("[{}]", i));
403 find_paths_recursive(item, target_cid, current_path, paths);
404 current_path.pop();
405 }
406 }
407 Ipld::Map(map) => {
408 for (key, value) in map {
409 current_path.push(key.clone());
410 find_paths_recursive(value, target_cid, current_path, paths);
411 current_path.pop();
412 }
413 }
414 _ => {}
415 }
416}
417
418/// Traverse a DAG in breadth-first order, collecting all IPLD nodes.
419///
420/// Note: This only traverses the structure of the provided IPLD data,
421/// it does not fetch linked blocks.
422///
423/// # Arguments
424///
425/// * `root` - The root IPLD node to start traversal from
426///
427/// # Returns
428///
429/// A vector of all IPLD nodes in breadth-first order
430pub fn traverse_bfs(root: &Ipld) -> Vec<Ipld> {
431 let mut result = Vec::new();
432 let mut queue = VecDeque::new();
433 queue.push_back(root.clone());
434
435 while let Some(node) = queue.pop_front() {
436 result.push(node.clone());
437
438 match &node {
439 Ipld::List(items) => {
440 for item in items {
441 queue.push_back(item.clone());
442 }
443 }
444 Ipld::Map(map) => {
445 for value in map.values() {
446 queue.push_back(value.clone());
447 }
448 }
449 _ => {}
450 }
451 }
452
453 result
454}
455
456/// Traverse a DAG in depth-first order, collecting all IPLD nodes.
457///
458/// # Arguments
459///
460/// * `root` - The root IPLD node to start traversal from
461///
462/// # Returns
463///
464/// A vector of all IPLD nodes in depth-first order
465pub fn traverse_dfs(root: &Ipld) -> Vec<Ipld> {
466 let mut result = Vec::new();
467 traverse_dfs_recursive(root, &mut result);
468 result
469}
470
471/// Recursive DFS helper
472fn traverse_dfs_recursive(node: &Ipld, result: &mut Vec<Ipld>) {
473 result.push(node.clone());
474
475 match node {
476 Ipld::List(items) => {
477 for item in items {
478 traverse_dfs_recursive(item, result);
479 }
480 }
481 Ipld::Map(map) => {
482 for value in map.values() {
483 traverse_dfs_recursive(value, result);
484 }
485 }
486 _ => {}
487 }
488}
489
490/// Additional DAG metrics beyond basic statistics.
491///
492/// Provides advanced graph-theoretic metrics for analyzing DAG structure.
493#[derive(Debug, Clone, Default, PartialEq)]
494pub struct DagMetrics {
495 /// Average branching factor (average number of children per non-leaf node)
496 pub avg_branching_factor: f64,
497 /// Maximum branching factor (maximum number of children for any node)
498 pub max_branching_factor: usize,
499 /// Width of the DAG (maximum number of nodes at any level)
500 pub width: usize,
501 /// Total number of nodes in the DAG
502 pub total_nodes: usize,
503 /// Average depth of leaf nodes
504 pub avg_leaf_depth: f64,
505}
506
507impl DagMetrics {
508 /// Calculate advanced metrics for an IPLD structure.
509 ///
510 /// # Arguments
511 ///
512 /// * `ipld` - The IPLD data to analyze
513 ///
514 /// # Returns
515 ///
516 /// Advanced metrics about the DAG structure
517 ///
518 /// # Examples
519 ///
520 /// ```rust
521 /// use ipfrs_core::{Ipld, CidBuilder};
522 /// use ipfrs_core::dag::DagMetrics;
523 /// use std::collections::BTreeMap;
524 ///
525 /// let cid = CidBuilder::new().build(b"data").unwrap();
526 /// let mut map = BTreeMap::new();
527 /// map.insert("link1".to_string(), Ipld::link(cid.clone()));
528 /// map.insert("link2".to_string(), Ipld::link(cid));
529 /// let ipld = Ipld::Map(map);
530 ///
531 /// let metrics = DagMetrics::from_ipld(&ipld);
532 /// assert_eq!(metrics.max_branching_factor, 2);
533 /// ```
534 pub fn from_ipld(ipld: &Ipld) -> Self {
535 let mut levels: HashMap<usize, usize> = HashMap::new();
536 let mut branching_factors = Vec::new();
537 let mut leaf_depths = Vec::new();
538 let mut total_nodes = 0;
539
540 calculate_metrics(
541 ipld,
542 0,
543 &mut levels,
544 &mut branching_factors,
545 &mut leaf_depths,
546 &mut total_nodes,
547 );
548
549 let width = levels.values().copied().max().unwrap_or(0);
550 let max_branching_factor = branching_factors.iter().copied().max().unwrap_or(0);
551 let avg_branching_factor = if branching_factors.is_empty() {
552 0.0
553 } else {
554 branching_factors.iter().sum::<usize>() as f64 / branching_factors.len() as f64
555 };
556 let avg_leaf_depth = if leaf_depths.is_empty() {
557 0.0
558 } else {
559 leaf_depths.iter().sum::<usize>() as f64 / leaf_depths.len() as f64
560 };
561
562 Self {
563 avg_branching_factor,
564 max_branching_factor,
565 width,
566 total_nodes,
567 avg_leaf_depth,
568 }
569 }
570}
571
572/// Helper function to calculate DAG metrics recursively
573fn calculate_metrics(
574 ipld: &Ipld,
575 depth: usize,
576 levels: &mut HashMap<usize, usize>,
577 branching_factors: &mut Vec<usize>,
578 leaf_depths: &mut Vec<usize>,
579 total_nodes: &mut usize,
580) {
581 *total_nodes += 1;
582 *levels.entry(depth).or_insert(0) += 1;
583
584 match ipld {
585 Ipld::List(items) => {
586 if items.is_empty() {
587 leaf_depths.push(depth);
588 } else {
589 branching_factors.push(items.len());
590 for item in items {
591 calculate_metrics(
592 item,
593 depth + 1,
594 levels,
595 branching_factors,
596 leaf_depths,
597 total_nodes,
598 );
599 }
600 }
601 }
602 Ipld::Map(map) => {
603 if map.is_empty() {
604 leaf_depths.push(depth);
605 } else {
606 branching_factors.push(map.len());
607 for value in map.values() {
608 calculate_metrics(
609 value,
610 depth + 1,
611 levels,
612 branching_factors,
613 leaf_depths,
614 total_nodes,
615 );
616 }
617 }
618 }
619 _ => {
620 leaf_depths.push(depth);
621 }
622 }
623}
624
625/// Perform a topological sort on IPLD nodes containing CID links.
626///
627/// Returns nodes in dependency order (dependencies before dependents).
628/// This is useful for processing DAGs where nodes depend on their children.
629///
630/// Note: Since we cannot fetch linked blocks, this only sorts the CIDs
631/// found in the IPLD structure based on their dependency relationships.
632///
633/// # Arguments
634///
635/// * `ipld` - The IPLD data to sort
636///
637/// # Returns
638///
639/// A vector of CIDs in topological order (leaves first, root last)
640///
641/// # Examples
642///
643/// ```rust
644/// use ipfrs_core::{Ipld, CidBuilder};
645/// use ipfrs_core::dag::topological_sort;
646/// use std::collections::BTreeMap;
647///
648/// let cid1 = CidBuilder::new().build(b"leaf1").unwrap();
649/// let cid2 = CidBuilder::new().build(b"leaf2").unwrap();
650///
651/// let mut map = BTreeMap::new();
652/// map.insert("child1".to_string(), Ipld::link(cid1));
653/// map.insert("child2".to_string(), Ipld::link(cid2));
654/// let ipld = Ipld::Map(map);
655///
656/// let sorted = topological_sort(&ipld);
657/// assert_eq!(sorted.len(), 2);
658/// ```
659pub fn topological_sort(ipld: &Ipld) -> Vec<Cid> {
660 let links = collect_all_links(ipld);
661 let mut result = Vec::new();
662 let mut visited = HashSet::new();
663
664 // Simple topological sort: collect all unique CIDs
665 // Since we can't fetch blocks, we just deduplicate and return in encounter order
666 for cid in links {
667 if visited.insert(cid) {
668 result.push(cid);
669 }
670 }
671
672 result
673}
674
675/// Calculate the size (number of nodes) of a subgraph rooted at the given IPLD node.
676///
677/// This counts all nodes reachable from the root, including the root itself.
678///
679/// # Arguments
680///
681/// * `ipld` - The root of the subgraph
682///
683/// # Returns
684///
685/// The total number of nodes in the subgraph
686///
687/// # Examples
688///
689/// ```rust
690/// use ipfrs_core::Ipld;
691/// use ipfrs_core::dag::subgraph_size;
692///
693/// let ipld = Ipld::List(vec![
694/// Ipld::Integer(1),
695/// Ipld::Integer(2),
696/// Ipld::List(vec![Ipld::Integer(3)]),
697/// ]);
698///
699/// let size = subgraph_size(&ipld);
700/// assert_eq!(size, 5); // Root list + 2 integers + nested list + nested integer
701/// ```
702pub fn subgraph_size(ipld: &Ipld) -> usize {
703 let mut count = 1; // Count the root
704
705 match ipld {
706 Ipld::List(items) => {
707 for item in items {
708 count += subgraph_size(item);
709 }
710 }
711 Ipld::Map(map) => {
712 for value in map.values() {
713 count += subgraph_size(value);
714 }
715 }
716 _ => {}
717 }
718
719 count
720}
721
722/// Calculate the fanout (number of direct children) for each level of the DAG.
723///
724/// Returns a vector where index i contains the total number of children
725/// at depth i in the DAG.
726///
727/// # Arguments
728///
729/// * `ipld` - The IPLD data to analyze
730///
731/// # Returns
732///
733/// A vector of fanout counts per level
734///
735/// # Examples
736///
737/// ```rust
738/// use ipfrs_core::Ipld;
739/// use ipfrs_core::dag::dag_fanout_by_level;
740///
741/// let ipld = Ipld::List(vec![
742/// Ipld::Integer(1),
743/// Ipld::List(vec![Ipld::Integer(2), Ipld::Integer(3)]),
744/// ]);
745///
746/// let fanout = dag_fanout_by_level(&ipld);
747/// assert!(fanout.len() >= 2);
748/// ```
749pub fn dag_fanout_by_level(ipld: &Ipld) -> Vec<usize> {
750 let mut fanout_by_level = Vec::new();
751 calculate_fanout(ipld, 0, &mut fanout_by_level);
752 fanout_by_level
753}
754
755/// Helper to calculate fanout at each level
756fn calculate_fanout(ipld: &Ipld, depth: usize, fanout_by_level: &mut Vec<usize>) {
757 // Ensure vector is large enough
758 while fanout_by_level.len() <= depth {
759 fanout_by_level.push(0);
760 }
761
762 match ipld {
763 Ipld::List(items) => {
764 fanout_by_level[depth] += items.len();
765 for item in items {
766 calculate_fanout(item, depth + 1, fanout_by_level);
767 }
768 }
769 Ipld::Map(map) => {
770 fanout_by_level[depth] += map.len();
771 for value in map.values() {
772 calculate_fanout(value, depth + 1, fanout_by_level);
773 }
774 }
775 _ => {}
776 }
777}
778
779/// Count the number of CID links at each depth level in the DAG.
780///
781/// Returns a vector where index i contains the count of CID links at depth i.
782///
783/// # Arguments
784///
785/// * `ipld` - The IPLD data to analyze
786///
787/// # Returns
788///
789/// A vector of link counts per depth level
790///
791/// # Examples
792///
793/// ```rust
794/// use ipfrs_core::{Ipld, CidBuilder};
795/// use ipfrs_core::dag::count_links_by_depth;
796///
797/// // Direct link at depth 0
798/// let cid = CidBuilder::new().build(b"test").unwrap();
799/// let ipld = Ipld::link(cid);
800///
801/// let counts = count_links_by_depth(&ipld);
802/// assert_eq!(counts[0], 1);
803/// ```
804pub fn count_links_by_depth(ipld: &Ipld) -> Vec<usize> {
805 let mut counts = Vec::new();
806 count_links_recursive(ipld, 0, &mut counts);
807 counts
808}
809
810/// Helper to count links at each depth
811fn count_links_recursive(ipld: &Ipld, depth: usize, counts: &mut Vec<usize>) {
812 // Ensure vector is large enough
813 while counts.len() <= depth {
814 counts.push(0);
815 }
816
817 match ipld {
818 Ipld::Link(_) => {
819 counts[depth] += 1;
820 }
821 Ipld::List(items) => {
822 for item in items {
823 count_links_recursive(item, depth + 1, counts);
824 }
825 }
826 Ipld::Map(map) => {
827 for value in map.values() {
828 count_links_recursive(value, depth + 1, counts);
829 }
830 }
831 _ => {}
832 }
833}
834
835/// Filter an IPLD structure to only include nodes matching a predicate.
836///
837/// This creates a new IPLD structure containing only the nodes where the
838/// predicate returns true.
839///
840/// # Arguments
841///
842/// * `ipld` - The IPLD data to filter
843/// * `predicate` - Function that returns true for nodes to keep
844///
845/// # Returns
846///
847/// A filtered IPLD structure, or None if the root doesn't match
848///
849/// # Examples
850///
851/// ```rust
852/// use ipfrs_core::Ipld;
853/// use ipfrs_core::dag::filter_dag;
854///
855/// let ipld = Ipld::List(vec![
856/// Ipld::Integer(1),
857/// Ipld::Integer(2),
858/// Ipld::String("hello".to_string()),
859/// ]);
860///
861/// // Keep only integers
862/// let filtered = filter_dag(&ipld, &|node| matches!(node, Ipld::Integer(_) | Ipld::List(_)));
863/// assert!(filtered.is_some());
864/// ```
865pub fn filter_dag<F>(ipld: &Ipld, predicate: &F) -> Option<Ipld>
866where
867 F: Fn(&Ipld) -> bool,
868{
869 if !predicate(ipld) {
870 return None;
871 }
872
873 match ipld {
874 Ipld::List(items) => {
875 let filtered_items: Vec<Ipld> = items
876 .iter()
877 .filter_map(|item| filter_dag(item, predicate))
878 .collect();
879 Some(Ipld::List(filtered_items))
880 }
881 Ipld::Map(map) => {
882 let filtered_map: std::collections::BTreeMap<String, Ipld> = map
883 .iter()
884 .filter_map(|(k, v)| filter_dag(v, predicate).map(|filtered| (k.clone(), filtered)))
885 .collect();
886 Some(Ipld::Map(filtered_map))
887 }
888 other => Some(other.clone()),
889 }
890}
891
892/// Transform all nodes in a DAG using a mapping function.
893///
894/// Applies the transformation function to each node in the IPLD structure,
895/// building a new transformed DAG.
896///
897/// # Arguments
898///
899/// * `ipld` - The IPLD data to transform
900/// * `transform` - Function to transform each node
901///
902/// # Returns
903///
904/// The transformed IPLD structure
905///
906/// # Examples
907///
908/// ```rust
909/// use ipfrs_core::Ipld;
910/// use ipfrs_core::dag::map_dag;
911///
912/// let ipld = Ipld::Integer(42);
913///
914/// // Double all integers
915/// let transformed = map_dag(&ipld, &|node| {
916/// match node {
917/// Ipld::Integer(n) => Ipld::Integer(n * 2),
918/// other => other.clone(),
919/// }
920/// });
921///
922/// assert_eq!(transformed, Ipld::Integer(84));
923/// ```
924pub fn map_dag<F>(ipld: &Ipld, transform: &F) -> Ipld
925where
926 F: Fn(&Ipld) -> Ipld,
927{
928 let transformed = match ipld {
929 Ipld::List(items) => {
930 let mapped_items: Vec<Ipld> =
931 items.iter().map(|item| map_dag(item, transform)).collect();
932 Ipld::List(mapped_items)
933 }
934 Ipld::Map(map) => {
935 let mapped_map: std::collections::BTreeMap<String, Ipld> = map
936 .iter()
937 .map(|(k, v)| (k.clone(), map_dag(v, transform)))
938 .collect();
939 Ipld::Map(mapped_map)
940 }
941 other => other.clone(),
942 };
943
944 transform(&transformed)
945}
946
947/// Find the differences between two IPLD DAGs
948///
949/// Returns a tuple of (unique_to_first, unique_to_second, common_links).
950/// This is useful for determining what changed between two versions of a DAG.
951///
952/// # Arguments
953///
954/// * `dag1` - First DAG to compare
955/// * `dag2` - Second DAG to compare
956///
957/// # Returns
958///
959/// A tuple of:
960/// - Links unique to dag1
961/// - Links unique to dag2
962/// - Links common to both
963///
964/// # Example
965///
966/// ```rust
967/// use ipfrs_core::{Ipld, CidBuilder};
968/// use ipfrs_core::dag::dag_diff;
969/// use std::collections::BTreeMap;
970///
971/// let cid1 = CidBuilder::new().build(b"a").unwrap();
972/// let cid2 = CidBuilder::new().build(b"b").unwrap();
973/// let cid3 = CidBuilder::new().build(b"c").unwrap();
974///
975/// let mut map1 = BTreeMap::new();
976/// map1.insert("link1".to_string(), Ipld::link(cid1));
977/// map1.insert("link2".to_string(), Ipld::link(cid2));
978///
979/// let mut map2 = BTreeMap::new();
980/// map2.insert("link2".to_string(), Ipld::link(cid2));
981/// map2.insert("link3".to_string(), Ipld::link(cid3));
982///
983/// let dag1 = Ipld::Map(map1);
984/// let dag2 = Ipld::Map(map2);
985///
986/// let (unique1, unique2, common) = dag_diff(&dag1, &dag2);
987/// assert_eq!(unique1.len(), 1); // cid1
988/// assert_eq!(unique2.len(), 1); // cid3
989/// assert_eq!(common.len(), 1); // cid2
990/// ```
991pub fn dag_diff(dag1: &Ipld, dag2: &Ipld) -> (HashSet<Cid>, HashSet<Cid>, HashSet<Cid>) {
992 let links1 = collect_unique_links(dag1);
993 let links2 = collect_unique_links(dag2);
994
995 let unique_to_first: HashSet<Cid> = links1.difference(&links2).copied().collect();
996 let unique_to_second: HashSet<Cid> = links2.difference(&links1).copied().collect();
997 let common: HashSet<Cid> = links1.intersection(&links2).copied().collect();
998
999 (unique_to_first, unique_to_second, common)
1000}
1001
1002/// Find common ancestor links between two DAGs
1003///
1004/// Returns the set of CID links that appear in both DAGs, which can help
1005/// identify shared structure or common ancestry.
1006///
1007/// # Arguments
1008///
1009/// * `dag1` - First DAG
1010/// * `dag2` - Second DAG
1011///
1012/// # Returns
1013///
1014/// A set of CIDs that appear in both DAGs
1015///
1016/// # Example
1017///
1018/// ```rust
1019/// use ipfrs_core::{Ipld, CidBuilder};
1020/// use ipfrs_core::dag::find_common_links;
1021///
1022/// let cid = CidBuilder::new().build(b"shared").unwrap();
1023///
1024/// let dag1 = Ipld::List(vec![Ipld::link(cid)]);
1025/// let dag2 = Ipld::List(vec![Ipld::link(cid)]);
1026///
1027/// let common = find_common_links(&dag1, &dag2);
1028/// assert_eq!(common.len(), 1);
1029/// ```
1030pub fn find_common_links(dag1: &Ipld, dag2: &Ipld) -> HashSet<Cid> {
1031 let links1 = collect_unique_links(dag1);
1032 let links2 = collect_unique_links(dag2);
1033
1034 links1.intersection(&links2).copied().collect()
1035}
1036
1037/// Prune nodes from a DAG based on a predicate
1038///
1039/// This function creates a new DAG with only the nodes that match the predicate.
1040/// It's useful for removing unwanted nodes or creating filtered views of a DAG.
1041///
1042/// # Arguments
1043///
1044/// * `ipld` - The DAG to prune
1045/// * `should_keep` - Predicate function returning true for nodes to keep
1046///
1047/// # Returns
1048///
1049/// A new IPLD structure with pruned nodes, or None if the root is pruned
1050///
1051/// # Example
1052///
1053/// ```rust
1054/// use ipfrs_core::Ipld;
1055/// use ipfrs_core::dag::prune_dag;
1056///
1057/// let ipld = Ipld::List(vec![
1058/// Ipld::Integer(1),
1059/// Ipld::Integer(2),
1060/// Ipld::String("keep".to_string()),
1061/// ]);
1062///
1063/// // Keep only strings
1064/// let pruned = prune_dag(&ipld, &|node| {
1065/// matches!(node, Ipld::String(_) | Ipld::List(_))
1066/// });
1067///
1068/// assert!(pruned.is_some());
1069/// ```
1070pub fn prune_dag<F>(ipld: &Ipld, should_keep: &F) -> Option<Ipld>
1071where
1072 F: Fn(&Ipld) -> bool,
1073{
1074 if !should_keep(ipld) {
1075 return None;
1076 }
1077
1078 match ipld {
1079 Ipld::List(items) => {
1080 let pruned_items: Vec<Ipld> = items
1081 .iter()
1082 .filter_map(|item| prune_dag(item, should_keep))
1083 .collect();
1084
1085 if pruned_items.is_empty() && !items.is_empty() {
1086 None
1087 } else {
1088 Some(Ipld::List(pruned_items))
1089 }
1090 }
1091 Ipld::Map(map) => {
1092 let pruned_map: std::collections::BTreeMap<String, Ipld> = map
1093 .iter()
1094 .filter_map(|(k, v)| prune_dag(v, should_keep).map(|pruned| (k.clone(), pruned)))
1095 .collect();
1096
1097 if pruned_map.is_empty() && !map.is_empty() {
1098 None
1099 } else {
1100 Some(Ipld::Map(pruned_map))
1101 }
1102 }
1103 other => Some(other.clone()),
1104 }
1105}
1106
1107/// Merge two DAGs into a single DAG
1108///
1109/// Creates a new DAG that contains all nodes from both input DAGs. When both
1110/// DAGs are maps, their keys are merged (dag2 values override dag1 on conflicts).
1111/// When both are lists, they are concatenated.
1112///
1113/// # Arguments
1114///
1115/// * `dag1` - First DAG
1116/// * `dag2` - Second DAG
1117///
1118/// # Returns
1119///
1120/// A merged IPLD structure
1121///
1122/// # Example
1123///
1124/// ```rust
1125/// use ipfrs_core::Ipld;
1126/// use ipfrs_core::dag::merge_dags;
1127/// use std::collections::BTreeMap;
1128///
1129/// let mut map1 = BTreeMap::new();
1130/// map1.insert("a".to_string(), Ipld::Integer(1));
1131///
1132/// let mut map2 = BTreeMap::new();
1133/// map2.insert("b".to_string(), Ipld::Integer(2));
1134///
1135/// let dag1 = Ipld::Map(map1);
1136/// let dag2 = Ipld::Map(map2);
1137///
1138/// let merged = merge_dags(&dag1, &dag2);
1139/// if let Ipld::Map(m) = merged {
1140/// assert_eq!(m.len(), 2);
1141/// }
1142/// ```
1143pub fn merge_dags(dag1: &Ipld, dag2: &Ipld) -> Ipld {
1144 match (dag1, dag2) {
1145 (Ipld::Map(map1), Ipld::Map(map2)) => {
1146 let mut merged = map1.clone();
1147 for (k, v) in map2 {
1148 merged.insert(k.clone(), v.clone());
1149 }
1150 Ipld::Map(merged)
1151 }
1152 (Ipld::List(list1), Ipld::List(list2)) => {
1153 let mut merged = list1.clone();
1154 merged.extend(list2.clone());
1155 Ipld::List(merged)
1156 }
1157 // If types don't match, prefer dag2
1158 (_, dag2) => dag2.clone(),
1159 }
1160}
1161
1162/// Count the total number of nodes in a DAG
1163///
1164/// This includes all nodes at all levels, counting duplicates if they appear
1165/// multiple times in the structure.
1166///
1167/// # Arguments
1168///
1169/// * `ipld` - The DAG to count nodes in
1170///
1171/// # Returns
1172///
1173/// The total number of nodes (including the root)
1174///
1175/// # Example
1176///
1177/// ```rust
1178/// use ipfrs_core::Ipld;
1179/// use ipfrs_core::dag::count_nodes;
1180///
1181/// let ipld = Ipld::List(vec![
1182/// Ipld::Integer(1),
1183/// Ipld::Integer(2),
1184/// Ipld::List(vec![Ipld::Integer(3)]),
1185/// ]);
1186///
1187/// assert_eq!(count_nodes(&ipld), 5); // List + 2 ints + inner list + 1 int
1188/// ```
1189pub fn count_nodes(ipld: &Ipld) -> usize {
1190 match ipld {
1191 Ipld::List(items) => 1 + items.iter().map(count_nodes).sum::<usize>(),
1192 Ipld::Map(map) => 1 + map.values().map(count_nodes).sum::<usize>(),
1193 _ => 1,
1194 }
1195}
1196
1197/// Get the maximum depth of a DAG
1198///
1199/// Returns the length of the longest path from the root to a leaf node.
1200///
1201/// # Arguments
1202///
1203/// * `ipld` - The DAG to measure
1204///
1205/// # Returns
1206///
1207/// The maximum depth (0 for leaf nodes, 1+ for containers)
1208///
1209/// # Example
1210///
1211/// ```rust
1212/// use ipfrs_core::Ipld;
1213/// use ipfrs_core::dag::dag_depth;
1214///
1215/// let ipld = Ipld::List(vec![
1216/// Ipld::Integer(1),
1217/// Ipld::List(vec![
1218/// Ipld::Integer(2),
1219/// Ipld::List(vec![Ipld::Integer(3)]),
1220/// ]),
1221/// ]);
1222///
1223/// assert_eq!(dag_depth(&ipld), 4);
1224/// ```
1225pub fn dag_depth(ipld: &Ipld) -> usize {
1226 match ipld {
1227 Ipld::List(items) => {
1228 if items.is_empty() {
1229 1
1230 } else {
1231 1 + items.iter().map(dag_depth).max().unwrap_or(0)
1232 }
1233 }
1234 Ipld::Map(map) => {
1235 if map.is_empty() {
1236 1
1237 } else {
1238 1 + map.values().map(dag_depth).max().unwrap_or(0)
1239 }
1240 }
1241 _ => 1,
1242 }
1243}
1244
1245/// Find all leaf nodes in a DAG
1246///
1247/// Returns all nodes that have no children (i.e., not List or Map, or empty containers).
1248///
1249/// # Arguments
1250///
1251/// * `ipld` - The DAG to search
1252///
1253/// # Returns
1254///
1255/// A vector of all leaf nodes
1256///
1257/// # Example
1258///
1259/// ```rust
1260/// use ipfrs_core::Ipld;
1261/// use ipfrs_core::dag::find_leaves;
1262///
1263/// let ipld = Ipld::List(vec![
1264/// Ipld::Integer(1),
1265/// Ipld::String("leaf".to_string()),
1266/// Ipld::List(vec![Ipld::Integer(2)]),
1267/// ]);
1268///
1269/// let leaves = find_leaves(&ipld);
1270/// assert_eq!(leaves.len(), 3); // Two integers and one string
1271/// ```
1272pub fn find_leaves(ipld: &Ipld) -> Vec<Ipld> {
1273 match ipld {
1274 Ipld::List(items) => {
1275 if items.is_empty() {
1276 vec![ipld.clone()]
1277 } else {
1278 items.iter().flat_map(find_leaves).collect()
1279 }
1280 }
1281 Ipld::Map(map) => {
1282 if map.is_empty() {
1283 vec![ipld.clone()]
1284 } else {
1285 map.values().flat_map(find_leaves).collect()
1286 }
1287 }
1288 _ => vec![ipld.clone()],
1289 }
1290}
1291
1292#[cfg(test)]
1293mod tests {
1294 use super::*;
1295 use crate::cid::CidBuilder;
1296 use std::collections::BTreeMap;
1297
1298 #[test]
1299 fn test_extract_links() {
1300 let cid1 = CidBuilder::new().build(b"test1").unwrap();
1301 let cid2 = CidBuilder::new().build(b"test2").unwrap();
1302
1303 let mut map = BTreeMap::new();
1304 map.insert("link1".to_string(), Ipld::link(cid1));
1305 map.insert("link2".to_string(), Ipld::link(cid2));
1306 map.insert("data".to_string(), Ipld::String("hello".to_string()));
1307
1308 let ipld = Ipld::Map(map);
1309 let links = extract_links(&ipld);
1310
1311 assert_eq!(links.len(), 2);
1312 assert!(links.contains(&cid1));
1313 assert!(links.contains(&cid2));
1314 }
1315
1316 #[test]
1317 fn test_collect_all_links_nested() {
1318 let cid1 = CidBuilder::new().build(b"test1").unwrap();
1319 let cid2 = CidBuilder::new().build(b"test2").unwrap();
1320 let cid3 = CidBuilder::new().build(b"test3").unwrap();
1321
1322 let mut inner = BTreeMap::new();
1323 inner.insert("deep_link".to_string(), Ipld::link(cid3));
1324
1325 let mut outer = BTreeMap::new();
1326 outer.insert("link1".to_string(), Ipld::link(cid1));
1327 outer.insert("link2".to_string(), Ipld::link(cid2));
1328 outer.insert("nested".to_string(), Ipld::Map(inner));
1329
1330 let ipld = Ipld::Map(outer);
1331 let links = collect_all_links(&ipld);
1332
1333 assert_eq!(links.len(), 3);
1334 assert!(links.contains(&cid1));
1335 assert!(links.contains(&cid2));
1336 assert!(links.contains(&cid3));
1337 }
1338
1339 #[test]
1340 fn test_collect_unique_links() {
1341 let cid1 = CidBuilder::new().build(b"test1").unwrap();
1342 let cid2 = CidBuilder::new().build(b"test2").unwrap();
1343
1344 // Create structure with duplicate links
1345 let list = vec![
1346 Ipld::link(cid1),
1347 Ipld::link(cid2),
1348 Ipld::link(cid1), // Duplicate
1349 ];
1350
1351 let ipld = Ipld::List(list);
1352 let unique = collect_unique_links(&ipld);
1353
1354 assert_eq!(unique.len(), 2);
1355 }
1356
1357 #[test]
1358 fn test_dag_stats() {
1359 let cid1 = CidBuilder::new().build(b"test1").unwrap();
1360 let cid2 = CidBuilder::new().build(b"test2").unwrap();
1361
1362 let mut map = BTreeMap::new();
1363 map.insert("link1".to_string(), Ipld::link(cid1));
1364 map.insert("link2".to_string(), Ipld::link(cid2));
1365 map.insert("dup_link".to_string(), Ipld::link(cid1)); // Duplicate
1366
1367 let ipld = Ipld::Map(map);
1368 let stats = DagStats::from_ipld(&ipld);
1369
1370 assert_eq!(stats.unique_cids, 2);
1371 assert_eq!(stats.total_links, 3);
1372 assert!(stats.deduplication_ratio() > 0.0);
1373 }
1374
1375 #[test]
1376 fn test_dag_stats_nested() {
1377 let cid1 = CidBuilder::new().build(b"test1").unwrap();
1378 let cid2 = CidBuilder::new().build(b"test2").unwrap();
1379
1380 let mut inner = BTreeMap::new();
1381 inner.insert("deep".to_string(), Ipld::link(cid2));
1382
1383 let mut outer = BTreeMap::new();
1384 outer.insert("link".to_string(), Ipld::link(cid1));
1385 outer.insert("nested".to_string(), Ipld::Map(inner));
1386
1387 let ipld = Ipld::Map(outer);
1388 let stats = DagStats::from_ipld(&ipld);
1389
1390 assert_eq!(stats.unique_cids, 2);
1391 assert_eq!(stats.max_depth, 2);
1392 }
1393
1394 #[test]
1395 fn test_is_dag() {
1396 let cid1 = CidBuilder::new().build(b"test1").unwrap();
1397
1398 let mut map = BTreeMap::new();
1399 map.insert("link".to_string(), Ipld::link(cid1));
1400
1401 let ipld = Ipld::Map(map);
1402 assert!(is_dag(&ipld));
1403 }
1404
1405 #[test]
1406 fn test_find_paths_to_cid() {
1407 let cid1 = CidBuilder::new().build(b"test1").unwrap();
1408 let cid2 = CidBuilder::new().build(b"test2").unwrap();
1409
1410 let mut inner = BTreeMap::new();
1411 inner.insert("target".to_string(), Ipld::link(cid1));
1412
1413 let mut outer = BTreeMap::new();
1414 outer.insert("other".to_string(), Ipld::link(cid2));
1415 outer.insert("nested".to_string(), Ipld::Map(inner));
1416
1417 let ipld = Ipld::Map(outer);
1418 let paths = find_paths_to_cid(&ipld, &cid1);
1419
1420 assert_eq!(paths.len(), 1);
1421 assert_eq!(paths[0], vec!["nested".to_string(), "target".to_string()]);
1422 }
1423
1424 #[test]
1425 fn test_traverse_bfs() {
1426 let list = vec![
1427 Ipld::Integer(1),
1428 Ipld::Integer(2),
1429 Ipld::List(vec![Ipld::Integer(3), Ipld::Integer(4)]),
1430 ];
1431 let ipld = Ipld::List(list);
1432
1433 let nodes = traverse_bfs(&ipld);
1434 assert_eq!(nodes.len(), 6); // Root + 3 direct children (1, 2, list) + 2 nested (3, 4)
1435 }
1436
1437 #[test]
1438 fn test_traverse_dfs() {
1439 let list = vec![
1440 Ipld::Integer(1),
1441 Ipld::List(vec![Ipld::Integer(2)]),
1442 Ipld::Integer(3),
1443 ];
1444 let ipld = Ipld::List(list);
1445
1446 let nodes = traverse_dfs(&ipld);
1447 assert_eq!(nodes.len(), 5); // Root + all children
1448 }
1449
1450 #[test]
1451 fn test_empty_ipld() {
1452 let ipld = Ipld::Null;
1453 let links = extract_links(&ipld);
1454 assert!(links.is_empty());
1455
1456 let stats = DagStats::from_ipld(&ipld);
1457 assert_eq!(stats.unique_cids, 0);
1458 assert_eq!(stats.total_links, 0);
1459 }
1460
1461 #[test]
1462 fn test_deduplication_ratio() {
1463 // No duplication
1464 let cid1 = CidBuilder::new().build(b"test1").unwrap();
1465 let cid2 = CidBuilder::new().build(b"test2").unwrap();
1466
1467 let list = vec![Ipld::link(cid1), Ipld::link(cid2)];
1468 let ipld = Ipld::List(list);
1469 let stats = DagStats::from_ipld(&ipld);
1470 assert_eq!(stats.deduplication_ratio(), 0.0);
1471
1472 // 50% duplication
1473 let list = vec![
1474 Ipld::link(cid1),
1475 Ipld::link(cid2),
1476 Ipld::link(cid1),
1477 Ipld::link(cid2),
1478 ];
1479 let ipld = Ipld::List(list);
1480 let stats = DagStats::from_ipld(&ipld);
1481 assert_eq!(stats.deduplication_ratio(), 0.5);
1482 }
1483
1484 #[test]
1485 fn test_dag_metrics() {
1486 let cid = CidBuilder::new().build(b"test").unwrap();
1487 let mut map = BTreeMap::new();
1488 map.insert("link1".to_string(), Ipld::link(cid));
1489 map.insert("link2".to_string(), Ipld::link(cid));
1490 let ipld = Ipld::Map(map);
1491
1492 let metrics = DagMetrics::from_ipld(&ipld);
1493 assert_eq!(metrics.max_branching_factor, 2);
1494 assert!(metrics.width >= 1);
1495 assert!(metrics.total_nodes > 0);
1496 }
1497
1498 #[test]
1499 fn test_dag_metrics_nested() {
1500 let cid = CidBuilder::new().build(b"test").unwrap();
1501
1502 let mut inner = BTreeMap::new();
1503 inner.insert("deep1".to_string(), Ipld::Integer(1));
1504 inner.insert("deep2".to_string(), Ipld::Integer(2));
1505 inner.insert("deep3".to_string(), Ipld::link(cid));
1506
1507 let mut outer = BTreeMap::new();
1508 outer.insert("nested".to_string(), Ipld::Map(inner));
1509 outer.insert("value".to_string(), Ipld::String("test".to_string()));
1510
1511 let ipld = Ipld::Map(outer);
1512 let metrics = DagMetrics::from_ipld(&ipld);
1513
1514 assert_eq!(metrics.max_branching_factor, 3);
1515 assert!(metrics.avg_branching_factor > 0.0);
1516 assert!(metrics.avg_leaf_depth > 0.0);
1517 }
1518
1519 #[test]
1520 fn test_topological_sort() {
1521 let cid1 = CidBuilder::new().build(b"leaf1").unwrap();
1522 let cid2 = CidBuilder::new().build(b"leaf2").unwrap();
1523
1524 let mut map = BTreeMap::new();
1525 map.insert("child1".to_string(), Ipld::link(cid1));
1526 map.insert("child2".to_string(), Ipld::link(cid2));
1527 let ipld = Ipld::Map(map);
1528
1529 let sorted = topological_sort(&ipld);
1530 assert_eq!(sorted.len(), 2);
1531 assert!(sorted.contains(&cid1));
1532 assert!(sorted.contains(&cid2));
1533 }
1534
1535 #[test]
1536 fn test_topological_sort_with_duplicates() {
1537 let cid1 = CidBuilder::new().build(b"test").unwrap();
1538
1539 let list = vec![Ipld::link(cid1), Ipld::link(cid1), Ipld::link(cid1)];
1540 let ipld = Ipld::List(list);
1541
1542 let sorted = topological_sort(&ipld);
1543 // Should deduplicate
1544 assert_eq!(sorted.len(), 1);
1545 assert_eq!(sorted[0], cid1);
1546 }
1547
1548 #[test]
1549 fn test_subgraph_size() {
1550 let ipld = Ipld::List(vec![
1551 Ipld::Integer(1),
1552 Ipld::Integer(2),
1553 Ipld::List(vec![Ipld::Integer(3)]),
1554 ]);
1555
1556 let size = subgraph_size(&ipld);
1557 assert_eq!(size, 5); // Root list + 2 integers + nested list + nested integer
1558 }
1559
1560 #[test]
1561 fn test_subgraph_size_single_node() {
1562 let ipld = Ipld::Integer(42);
1563 let size = subgraph_size(&ipld);
1564 assert_eq!(size, 1);
1565 }
1566
1567 #[test]
1568 fn test_dag_fanout_by_level() {
1569 let ipld = Ipld::List(vec![
1570 Ipld::Integer(1),
1571 Ipld::List(vec![Ipld::Integer(2), Ipld::Integer(3)]),
1572 ]);
1573
1574 let fanout = dag_fanout_by_level(&ipld);
1575 assert!(fanout.len() >= 2);
1576 assert_eq!(fanout[0], 2); // Root has 2 children
1577 }
1578
1579 #[test]
1580 fn test_dag_fanout_empty() {
1581 let ipld = Ipld::Integer(42);
1582 let fanout = dag_fanout_by_level(&ipld);
1583 // Scalar value has no children
1584 assert!(fanout.is_empty() || fanout.iter().all(|&f| f == 0));
1585 }
1586
1587 #[test]
1588 fn test_count_links_by_depth() {
1589 let cid = CidBuilder::new().build(b"test").unwrap();
1590 let mut map = BTreeMap::new();
1591 map.insert("link".to_string(), Ipld::link(cid));
1592 let ipld = Ipld::Map(map);
1593
1594 let counts = count_links_by_depth(&ipld);
1595 assert!(!counts.is_empty());
1596 // Links inside map values are at depth 1
1597 assert_eq!(counts[1], 1);
1598 }
1599
1600 #[test]
1601 fn test_count_links_by_depth_nested() {
1602 let cid1 = CidBuilder::new().build(b"test1").unwrap();
1603 let cid2 = CidBuilder::new().build(b"test2").unwrap();
1604
1605 let mut inner = BTreeMap::new();
1606 inner.insert("deep".to_string(), Ipld::link(cid2));
1607
1608 let mut outer = BTreeMap::new();
1609 outer.insert("shallow".to_string(), Ipld::link(cid1));
1610 outer.insert("nested".to_string(), Ipld::Map(inner));
1611
1612 let ipld = Ipld::Map(outer);
1613 let counts = count_links_by_depth(&ipld);
1614
1615 assert!(counts.len() >= 2);
1616 // Links in outer map are at depth 1
1617 assert_eq!(counts[1], 1); // One link at depth 1 (shallow)
1618 // Links in inner map are at depth 2
1619 assert_eq!(counts[2], 1); // One link at depth 2 (deep)
1620 }
1621
1622 #[test]
1623 fn test_filter_dag() {
1624 let ipld = Ipld::List(vec![
1625 Ipld::Integer(1),
1626 Ipld::Integer(2),
1627 Ipld::String("hello".to_string()),
1628 ]);
1629
1630 // Keep only integers and lists
1631 let filtered = filter_dag(&ipld, &|node| {
1632 matches!(node, Ipld::Integer(_) | Ipld::List(_))
1633 });
1634 assert!(filtered.is_some());
1635
1636 if let Some(Ipld::List(items)) = filtered {
1637 assert_eq!(items.len(), 2); // Only the two integers
1638 } else {
1639 panic!("Expected filtered list");
1640 }
1641 }
1642
1643 #[test]
1644 fn test_filter_dag_all_filtered() {
1645 let ipld = Ipld::Integer(42);
1646
1647 // Filter out everything
1648 let filtered = filter_dag(&ipld, &|_| false);
1649 assert!(filtered.is_none());
1650 }
1651
1652 #[test]
1653 fn test_map_dag() {
1654 let ipld = Ipld::Integer(42);
1655
1656 // Double all integers
1657 let transformed = map_dag(&ipld, &|node| match node {
1658 Ipld::Integer(n) => Ipld::Integer(n * 2),
1659 other => other.clone(),
1660 });
1661
1662 assert_eq!(transformed, Ipld::Integer(84));
1663 }
1664
1665 #[test]
1666 fn test_map_dag_nested() {
1667 let ipld = Ipld::List(vec![Ipld::Integer(1), Ipld::Integer(2)]);
1668
1669 // Double all integers
1670 let transformed = map_dag(&ipld, &|node| match node {
1671 Ipld::Integer(n) => Ipld::Integer(n * 2),
1672 other => other.clone(),
1673 });
1674
1675 if let Ipld::List(items) = transformed {
1676 assert_eq!(items[0], Ipld::Integer(2));
1677 assert_eq!(items[1], Ipld::Integer(4));
1678 } else {
1679 panic!("Expected list");
1680 }
1681 }
1682
1683 #[test]
1684 fn test_map_dag_preserve_structure() {
1685 let mut map = BTreeMap::new();
1686 map.insert("a".to_string(), Ipld::Integer(1));
1687 map.insert("b".to_string(), Ipld::Integer(2));
1688 let ipld = Ipld::Map(map);
1689
1690 // Transform preserves map structure
1691 let transformed = map_dag(&ipld, &|node| match node {
1692 Ipld::Integer(n) => Ipld::Integer(n + 10),
1693 other => other.clone(),
1694 });
1695
1696 if let Ipld::Map(result_map) = transformed {
1697 assert_eq!(result_map.get("a"), Some(&Ipld::Integer(11)));
1698 assert_eq!(result_map.get("b"), Some(&Ipld::Integer(12)));
1699 } else {
1700 panic!("Expected map");
1701 }
1702 }
1703}