hedl_core/visitor/
visitor.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//! Immutable visitor trait for read-only document traversal.
8
9use crate::visitor::{VisitDecision, VisitorContext};
10use crate::{Document, MatrixList, Node, Value};
11
12/// Immutable visitor trait for read-only tree traversal.
13///
14/// This is the primary visitor trait for analyzing and inspecting
15/// HEDL documents without modification. All methods have default
16/// implementations that return `Continue`, allowing implementations
17/// to override only the methods they need.
18///
19/// # Control Flow
20///
21/// Methods return [`VisitDecision`] to control traversal:
22/// - `Continue`: Visit this element and its children
23/// - `SkipChildren`: Visit this element but skip its children
24/// - `Stop`: Terminate traversal immediately
25///
26/// # Example: Count Nodes by Type
27///
28/// ```
29/// use hedl_core::visitor::{Visitor, VisitDecision, VisitorContext};
30/// use hedl_core::Node;
31/// use std::collections::HashMap;
32///
33/// struct TypeCounter {
34///     counts: HashMap<String, usize>,
35/// }
36///
37/// impl Visitor for TypeCounter {
38///     fn visit_node(&mut self, node: &Node, _ctx: &VisitorContext<'_>) -> VisitDecision {
39///         *self.counts.entry(node.type_name.clone()).or_insert(0) += 1;
40///         VisitDecision::Continue
41///     }
42/// }
43/// ```
44///
45/// # Example: Find First Match
46///
47/// ```
48/// use hedl_core::visitor::{Visitor, VisitDecision, VisitorContext};
49/// use hedl_core::Node;
50///
51/// struct FindUser {
52///     target: String,
53///     found: Option<String>,
54/// }
55///
56/// impl Visitor for FindUser {
57///     fn visit_node(&mut self, node: &Node, _ctx: &VisitorContext<'_>) -> VisitDecision {
58///         if node.type_name == "User" && node.id == self.target {
59///             self.found = Some(node.id.clone());
60///             VisitDecision::Stop  // Early termination
61///         } else {
62///             VisitDecision::Continue
63///         }
64///     }
65/// }
66/// ```
67pub trait Visitor {
68    /// Called at the start of document traversal.
69    ///
70    /// # Arguments
71    ///
72    /// - `doc`: The document being traversed
73    /// - `ctx`: Visitor context with path and depth information
74    ///
75    /// # Returns
76    ///
77    /// `VisitDecision` to control whether to continue traversal.
78    fn begin_document(&mut self, _doc: &Document, _ctx: &VisitorContext<'_>) -> VisitDecision {
79        VisitDecision::Continue
80    }
81
82    /// Called at the end of document traversal.
83    ///
84    /// This is called after all root items have been visited, even if
85    /// some traversal was skipped via `SkipChildren`.
86    fn end_document(&mut self, _doc: &Document, _ctx: &VisitorContext<'_>) -> VisitDecision {
87        VisitDecision::Continue
88    }
89
90    /// Called when visiting a scalar value.
91    ///
92    /// # Arguments
93    ///
94    /// - `key`: The key/field name for this scalar
95    /// - `value`: The scalar value
96    /// - `ctx`: Visitor context
97    fn visit_scalar(
98        &mut self,
99        _key: &str,
100        _value: &Value,
101        _ctx: &VisitorContext<'_>,
102    ) -> VisitDecision {
103        VisitDecision::Continue
104    }
105
106    /// Called before visiting an object's children.
107    ///
108    /// Return `SkipChildren` to skip the object's contents.
109    fn begin_object(&mut self, _key: &str, _ctx: &VisitorContext<'_>) -> VisitDecision {
110        VisitDecision::Continue
111    }
112
113    /// Called after visiting an object's children.
114    fn end_object(&mut self, _key: &str, _ctx: &VisitorContext<'_>) -> VisitDecision {
115        VisitDecision::Continue
116    }
117
118    /// Called before visiting a list's rows.
119    ///
120    /// # Arguments
121    ///
122    /// - `key`: The key for this list
123    /// - `list`: The matrix list with schema and rows
124    /// - `ctx`: Visitor context
125    ///
126    /// Return `SkipChildren` to skip all rows in the list.
127    fn begin_list(
128        &mut self,
129        _key: &str,
130        _list: &MatrixList,
131        _ctx: &VisitorContext<'_>,
132    ) -> VisitDecision {
133        VisitDecision::Continue
134    }
135
136    /// Called after visiting a list's rows.
137    fn end_list(
138        &mut self,
139        _key: &str,
140        _list: &MatrixList,
141        _ctx: &VisitorContext<'_>,
142    ) -> VisitDecision {
143        VisitDecision::Continue
144    }
145
146    /// Called when visiting a node (row) in a list.
147    ///
148    /// This is called for both top-level list rows and nested child nodes.
149    ///
150    /// # Arguments
151    ///
152    /// - `node`: The node being visited
153    /// - `ctx`: Visitor context with current path and depth
154    fn visit_node(&mut self, _node: &Node, _ctx: &VisitorContext<'_>) -> VisitDecision {
155        VisitDecision::Continue
156    }
157
158    /// Called before visiting a node's children.
159    ///
160    /// Return `SkipChildren` to skip nested child nodes.
161    fn begin_node_children(&mut self, _node: &Node, _ctx: &VisitorContext<'_>) -> VisitDecision {
162        VisitDecision::Continue
163    }
164
165    /// Called after visiting a node's children.
166    fn end_node_children(&mut self, _node: &Node, _ctx: &VisitorContext<'_>) -> VisitDecision {
167        VisitDecision::Continue
168    }
169
170    /// Called when visiting a reference value.
171    ///
172    /// This is called for `Value::Reference` instances.
173    fn visit_reference(
174        &mut self,
175        _reference: &crate::Reference,
176        _ctx: &VisitorContext<'_>,
177    ) -> VisitDecision {
178        VisitDecision::Continue
179    }
180
181    /// Called when visiting an expression value.
182    ///
183    /// This is called for `Value::Expression` instances.
184    fn visit_expression(
185        &mut self,
186        _expr: &crate::lex::Expression,
187        _ctx: &VisitorContext<'_>,
188    ) -> VisitDecision {
189        VisitDecision::Continue
190    }
191
192    /// Called when visiting a tensor value.
193    ///
194    /// This is called for `Value::Tensor` instances.
195    fn visit_tensor(
196        &mut self,
197        _tensor: &crate::lex::Tensor,
198        _ctx: &VisitorContext<'_>,
199    ) -> VisitDecision {
200        VisitDecision::Continue
201    }
202}
203
204#[cfg(test)]
205mod tests {
206    use super::*;
207
208    struct NoOpVisitor;
209    impl Visitor for NoOpVisitor {}
210
211    #[test]
212    fn test_default_implementations_return_continue() {
213        let mut visitor = NoOpVisitor;
214        let doc = Document::new((1, 0));
215        let ctx = VisitorContext::new(&doc);
216
217        assert_eq!(visitor.begin_document(&doc, &ctx), VisitDecision::Continue);
218        assert_eq!(visitor.end_document(&doc, &ctx), VisitDecision::Continue);
219        assert_eq!(
220            visitor.visit_scalar("key", &Value::Null, &ctx),
221            VisitDecision::Continue
222        );
223        assert_eq!(visitor.begin_object("key", &ctx), VisitDecision::Continue);
224        assert_eq!(visitor.end_object("key", &ctx), VisitDecision::Continue);
225    }
226
227    struct CountingVisitor {
228        scalar_count: usize,
229        node_count: usize,
230    }
231
232    impl Visitor for CountingVisitor {
233        fn visit_scalar(&mut self, _: &str, _: &Value, _: &VisitorContext<'_>) -> VisitDecision {
234            self.scalar_count += 1;
235            VisitDecision::Continue
236        }
237
238        fn visit_node(&mut self, _: &Node, _: &VisitorContext<'_>) -> VisitDecision {
239            self.node_count += 1;
240            VisitDecision::Continue
241        }
242    }
243
244    #[test]
245    fn test_visitor_can_count_elements() {
246        let mut visitor = CountingVisitor {
247            scalar_count: 0,
248            node_count: 0,
249        };
250
251        let doc = Document::new((1, 0));
252        let ctx = VisitorContext::new(&doc);
253
254        visitor.visit_scalar("key", &Value::Int(42), &ctx);
255        visitor.visit_scalar("key2", &Value::String("test".into()), &ctx);
256
257        let node = Node::new("User", "1", vec![]);
258        visitor.visit_node(&node, &ctx);
259
260        assert_eq!(visitor.scalar_count, 2);
261        assert_eq!(visitor.node_count, 1);
262    }
263
264    struct EarlyStopVisitor {
265        stop_after: usize,
266        count: usize,
267    }
268
269    impl Visitor for EarlyStopVisitor {
270        fn visit_node(&mut self, _: &Node, _: &VisitorContext<'_>) -> VisitDecision {
271            self.count += 1;
272            if self.count >= self.stop_after {
273                VisitDecision::Stop
274            } else {
275                VisitDecision::Continue
276            }
277        }
278    }
279
280    #[test]
281    fn test_visitor_can_stop_early() {
282        let mut visitor = EarlyStopVisitor {
283            stop_after: 2,
284            count: 0,
285        };
286
287        let doc = Document::new((1, 0));
288        let ctx = VisitorContext::new(&doc);
289
290        let node = Node::new("User", "1", vec![]);
291
292        assert_eq!(visitor.visit_node(&node, &ctx), VisitDecision::Continue);
293        assert_eq!(visitor.visit_node(&node, &ctx), VisitDecision::Stop);
294        assert_eq!(visitor.count, 2);
295    }
296
297    struct SkipChildrenVisitor;
298
299    impl Visitor for SkipChildrenVisitor {
300        fn begin_object(&mut self, _: &str, _: &VisitorContext<'_>) -> VisitDecision {
301            VisitDecision::SkipChildren
302        }
303    }
304
305    #[test]
306    fn test_visitor_can_skip_children() {
307        let mut visitor = SkipChildrenVisitor;
308        let doc = Document::new((1, 0));
309        let ctx = VisitorContext::new(&doc);
310
311        assert_eq!(
312            visitor.begin_object("obj", &ctx),
313            VisitDecision::SkipChildren
314        );
315    }
316}