Skip to main content

yaml_edit/
visitor.rs

1//! Visitor pattern for traversing YAML AST nodes.
2//!
3//! Allows traversing and processing YAML structures without modifying node types.
4//! Useful for validation, transformation, collection, or analysis.
5//!
6//! # Example
7//!
8//! ```rust
9//! use yaml_edit::visitor::{YamlVisitor, YamlAccept};
10//! use yaml_edit::{Document, Scalar, Mapping, Sequence};
11//! use std::str::FromStr;
12//!
13//! struct ScalarCounter {
14//!     count: usize,
15//! }
16//!
17//! impl YamlVisitor for ScalarCounter {
18//!     fn visit_scalar(&mut self, _scalar: &Scalar) {
19//!         self.count += 1;
20//!     }
21//! }
22//!
23//! let doc = Document::from_str("key: value\nlist:\n  - item1\n  - item2").unwrap();
24//! let mut counter = ScalarCounter { count: 0 };
25//! doc.accept(&mut counter);
26//! assert_eq!(counter.count, 5); // "key", "value", "list", "item1", "item2"
27//! ```
28//!
29//! Default traversal implementations automatically visit child nodes. Override
30//! `visit_mapping`, `visit_sequence` or `visit_tagged_node` for custom
31//! traversal logic.
32//!
33//! A tagged node (`!!str x`, `!custom {a: 1}`) is handed to
34//! `visit_tagged_node`, which by default descends into the tagged content, so
35//! a visitor that only implements `visit_scalar` still sees the scalar
36//! inside `!!str x`.
37
38use crate::yaml::{Document, Mapping, Scalar, Sequence, TaggedNode, YamlFile};
39
40/// Trait for implementing the visitor pattern on YAML nodes.
41pub trait YamlVisitor {
42    /// Visit a YAML root node
43    fn visit_yaml(&mut self, yaml: &YamlFile) {
44        // Default implementation visits all documents
45        for doc in yaml.documents() {
46            self.visit_document(&doc);
47        }
48    }
49
50    /// Visit a document node
51    fn visit_document(&mut self, document: &Document) {
52        // Default implementation visits the document's content
53        if let Some(mapping) = document.as_mapping() {
54            self.visit_mapping(&mapping);
55        } else if let Some(sequence) = document.as_sequence() {
56            self.visit_sequence(&sequence);
57        } else if let Some(scalar) = document.as_scalar() {
58            self.visit_scalar(&scalar);
59        }
60    }
61
62    /// Visit a scalar node
63    fn visit_scalar(&mut self, _scalar: &Scalar) {}
64
65    /// Visit a mapping node
66    ///
67    /// The default implementation traverses all key-value pairs in the mapping,
68    /// visiting both keys and values recursively. Override this method if you need
69    /// custom logic when encountering mappings.
70    fn visit_mapping(&mut self, mapping: &Mapping) {
71        self.walk_mapping(mapping);
72    }
73
74    /// Visit a sequence node
75    ///
76    /// The default implementation traverses all items in the sequence recursively.
77    /// Override this method if you need custom logic when encountering sequences.
78    fn visit_sequence(&mut self, sequence: &Sequence) {
79        self.walk_sequence(sequence);
80    }
81
82    /// Visit a node carrying an explicit tag (`!!str x`, `!custom {a: 1}`).
83    ///
84    /// The default implementation descends into the tagged content, so a
85    /// visitor that only cares about scalars still sees `!!str x`. Override
86    /// this method to inspect the tag itself.
87    fn visit_tagged_node(&mut self, tagged: &TaggedNode) {
88        self.walk_tagged_node(tagged);
89    }
90
91    /// Traverse the content a tag is attached to (helper for default
92    /// traversal).
93    fn walk_tagged_node(&mut self, tagged: &TaggedNode) {
94        if let Some(mapping) = tagged.as_mapping() {
95            self.visit_mapping(&mapping);
96        } else if let Some(sequence) = tagged.as_sequence() {
97            self.visit_sequence(&sequence);
98        } else if let Some(scalar) = tagged.value() {
99            self.visit_scalar(&scalar);
100        }
101    }
102
103    /// Traverse all key-value pairs in a mapping (helper for default traversal).
104    ///
105    /// This method is called by the default `visit_mapping` implementation.
106    /// You can call it explicitly if you override `visit_mapping` and want to
107    /// preserve the default traversal behavior.
108    fn walk_mapping(&mut self, mapping: &Mapping) {
109        use crate::yaml::{extract_mapping, extract_scalar, extract_sequence, extract_tagged_node};
110
111        for (key_node, value_node) in mapping.pairs() {
112            // Visit key
113            if let Some(scalar) = extract_scalar(&key_node) {
114                self.visit_scalar(&scalar);
115            } else if let Some(tagged) = extract_tagged_node(&key_node) {
116                self.visit_tagged_node(&tagged);
117            } else if let Some(sequence) = extract_sequence(&key_node) {
118                self.visit_sequence(&sequence);
119            } else if let Some(mapping) = extract_mapping(&key_node) {
120                self.visit_mapping(&mapping);
121            }
122
123            // Visit value
124            if let Some(scalar) = extract_scalar(&value_node) {
125                self.visit_scalar(&scalar);
126            } else if let Some(tagged) = extract_tagged_node(&value_node) {
127                self.visit_tagged_node(&tagged);
128            } else if let Some(nested_mapping) = extract_mapping(&value_node) {
129                self.visit_mapping(&nested_mapping);
130            } else if let Some(nested_sequence) = extract_sequence(&value_node) {
131                self.visit_sequence(&nested_sequence);
132            }
133        }
134    }
135
136    /// Traverse all items in a sequence (helper for default traversal).
137    ///
138    /// This method is called by the default `visit_sequence` implementation.
139    /// You can call it explicitly if you override `visit_sequence` and want to
140    /// preserve the default traversal behavior.
141    fn walk_sequence(&mut self, sequence: &Sequence) {
142        use crate::yaml::{extract_mapping, extract_scalar, extract_sequence, extract_tagged_node};
143
144        for item in sequence.items() {
145            if let Some(scalar) = extract_scalar(&item) {
146                self.visit_scalar(&scalar);
147            } else if let Some(tagged) = extract_tagged_node(&item) {
148                self.visit_tagged_node(&tagged);
149            } else if let Some(nested_mapping) = extract_mapping(&item) {
150                self.visit_mapping(&nested_mapping);
151            } else if let Some(nested_sequence) = extract_sequence(&item) {
152                self.visit_sequence(&nested_sequence);
153            }
154        }
155    }
156}
157
158/// Trait for nodes that can accept a visitor
159pub trait YamlAccept {
160    /// Accept a visitor for traversal
161    fn accept<V: YamlVisitor>(&self, visitor: &mut V);
162}
163
164impl YamlAccept for YamlFile {
165    fn accept<V: YamlVisitor>(&self, visitor: &mut V) {
166        visitor.visit_yaml(self);
167    }
168}
169
170impl YamlAccept for Document {
171    fn accept<V: YamlVisitor>(&self, visitor: &mut V) {
172        visitor.visit_document(self);
173    }
174}
175
176impl YamlAccept for Scalar {
177    fn accept<V: YamlVisitor>(&self, visitor: &mut V) {
178        visitor.visit_scalar(self);
179    }
180}
181
182impl YamlAccept for Mapping {
183    fn accept<V: YamlVisitor>(&self, visitor: &mut V) {
184        visitor.visit_mapping(self);
185    }
186}
187
188impl YamlAccept for Sequence {
189    fn accept<V: YamlVisitor>(&self, visitor: &mut V) {
190        visitor.visit_sequence(self);
191    }
192}
193
194/// A visitor that collects all scalar values from a YAML document
195#[derive(Debug, Clone)]
196pub struct ScalarCollector {
197    /// The collected scalar values
198    pub scalars: Vec<String>,
199}
200
201impl ScalarCollector {
202    /// Create a new scalar collector
203    pub fn new() -> Self {
204        Self {
205            scalars: Vec::new(),
206        }
207    }
208}
209
210impl Default for ScalarCollector {
211    fn default() -> Self {
212        Self::new()
213    }
214}
215
216impl YamlVisitor for ScalarCollector {
217    fn visit_scalar(&mut self, scalar: &Scalar) {
218        self.scalars.push(scalar.to_string());
219    }
220    // Default traversal methods handle mapping and sequence traversal automatically
221}
222
223/// A visitor that counts different types of nodes
224#[derive(Debug, Clone)]
225pub struct NodeCounter {
226    /// Number of document nodes encountered
227    pub document_count: usize,
228    /// Number of scalar nodes encountered
229    pub scalar_count: usize,
230    /// Number of mapping nodes encountered
231    pub mapping_count: usize,
232    /// Number of sequence nodes encountered
233    pub sequence_count: usize,
234}
235
236impl NodeCounter {
237    /// Create a new node counter
238    pub fn new() -> Self {
239        Self {
240            document_count: 0,
241            scalar_count: 0,
242            mapping_count: 0,
243            sequence_count: 0,
244        }
245    }
246
247    /// Get total node count
248    pub fn total(&self) -> usize {
249        self.document_count + self.scalar_count + self.mapping_count + self.sequence_count
250    }
251}
252
253impl Default for NodeCounter {
254    fn default() -> Self {
255        Self::new()
256    }
257}
258
259impl YamlVisitor for NodeCounter {
260    fn visit_document(&mut self, document: &Document) {
261        self.document_count += 1;
262        // Continue visiting children
263        if let Some(mapping) = document.as_mapping() {
264            self.visit_mapping(&mapping);
265        } else if let Some(sequence) = document.as_sequence() {
266            self.visit_sequence(&sequence);
267        } else if let Some(scalar) = document.as_scalar() {
268            self.visit_scalar(&scalar);
269        }
270    }
271
272    fn visit_scalar(&mut self, _scalar: &Scalar) {
273        self.scalar_count += 1;
274    }
275
276    fn visit_mapping(&mut self, mapping: &Mapping) {
277        self.mapping_count += 1;
278        // Call default traversal to visit children
279        self.walk_mapping(mapping);
280    }
281
282    fn visit_sequence(&mut self, sequence: &Sequence) {
283        self.sequence_count += 1;
284        // Call default traversal to visit children
285        self.walk_sequence(sequence);
286    }
287}
288
289/// A visitor that transforms scalar values
290pub struct ScalarTransformer<F>
291where
292    F: FnMut(&str) -> String,
293{
294    transform: F,
295    transformed: Vec<(String, String)>, // (original, transformed) pairs
296}
297
298impl<F> ScalarTransformer<F>
299where
300    F: FnMut(&str) -> String,
301{
302    /// Create a new scalar transformer with the given transformation function
303    pub fn new(transform: F) -> Self {
304        Self {
305            transform,
306            transformed: Vec::new(),
307        }
308    }
309
310    /// Get the transformed pairs
311    pub fn results(&self) -> &[(String, String)] {
312        &self.transformed
313    }
314}
315
316impl<F> YamlVisitor for ScalarTransformer<F>
317where
318    F: FnMut(&str) -> String,
319{
320    fn visit_scalar(&mut self, scalar: &Scalar) {
321        let original = scalar.to_string();
322        let transformed = (self.transform)(&original);
323        self.transformed.push((original, transformed));
324    }
325
326    // Default traversal methods handle mapping and sequence traversal automatically
327}
328#[cfg(test)]
329mod tests {
330    use super::*;
331    use crate::YamlFile;
332
333    struct ScalarNames {
334        seen: Vec<String>,
335    }
336
337    impl YamlVisitor for ScalarNames {
338        fn visit_scalar(&mut self, scalar: &Scalar) {
339            self.seen.push(scalar.as_string());
340        }
341    }
342
343    fn visited_scalars(src: &str) -> Vec<String> {
344        use std::str::FromStr;
345        let doc = Document::from_str(src).unwrap();
346        let mut v = ScalarNames { seen: vec![] };
347        doc.accept(&mut v);
348        v.seen
349    }
350
351    #[test]
352    fn test_walks_into_tagged_nodes() {
353        // A tag used to stop traversal dead: walk_sequence cast the item
354        // straight to Scalar/Mapping/Sequence, and the mapping walk unwrapped
355        // only KEY/VALUE, so everything under a TAGGED_NODE went unvisited.
356        assert_eq!(visited_scalars("- !!str a\n- b\n"), ["a", "b"]);
357        assert_eq!(visited_scalars("k: !!str v\n"), ["k", "v"]);
358        assert_eq!(visited_scalars("!!str k: v\n"), ["k", "v"]);
359        assert_eq!(visited_scalars("k: !!map\n  a: 1\n"), ["k", "a", "1"]);
360        assert_eq!(visited_scalars("- !!seq\n  - a\n"), ["a"]);
361    }
362
363    #[test]
364    fn test_visit_tagged_node_can_be_overridden() {
365        struct Tags {
366            tags: Vec<String>,
367        }
368        impl YamlVisitor for Tags {
369            fn visit_tagged_node(&mut self, tagged: &TaggedNode) {
370                self.tags.push(tagged.tag().unwrap_or_default());
371                // Deliberately do not descend.
372            }
373        }
374        use std::str::FromStr;
375        let doc = Document::from_str("- !!str a\n- !custom b\n").unwrap();
376        let mut v = Tags { tags: vec![] };
377        doc.accept(&mut v);
378        assert_eq!(v.tags, ["!!str", "!custom"]);
379    }
380
381    #[test]
382    fn test_scalar_collector() {
383        let yaml_text = r#"
384name: John Doe
385age: 30
386address:
387  street: 123 Main St
388  city: New York
389  country: USA
390hobbies:
391  - reading
392  - coding
393  - hiking
394"#;
395
396        let parsed = YamlFile::parse(yaml_text);
397        let yaml = parsed.tree();
398
399        let mut collector = ScalarCollector::new();
400        yaml.accept(&mut collector);
401
402        // Should collect all scalar values from the document
403        assert_eq!(
404            collector.scalars,
405            vec![
406                "name",
407                "John Doe",
408                "age",
409                "30",
410                "address",
411                "street",
412                "123 Main St",
413                "city",
414                "New York",
415                "country",
416                "USA",
417                "hobbies",
418                "reading",
419                "coding",
420                "hiking",
421            ]
422        );
423    }
424
425    #[test]
426    fn test_node_counter() {
427        let yaml_text = r#"
428name: John Doe
429age: 30
430address:
431  street: 123 Main St
432  city: New York
433  country: USA
434hobbies:
435  - reading
436  - coding
437  - hiking
438"#;
439
440        let parsed = YamlFile::parse(yaml_text);
441        let yaml = parsed.tree();
442
443        let mut counter = NodeCounter::new();
444        yaml.accept(&mut counter);
445
446        // Should count all node types
447        assert_eq!(counter.document_count, 1);
448        assert_eq!(counter.scalar_count, 15);
449        assert_eq!(counter.mapping_count, 2); // Root mapping and address mapping
450        assert_eq!(counter.sequence_count, 1); // hobbies sequence
451
452        // Total should be sum of all counts
453        assert_eq!(
454            counter.total(),
455            counter.document_count
456                + counter.scalar_count
457                + counter.mapping_count
458                + counter.sequence_count
459        );
460    }
461
462    #[test]
463    fn test_scalar_transformer() {
464        let yaml_text = r#"
465name: john
466city: new york
467country: usa
468"#;
469
470        let parsed = YamlFile::parse(yaml_text);
471        let yaml = parsed.tree();
472
473        // Transform all scalars to uppercase
474        let mut transformer = ScalarTransformer::new(|s: &str| s.to_uppercase());
475        yaml.accept(&mut transformer);
476
477        let results = transformer.results();
478
479        // Check that transformations were applied
480        assert_eq!(
481            results,
482            &[
483                ("name".to_string(), "NAME".to_string()),
484                ("john".to_string(), "JOHN".to_string()),
485                ("city".to_string(), "CITY".to_string()),
486                ("new york".to_string(), "NEW YORK".to_string()),
487                ("country".to_string(), "COUNTRY".to_string()),
488                ("usa".to_string(), "USA".to_string()),
489            ]
490        );
491    }
492
493    #[test]
494    fn test_visitor_on_sequence() {
495        let yaml_text = r#"
496- item1
497- item2
498- nested:
499    - subitem1
500    - subitem2
501"#;
502
503        let parsed = YamlFile::parse(yaml_text);
504        let yaml = parsed.tree();
505
506        let mut collector = ScalarCollector::new();
507        yaml.accept(&mut collector);
508
509        // Should collect all scalars including nested ones
510        assert_eq!(
511            collector.scalars,
512            vec!["item1", "item2", "nested", "subitem1", "subitem2"]
513        );
514    }
515
516    #[test]
517    fn test_visitor_on_empty_document() {
518        let yaml_text = "";
519
520        let parsed = YamlFile::parse(yaml_text);
521        let yaml = parsed.tree();
522
523        let mut counter = NodeCounter::new();
524        yaml.accept(&mut counter);
525
526        // Empty document should have no nodes (or minimal structure)
527        assert_eq!(counter.total(), 0);
528    }
529
530    #[test]
531    fn test_custom_visitor() {
532        // Define a custom visitor that counts only keys in mappings
533        struct KeyCounter {
534            key_count: usize,
535        }
536
537        impl YamlVisitor for KeyCounter {
538            fn visit_scalar(&mut self, _scalar: &crate::Scalar) {
539                // Don't count scalars that are not keys
540            }
541
542            fn visit_mapping(&mut self, mapping: &crate::Mapping) {
543                // Count keys in this mapping
544                for (_key, value) in mapping.iter() {
545                    self.key_count += 1;
546                    // Recursively visit nested structures
547                    if let Some(nested_mapping) = value.as_mapping() {
548                        nested_mapping.accept(self);
549                    } else if let Some(nested_sequence) = value.as_sequence() {
550                        nested_sequence.accept(self);
551                    }
552                }
553            }
554
555            fn visit_sequence(&mut self, sequence: &crate::Sequence) {
556                // Visit items in sequence to find nested mappings
557                for value in sequence.values() {
558                    if let Some(nested_mapping) = value.as_mapping() {
559                        nested_mapping.accept(self);
560                    } else if let Some(nested_sequence) = value.as_sequence() {
561                        nested_sequence.accept(self);
562                    }
563                }
564            }
565        }
566
567        let yaml_text = r#"
568name: John
569age: 30
570address:
571  street: 123 Main St
572  city: New York
573metadata:
574  created: 2024-01-01
575  updated: 2024-01-02
576"#;
577
578        let parsed = YamlFile::parse(yaml_text);
579        let yaml = parsed.tree();
580
581        let mut key_counter = KeyCounter { key_count: 0 };
582        yaml.accept(&mut key_counter);
583
584        // Should count: name, age, address, street, city, metadata, created, updated = 8 keys
585        assert_eq!(key_counter.key_count, 8);
586    }
587
588    #[test]
589    fn test_visitor_with_multiple_documents() {
590        let yaml_text = r#"
591---
592doc1: value1
593---
594doc2: value2
595---
596doc3: value3
597"#;
598
599        let parsed = YamlFile::parse(yaml_text);
600        let yaml = parsed.tree();
601
602        let mut counter = NodeCounter::new();
603        yaml.accept(&mut counter);
604
605        // Should count 3 documents
606        assert_eq!(counter.document_count, 3);
607        assert!(counter.scalar_count >= 6); // At least 3 keys and 3 values
608    }
609}