1use crate::Document;
10use std::collections::HashMap;
11
12#[derive(Debug, Clone, PartialEq, Eq)]
14pub enum PathSegment {
15 Key(String),
17 NestedKey(String),
19 Index(usize),
21 NodeId(String),
23}
24
25impl PathSegment {
26 pub fn as_str(&self) -> String {
28 match self {
29 PathSegment::Key(s) | PathSegment::NestedKey(s) | PathSegment::NodeId(s) => s.clone(),
30 PathSegment::Index(i) => format!("[{}]", i),
31 }
32 }
33}
34
35#[derive(Debug)]
55pub struct VisitorContext<'a> {
56 pub depth: usize,
58
59 pub path: Vec<PathSegment>,
61
62 pub document: &'a Document,
64
65 pub current_schema: Option<&'a [String]>,
67
68 metadata: HashMap<String, String>,
70
71 stats: TraversalStats,
73}
74
75#[derive(Debug, Clone, Default)]
77pub struct TraversalStats {
78 pub nodes_visited: usize,
80 pub scalars_visited: usize,
82 pub lists_visited: usize,
84 pub objects_visited: usize,
86 pub max_depth_reached: usize,
88}
89
90impl<'a> VisitorContext<'a> {
91 pub fn new(document: &'a Document) -> Self {
97 Self {
98 depth: 0,
99 path: Vec::new(),
100 document,
101 current_schema: None,
102 metadata: HashMap::new(),
103 stats: TraversalStats::default(),
104 }
105 }
106
107 pub fn child(&self, segment: PathSegment) -> Self {
113 let mut path = self.path.clone();
114 path.push(segment);
115 Self {
116 depth: self.depth + 1,
117 path,
118 document: self.document,
119 current_schema: self.current_schema,
120 metadata: self.metadata.clone(),
121 stats: self.stats.clone(),
122 }
123 }
124
125 pub fn with_schema(&self, schema: &'a [String]) -> Self {
129 Self {
130 depth: self.depth,
131 path: self.path.clone(),
132 document: self.document,
133 current_schema: Some(schema),
134 metadata: self.metadata.clone(),
135 stats: self.stats.clone(),
136 }
137 }
138
139 pub fn path_string(&self) -> String {
155 if self.path.is_empty() {
156 "root".to_string()
157 } else {
158 self.path
159 .iter()
160 .map(|seg| seg.as_str())
161 .collect::<Vec<_>>()
162 .join(".")
163 }
164 }
165
166 pub fn set_metadata(&mut self, key: impl Into<String>, value: impl Into<String>) {
171 self.metadata.insert(key.into(), value.into());
172 }
173
174 pub fn get_metadata(&self, key: &str) -> Option<&str> {
176 self.metadata.get(key).map(|s| s.as_str())
177 }
178
179 pub fn stats(&self) -> &TraversalStats {
181 &self.stats
182 }
183
184 #[allow(dead_code)]
186 pub(crate) fn stats_mut(&mut self) -> &mut TraversalStats {
187 &mut self.stats
188 }
189
190 pub(crate) fn record_node_visit(&mut self) {
192 self.stats.nodes_visited += 1;
193 self.stats.max_depth_reached = self.stats.max_depth_reached.max(self.depth);
194 }
195
196 pub(crate) fn record_scalar_visit(&mut self) {
198 self.stats.scalars_visited += 1;
199 self.stats.max_depth_reached = self.stats.max_depth_reached.max(self.depth);
200 }
201
202 pub(crate) fn record_list_visit(&mut self) {
204 self.stats.lists_visited += 1;
205 }
206
207 pub(crate) fn record_object_visit(&mut self) {
209 self.stats.objects_visited += 1;
210 }
211}
212
213#[cfg(test)]
214mod tests {
215 use super::*;
216
217 #[test]
218 fn test_new_context() {
219 let doc = Document::new((1, 0));
220 let ctx = VisitorContext::new(&doc);
221
222 assert_eq!(ctx.depth, 0);
223 assert!(ctx.path.is_empty());
224 assert_eq!(ctx.path_string(), "root");
225 assert!(ctx.current_schema.is_none());
226 }
227
228 #[test]
229 fn test_child_context_increments_depth() {
230 let doc = Document::new((1, 0));
231 let ctx = VisitorContext::new(&doc);
232 let child = ctx.child(PathSegment::Key("users".to_string()));
233
234 assert_eq!(child.depth, 1);
235 assert_eq!(child.path.len(), 1);
236 }
237
238 #[test]
239 fn test_path_string_with_nested_keys() {
240 let doc = Document::new((1, 0));
241 let ctx = VisitorContext::new(&doc);
242 let ctx = ctx.child(PathSegment::Key("a".to_string()));
243 let ctx = ctx.child(PathSegment::NestedKey("b".to_string()));
244 let ctx = ctx.child(PathSegment::Key("c".to_string()));
245
246 assert_eq!(ctx.path_string(), "a.b.c");
247 }
248
249 #[test]
250 fn test_path_string_with_index() {
251 let doc = Document::new((1, 0));
252 let ctx = VisitorContext::new(&doc);
253 let ctx = ctx.child(PathSegment::Key("users".to_string()));
254 let ctx = ctx.child(PathSegment::Index(0));
255
256 assert_eq!(ctx.path_string(), "users.[0]");
257 }
258
259 #[test]
260 fn test_with_schema() {
261 let doc = Document::new((1, 0));
262 let ctx = VisitorContext::new(&doc);
263 let schema = vec!["id".to_string(), "name".to_string()];
264 let ctx_with_schema = ctx.with_schema(&schema);
265
266 assert!(ctx_with_schema.current_schema.is_some());
267 assert_eq!(ctx_with_schema.current_schema.unwrap().len(), 2);
268 assert_eq!(ctx_with_schema.depth, ctx.depth);
269 }
270
271 #[test]
272 fn test_metadata_storage() {
273 let doc = Document::new((1, 0));
274 let mut ctx = VisitorContext::new(&doc);
275
276 ctx.set_metadata("key", "value");
277 assert_eq!(ctx.get_metadata("key"), Some("value"));
278 assert_eq!(ctx.get_metadata("missing"), None);
279 }
280
281 #[test]
282 fn test_metadata_persists_in_child() {
283 let doc = Document::new((1, 0));
284 let mut ctx = VisitorContext::new(&doc);
285 ctx.set_metadata("key", "value");
286
287 let child = ctx.child(PathSegment::Key("child".to_string()));
288 assert_eq!(child.get_metadata("key"), Some("value"));
289 }
290
291 #[test]
292 fn test_stats_tracking() {
293 let doc = Document::new((1, 0));
294 let mut ctx = VisitorContext::new(&doc);
295
296 ctx.record_node_visit();
297 ctx.record_scalar_visit();
298 ctx.record_list_visit();
299 ctx.record_object_visit();
300
301 let stats = ctx.stats();
302 assert_eq!(stats.nodes_visited, 1);
303 assert_eq!(stats.scalars_visited, 1);
304 assert_eq!(stats.lists_visited, 1);
305 assert_eq!(stats.objects_visited, 1);
306 }
307
308 #[test]
309 fn test_stats_max_depth() {
310 let doc = Document::new((1, 0));
311 let mut ctx = VisitorContext::new(&doc);
312
313 ctx.record_node_visit();
314 assert_eq!(ctx.stats().max_depth_reached, 0);
315
316 let mut child = ctx.child(PathSegment::Key("a".to_string()));
317 child.record_node_visit();
318 assert_eq!(child.stats().max_depth_reached, 1);
319 }
320
321 #[test]
322 fn test_path_segment_as_str() {
323 assert_eq!(PathSegment::Key("test".to_string()).as_str(), "test");
324 assert_eq!(
325 PathSegment::NestedKey("nested".to_string()).as_str(),
326 "nested"
327 );
328 assert_eq!(PathSegment::Index(5).as_str(), "[5]");
329 assert_eq!(PathSegment::NodeId("id123".to_string()).as_str(), "id123");
330 }
331
332 #[test]
333 fn test_stats_default() {
334 let stats = TraversalStats::default();
335 assert_eq!(stats.nodes_visited, 0);
336 assert_eq!(stats.scalars_visited, 0);
337 assert_eq!(stats.lists_visited, 0);
338 assert_eq!(stats.objects_visited, 0);
339 assert_eq!(stats.max_depth_reached, 0);
340 }
341
342 #[test]
343 fn test_stats_clone() {
344 let stats = TraversalStats {
345 nodes_visited: 5,
346 ..Default::default()
347 };
348 let cloned = stats.clone();
349 assert_eq!(cloned.nodes_visited, 5);
350 }
351}