1use anyhow::{Result, bail};
9use std::{collections::HashSet, path::PathBuf};
10
11#[derive(Debug)]
13pub struct NavigationContext {
14 depth: usize,
16 max_depth: usize,
18 visited: HashSet<PathBuf>,
20}
21
22impl NavigationContext {
23 pub fn new() -> Self {
25 NavigationContext {
26 depth: 0,
27 max_depth: 50,
28 visited: HashSet::new(),
29 }
30 }
31
32 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 pub fn depth(&self) -> usize {
43 self.depth
44 }
45
46 pub fn max_depth(&self) -> usize {
48 self.max_depth
49 }
50
51 pub fn at_max_depth(&self) -> bool {
53 self.depth >= self.max_depth
54 }
55
56 pub fn descend(&mut self) -> Result<DepthGuard<'_>> {
61 self.depth += 1;
62 if self.depth > self.max_depth {
63 self.depth -= 1; bail!("Recursion depth exceeded (max: {})", self.max_depth);
65 }
66 Ok(DepthGuard { ctx: self })
67 }
68
69 pub fn is_visited(&self, path: &PathBuf) -> bool {
71 self.visited.contains(path)
72 }
73
74 pub fn mark_visited(&mut self, path: PathBuf) {
76 self.visited.insert(path);
77 }
78
79 pub fn clear_visited(&mut self) {
81 self.visited.clear();
82 }
83
84 pub fn num_visited(&self) -> usize {
86 self.visited.len()
87 }
88
89 #[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
102pub 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 assert!(ctx.descend().is_ok());
163
164 ctx.depth = 0; ctx.depth = 1;
169 assert_eq!(ctx.depth(), 1);
170
171 ctx.depth = 2;
172 assert_eq!(ctx.depth(), 2);
173
174 {
176 let result = ctx.descend();
177 assert!(result.is_err());
178 }
179 assert_eq!(ctx.depth(), 2); }
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}