hedl_core/visitor/
context.rs

1// Dweve HEDL - Hierarchical Entity Data Language
2//
3// Copyright (c) 2025 Dweve IP B.V. and individual contributors.
4//
5// SPDX-License-Identifier: Apache-2.0
6
7//! Visitor context for tracking traversal state.
8
9use crate::Document;
10use std::collections::HashMap;
11
12/// Path segment in the document tree.
13#[derive(Debug, Clone, PartialEq, Eq)]
14pub enum PathSegment {
15    /// Root-level key
16    Key(String),
17    /// Object nested key
18    NestedKey(String),
19    /// List row index
20    Index(usize),
21    /// Node ID
22    NodeId(String),
23}
24
25impl PathSegment {
26    /// Convert to string representation.
27    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/// Context provided to visitors during traversal.
36///
37/// This context tracks the current position in the document tree,
38/// provides access to the document metadata, and supports custom
39/// data storage for visitor state.
40///
41/// # Examples
42///
43/// ```
44/// use hedl_core::visitor::VisitorContext;
45/// use hedl_core::Document;
46///
47/// let doc = Document::new((1, 0));
48/// let ctx = VisitorContext::new(&doc);
49///
50/// assert_eq!(ctx.depth, 0);
51/// assert!(ctx.path.is_empty());
52/// assert_eq!(ctx.path_string(), "root");
53/// ```
54#[derive(Debug)]
55pub struct VisitorContext<'a> {
56    /// Current nesting depth (0 = root level).
57    pub depth: usize,
58
59    /// Path from root to current element.
60    pub path: Vec<PathSegment>,
61
62    /// Reference to the document being traversed.
63    pub document: &'a Document,
64
65    /// Schema for the current list (if within a list context).
66    pub current_schema: Option<&'a [String]>,
67
68    /// Custom metadata storage for visitor-specific data.
69    metadata: HashMap<String, String>,
70
71    /// Statistics tracking.
72    stats: TraversalStats,
73}
74
75/// Traversal statistics collected during document traversal.
76#[derive(Debug, Clone, Default)]
77pub struct TraversalStats {
78    /// Number of nodes visited.
79    pub nodes_visited: usize,
80    /// Number of scalars visited.
81    pub scalars_visited: usize,
82    /// Number of lists visited.
83    pub lists_visited: usize,
84    /// Number of objects visited.
85    pub objects_visited: usize,
86    /// Maximum depth reached.
87    pub max_depth_reached: usize,
88}
89
90impl<'a> VisitorContext<'a> {
91    /// Create a new context for the root level.
92    ///
93    /// # Arguments
94    ///
95    /// - `document`: Reference to the document being traversed
96    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    /// Create a child context with incremented depth.
108    ///
109    /// # Arguments
110    ///
111    /// - `segment`: Path segment for the child element
112    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    /// Create a child context with a different schema.
126    ///
127    /// Used when entering a list with its own schema.
128    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    /// Get the current path as a string (for error messages).
140    ///
141    /// # Example
142    ///
143    /// ```
144    /// use hedl_core::visitor::{VisitorContext, PathSegment};
145    /// use hedl_core::Document;
146    ///
147    /// let doc = Document::new((1, 0));
148    /// let ctx = VisitorContext::new(&doc);
149    /// assert_eq!(ctx.path_string(), "root");
150    ///
151    /// let child_ctx = ctx.child(PathSegment::Key("users".to_string()));
152    /// assert_eq!(child_ctx.path_string(), "users");
153    /// ```
154    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    /// Store custom metadata.
167    ///
168    /// This allows visitors to attach arbitrary string metadata
169    /// to specific paths during traversal.
170    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    /// Retrieve custom metadata.
175    pub fn get_metadata(&self, key: &str) -> Option<&str> {
176        self.metadata.get(key).map(|s| s.as_str())
177    }
178
179    /// Get traversal statistics.
180    pub fn stats(&self) -> &TraversalStats {
181        &self.stats
182    }
183
184    /// Get mutable traversal statistics.
185    #[allow(dead_code)]
186    pub(crate) fn stats_mut(&mut self) -> &mut TraversalStats {
187        &mut self.stats
188    }
189
190    /// Record a node visit in statistics.
191    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    /// Record a scalar visit in statistics.
197    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    /// Record a list visit in statistics.
203    pub(crate) fn record_list_visit(&mut self) {
204        self.stats.lists_visited += 1;
205    }
206
207    /// Record an object visit in statistics.
208    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}