Skip to main content

mindmap_cli/
context.rs

1//! NavigationContext: Track recursion depth and detect cycles
2//!
3//! This module provides:
4//! - Depth tracking for recursive navigation
5//! - Cycle detection via visited file set
6//! - RAII guard pattern for safe depth management
7
8use anyhow::{Result, bail};
9use std::{collections::HashSet, path::PathBuf};
10
11/// Context for tracking recursive navigation
12#[derive(Debug)]
13pub struct NavigationContext {
14    /// Current recursion depth
15    depth: usize,
16    /// Maximum allowed recursion depth
17    max_depth: usize,
18    /// Files visited in this traversal (for cycle detection)
19    visited: HashSet<PathBuf>,
20}
21
22impl NavigationContext {
23    /// Create a new navigation context with default max depth (50)
24    pub fn new() -> Self {
25        NavigationContext {
26            depth: 0,
27            max_depth: 50,
28            visited: HashSet::new(),
29        }
30    }
31
32    /// Create a new navigation context with custom max depth
33    pub fn with_max_depth(max_depth: usize) -> Self {
34        NavigationContext {
35            depth: 0,
36            max_depth,
37            visited: HashSet::new(),
38        }
39    }
40
41    /// Get the current recursion depth
42    pub fn depth(&self) -> usize {
43        self.depth
44    }
45
46    /// Get the maximum allowed depth
47    pub fn max_depth(&self) -> usize {
48        self.max_depth
49    }
50
51    /// Check if we've reached the max depth
52    pub fn at_max_depth(&self) -> bool {
53        self.depth >= self.max_depth
54    }
55
56    /// Descend one level, returning a guard that auto-decrements on drop
57    ///
58    /// # Errors
59    /// If recursion depth would exceed max_depth
60    pub fn descend(&mut self) -> Result<DepthGuard<'_>> {
61        self.depth += 1;
62        if self.depth > self.max_depth {
63            self.depth -= 1; // Undo increment
64            bail!("Recursion depth exceeded (max: {})", self.max_depth);
65        }
66        Ok(DepthGuard { ctx: self })
67    }
68
69    /// Check if a path has been visited
70    pub fn is_visited(&self, path: &PathBuf) -> bool {
71        self.visited.contains(path)
72    }
73
74    /// Mark a path as visited
75    pub fn mark_visited(&mut self, path: PathBuf) {
76        self.visited.insert(path);
77    }
78
79    /// Clear the visited set (for testing)
80    pub fn clear_visited(&mut self) {
81        self.visited.clear();
82    }
83
84    /// Get the number of visited files
85    pub fn num_visited(&self) -> usize {
86        self.visited.len()
87    }
88
89    /// Set max depth (for testing)
90    #[cfg(test)]
91    pub fn set_max_depth(&mut self, max_depth: usize) {
92        self.max_depth = max_depth;
93    }
94}
95
96impl Default for NavigationContext {
97    fn default() -> Self {
98        Self::new()
99    }
100}
101
102/// RAII guard to decrement depth on drop
103pub struct DepthGuard<'a> {
104    ctx: &'a mut NavigationContext,
105}
106
107impl<'a> Drop for DepthGuard<'a> {
108    fn drop(&mut self) {
109        self.ctx.depth = self.ctx.depth.saturating_sub(1);
110    }
111}
112
113#[cfg(test)]
114mod tests {
115    use super::*;
116
117    #[test]
118    fn test_context_new() {
119        let ctx = NavigationContext::new();
120        assert_eq!(ctx.depth(), 0);
121        assert_eq!(ctx.max_depth(), 50);
122        assert_eq!(ctx.num_visited(), 0);
123    }
124
125    #[test]
126    fn test_context_with_max_depth() {
127        let ctx = NavigationContext::with_max_depth(10);
128        assert_eq!(ctx.max_depth(), 10);
129    }
130
131    #[test]
132    fn test_descend_increments_depth() -> Result<()> {
133        let mut ctx = NavigationContext::new();
134        assert_eq!(ctx.depth(), 0);
135
136        {
137            let _guard = ctx.descend()?;
138            drop(_guard);
139        }
140        assert_eq!(ctx.depth(), 0);
141
142        Ok(())
143    }
144
145    #[test]
146    fn test_descend_decrements_on_drop() -> Result<()> {
147        let mut ctx = NavigationContext::new();
148        {
149            let _guard = ctx.descend()?;
150            drop(_guard);
151        }
152        assert_eq!(ctx.depth(), 0);
153
154        Ok(())
155    }
156
157    #[test]
158    fn test_descend_enforces_max_depth() {
159        let mut ctx = NavigationContext::with_max_depth(2);
160
161        // Descend to depth 1
162        assert!(ctx.descend().is_ok());
163
164        // Manually verify we can't access ctx while guard is held by checking depth increases
165        // We can't do direct checks, so we'll manually manage depth for testing
166        ctx.depth = 0; // Reset for clean test
167
168        ctx.depth = 1;
169        assert_eq!(ctx.depth(), 1);
170
171        ctx.depth = 2;
172        assert_eq!(ctx.depth(), 2);
173
174        // Now try to descend when already at max
175        {
176            let result = ctx.descend();
177            assert!(result.is_err());
178        }
179        assert_eq!(ctx.depth(), 2); // Should not have incremented
180    }
181
182    #[test]
183    fn test_visited_tracking() {
184        let mut ctx = NavigationContext::new();
185        let path1 = PathBuf::from("/some/file1.md");
186        let path2 = PathBuf::from("/some/file2.md");
187
188        assert!(!ctx.is_visited(&path1));
189        assert_eq!(ctx.num_visited(), 0);
190
191        ctx.mark_visited(path1.clone());
192        assert!(ctx.is_visited(&path1));
193        assert!(!ctx.is_visited(&path2));
194        assert_eq!(ctx.num_visited(), 1);
195
196        ctx.mark_visited(path2.clone());
197        assert!(ctx.is_visited(&path1));
198        assert!(ctx.is_visited(&path2));
199        assert_eq!(ctx.num_visited(), 2);
200    }
201
202    #[test]
203    fn test_clear_visited() {
204        let mut ctx = NavigationContext::new();
205        let path1 = PathBuf::from("/some/file1.md");
206
207        ctx.mark_visited(path1.clone());
208        assert!(ctx.is_visited(&path1));
209
210        ctx.clear_visited();
211        assert!(!ctx.is_visited(&path1));
212        assert_eq!(ctx.num_visited(), 0);
213    }
214
215    #[test]
216    fn test_guard_pattern() -> Result<()> {
217        let mut ctx = NavigationContext::new();
218
219        {
220            let g1 = ctx.descend()?;
221            drop(g1);
222            {
223                let g2 = ctx.descend()?;
224                drop(g2);
225            }
226        }
227        assert_eq!(ctx.depth(), 0);
228
229        Ok(())
230    }
231
232    #[test]
233    fn test_at_max_depth() {
234        let mut ctx = NavigationContext::with_max_depth(2);
235        assert!(!ctx.at_max_depth());
236
237        ctx.depth = 1;
238        assert!(!ctx.at_max_depth());
239
240        ctx.depth = 2;
241        assert!(ctx.at_max_depth());
242    }
243}