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}