Skip to main content

yaml_edit/
path.rs

1//! Path-based access to YAML documents.
2//!
3//! Provides convenient dot-separated path syntax for accessing nested YAML values
4//! like `"server.host"` or `"database.primary.port"`.
5//!
6//! Operations: [`get_path`](YamlPath::get_path), [`set_path`](YamlPath::set_path), [`remove_path`](YamlPath::remove_path)
7//!
8//! # Example
9//!
10//! ```
11//! use yaml_edit::{Document, path::YamlPath};
12//! use std::str::FromStr;
13//!
14//! let yaml = Document::from_str("server:\n  host: localhost\n  port: 8080\n").unwrap();
15//!
16//! // Get nested values
17//! let host = yaml.get_path("server.host");
18//!
19//! // Set nested values (creates intermediate mappings)
20//! yaml.set_path("database.primary.host", "db.example.com");
21//!
22//! // Remove nested values
23//! yaml.remove_path("server.port");
24//! ```
25//!
26//! All operations preserve formatting, comments, and whitespace.
27
28use crate::builder::MappingBuilder;
29use crate::yaml::Mapping;
30
31/// Trait for YAML types that support path-based access.
32///
33/// Path syntax uses dots (`.`) as separators to navigate nested mappings.
34/// For example, `"server.database.host"` accesses:
35/// ```yaml
36/// server:
37///   database:
38///     host: value
39/// ```
40pub trait YamlPath {
41    /// Get a value at a nested path.
42    ///
43    /// # Arguments
44    ///
45    /// * `path` - Dot-separated path like `"server.host"` or `"db.primary.port"`
46    ///
47    /// # Returns
48    ///
49    /// `Some(YamlNode)` if the path exists, `None` otherwise.
50    ///
51    /// # Examples
52    ///
53    /// ```
54    /// use yaml_edit::{Document, path::YamlPath};
55    /// use std::str::FromStr;
56    ///
57    /// let yaml = Document::from_str("server:\n  host: localhost\n").unwrap();
58    /// let host = yaml.get_path("server.host");
59    /// assert!(host.is_some());
60    /// ```
61    fn get_path(&self, path: &str) -> Option<crate::as_yaml::YamlNode>;
62
63    /// Set a value at a nested path, creating intermediate mappings if needed.
64    ///
65    /// # Arguments
66    ///
67    /// * `path` - Dot-separated path like `"server.host"`
68    /// * `value` - Value to set (can be any type implementing `AsYaml`)
69    ///
70    /// # Examples
71    ///
72    /// ```
73    /// use yaml_edit::{Document, path::YamlPath};
74    /// use std::str::FromStr;
75    ///
76    /// let yaml = Document::from_str("name: test\n").unwrap();
77    /// yaml.set_path("server.host", "localhost");
78    /// yaml.set_path("server.port", 8080);
79    /// ```
80    fn set_path(&self, path: &str, value: impl crate::AsYaml);
81
82    /// Remove a value at a nested path.
83    ///
84    /// # Arguments
85    ///
86    /// * `path` - Dot-separated path to the value to remove
87    ///
88    /// # Returns
89    ///
90    /// `true` if the value was found and removed, `false` otherwise.
91    ///
92    /// # Examples
93    ///
94    /// ```
95    /// use yaml_edit::{Document, path::YamlPath};
96    /// use std::str::FromStr;
97    ///
98    /// let yaml = Document::from_str("server:\n  host: localhost\n  port: 8080\n").unwrap();
99    /// assert_eq!(yaml.remove_path("server.port"), true);
100    /// assert_eq!(yaml.remove_path("server.missing"), false);
101    /// ```
102    fn remove_path(&self, path: &str) -> bool;
103}
104
105/// Represents a segment in a YAML path.
106#[derive(Debug, Clone, PartialEq)]
107pub enum PathSegment {
108    /// A mapping key (e.g., "server" in "server.host")
109    Key(String),
110    /// An array index (e.g., `0` in "items\[0\]" or "items.0")
111    Index(usize),
112}
113
114/// Parse a path string into components.
115///
116/// Supports multiple syntaxes:
117/// - Dot notation: `"server.host"` → `[Key("server"), Key("host")]`
118/// - Array indices with brackets: `"items[0].name"` → `[Key("items"), Index(0), Key("name")]`
119/// - Array indices with dots: `"items.0.name"` → `[Key("items"), Index(0), Key("name")]`
120/// - Escaped dots: `"key\\.with\\.dots"` → `[Key("key.with.dots")]`
121///
122/// # Examples
123///
124/// ```
125/// use yaml_edit::path::{parse_path, PathSegment};
126///
127/// let segments = parse_path("server.host");
128/// assert_eq!(segments, vec![
129///     PathSegment::Key("server".to_string()),
130///     PathSegment::Key("host".to_string())
131/// ]);
132///
133/// let segments = parse_path("items[0].name");
134/// assert_eq!(segments, vec![
135///     PathSegment::Key("items".to_string()),
136///     PathSegment::Index(0),
137///     PathSegment::Key("name".to_string())
138/// ]);
139///
140/// let segments = parse_path("items.0");
141/// assert_eq!(segments, vec![
142///     PathSegment::Key("items".to_string()),
143///     PathSegment::Index(0)
144/// ]);
145/// ```
146pub fn parse_path(path: &str) -> Vec<PathSegment> {
147    if path.is_empty() {
148        return vec![];
149    }
150
151    let mut segments = Vec::new();
152    let mut current = String::new();
153    let mut chars = path.chars().peekable();
154    let mut escaped = false;
155
156    while let Some(ch) = chars.next() {
157        if escaped {
158            // Previous character was backslash, add this character literally
159            current.push(ch);
160            escaped = false;
161            continue;
162        }
163
164        match ch {
165            '\\' => {
166                // Escape next character
167                escaped = true;
168            }
169            '.' => {
170                // Segment separator
171                if !current.is_empty() {
172                    // Check if current segment is a number (for array index notation like "items.0")
173                    if let Ok(index) = current.parse::<usize>() {
174                        segments.push(PathSegment::Index(index));
175                    } else {
176                        segments.push(PathSegment::Key(current.clone()));
177                    }
178                    current.clear();
179                }
180            }
181            '[' => {
182                // Array index with bracket notation
183                if !current.is_empty() {
184                    segments.push(PathSegment::Key(current.clone()));
185                    current.clear();
186                }
187
188                // Parse the index until we hit ']'
189                let mut index_str = String::new();
190                while let Some(&next_ch) = chars.peek() {
191                    if next_ch == ']' {
192                        chars.next(); // consume the ']'
193                        break;
194                    }
195                    index_str.push(chars.next().unwrap());
196                }
197
198                // Parse the index
199                if let Ok(index) = index_str.parse::<usize>() {
200                    segments.push(PathSegment::Index(index));
201                }
202            }
203            _ => {
204                current.push(ch);
205            }
206        }
207    }
208
209    // Add the last segment
210    if !current.is_empty() {
211        if let Ok(index) = current.parse::<usize>() {
212            segments.push(PathSegment::Index(index));
213        } else {
214            segments.push(PathSegment::Key(current));
215        }
216    }
217
218    segments
219}
220
221/// Navigate through a YAML structure following path segments.
222///
223/// Handles both mapping keys and sequence indices.
224fn navigate_path(
225    mut current: crate::as_yaml::YamlNode,
226    segments: &[PathSegment],
227) -> Option<crate::as_yaml::YamlNode> {
228    for segment in segments {
229        match segment {
230            PathSegment::Key(key) => {
231                // Navigate through a mapping
232                let mapping = current.as_mapping()?;
233                current = mapping.get(key)?;
234            }
235            PathSegment::Index(index) => {
236                // Navigate through a sequence
237                let sequence = current.as_sequence()?;
238                current = sequence.get(*index)?;
239            }
240        }
241    }
242
243    Some(current)
244}
245
246// Implementation for Document
247impl YamlPath for crate::yaml::Document {
248    fn get_path(&self, path: &str) -> Option<crate::as_yaml::YamlNode> {
249        let segments = parse_path(path);
250        if segments.is_empty() {
251            return None;
252        }
253
254        // Start from the document's root content
255        let root = if let Some(m) = self.as_mapping() {
256            crate::as_yaml::YamlNode::Mapping(m)
257        } else if let Some(s) = self.as_sequence() {
258            crate::as_yaml::YamlNode::Sequence(s)
259        } else if let Some(sc) = self.as_scalar() {
260            crate::as_yaml::YamlNode::Scalar(sc)
261        } else {
262            return None;
263        };
264
265        // Navigate through the path segments
266        navigate_path(root, &segments)
267    }
268
269    fn set_path(&self, path: &str, value: impl crate::AsYaml) {
270        let segments = parse_path(path);
271        if segments.is_empty() {
272            return;
273        }
274
275        // Get the root mapping (can only set paths on mappings at the root)
276        let mapping = match self.as_mapping() {
277            Some(m) => m,
278            None => return,
279        };
280
281        set_path_impl(&mapping, &segments, value);
282    }
283
284    fn remove_path(&self, path: &str) -> bool {
285        let segments = parse_path(path);
286        if segments.is_empty() {
287            return false;
288        }
289
290        // Start from document root
291        let root = if let Some(m) = self.as_mapping() {
292            crate::as_yaml::YamlNode::Mapping(m)
293        } else if let Some(s) = self.as_sequence() {
294            crate::as_yaml::YamlNode::Sequence(s)
295        } else {
296            return false;
297        };
298
299        remove_path_impl(root, &segments)
300    }
301}
302
303/// Set a value at a path, creating intermediate mappings as needed.
304///
305/// This is used by Document::set_path() to handle the full path navigation.
306fn set_path_impl<V: crate::AsYaml>(mapping: &Mapping, segments: &[PathSegment], value: V) {
307    set_path_on_mapping(mapping, segments, value);
308}
309
310/// Remove a value at a nested path.
311///
312/// This is used by Document::remove_path() to handle the full path navigation.
313fn remove_path_impl(root: crate::as_yaml::YamlNode, segments: &[PathSegment]) -> bool {
314    if segments.is_empty() {
315        return false;
316    }
317
318    if segments.len() == 1 {
319        // Base case: remove from the current node
320        match &segments[0] {
321            PathSegment::Key(key) => {
322                if let Some(mapping) = root.as_mapping() {
323                    return mapping.remove(key.as_str()).is_some();
324                }
325            }
326            PathSegment::Index(_) => {
327                // Removing by index from a sequence is not supported
328                // (would require shifting all subsequent elements)
329                return false;
330            }
331        }
332        return false;
333    }
334
335    // Navigate to the parent and recurse
336    match &segments[0] {
337        PathSegment::Key(key) => {
338            if let Some(mapping) = root.as_mapping() {
339                if let Some(nested) = mapping.get(key.as_str()) {
340                    return remove_path_impl(nested, &segments[1..]);
341                }
342            }
343        }
344        PathSegment::Index(index) => {
345            if let Some(sequence) = root.as_sequence() {
346                if let Some(nested) = sequence.get(*index) {
347                    return remove_path_impl(nested, &segments[1..]);
348                }
349            }
350        }
351    }
352
353    false
354}
355
356// Implementation for Mapping
357impl YamlPath for Mapping {
358    fn get_path(&self, path: &str) -> Option<crate::as_yaml::YamlNode> {
359        let segments = parse_path(path);
360        if segments.is_empty() {
361            return None;
362        }
363
364        // Start from the first segment (must be a key for mappings)
365        let first_key = match &segments[0] {
366            PathSegment::Key(key) => key.as_str(),
367            PathSegment::Index(_) => return None, // Can't index into a mapping directly
368        };
369
370        if segments.len() == 1 {
371            return self.get(first_key);
372        }
373
374        // Get the value at the first key and navigate the rest
375        let current = self.get(first_key)?;
376        navigate_path(current, &segments[1..])
377    }
378
379    fn set_path(&self, path: &str, value: impl crate::AsYaml) {
380        let segments = parse_path(path);
381        if segments.is_empty() {
382            return;
383        }
384
385        set_path_on_mapping(self, &segments, value);
386    }
387
388    fn remove_path(&self, path: &str) -> bool {
389        let segments = parse_path(path);
390        if segments.is_empty() {
391            return false;
392        }
393
394        remove_path_from_mapping(self, &segments)
395    }
396}
397
398/// Set a value at a path on a mapping, creating intermediate mappings as needed.
399///
400/// This function uses only the public API (get_mapping, set) and does NOT rebuild nodes.
401fn set_path_on_mapping<V: crate::AsYaml>(mapping: &Mapping, segments: &[PathSegment], value: V) {
402    if segments.is_empty() {
403        return;
404    }
405
406    // First segment must be a key for mappings
407    let first_key = match &segments[0] {
408        PathSegment::Key(key) => key.as_str(),
409        PathSegment::Index(_) => return, // Can't set by index on a mapping
410    };
411
412    if segments.len() == 1 {
413        // Base case: set directly
414        mapping.set(first_key, value);
415        return;
416    }
417
418    // Try to navigate to existing nested mapping
419    if let Some(nested) = mapping.get_mapping(first_key) {
420        // Nested mapping exists, recurse
421        set_path_on_mapping(&nested, &segments[1..], value);
422    } else {
423        // Need to create intermediate structure
424        let empty_mapping = MappingBuilder::new()
425            .build_document()
426            .as_mapping()
427            .expect("MappingBuilder always produces a mapping");
428        mapping.set(first_key, &empty_mapping);
429
430        // Retrieve and recurse into the newly created mapping
431        if let Some(nested) = mapping.get_mapping(first_key) {
432            set_path_on_mapping(&nested, &segments[1..], value);
433        }
434    }
435}
436
437/// Remove a value at a path from a mapping.
438///
439/// This function uses only the public API and does NOT rebuild nodes.
440fn remove_path_from_mapping(mapping: &Mapping, segments: &[PathSegment]) -> bool {
441    if segments.is_empty() {
442        return false;
443    }
444
445    // First segment must be a key for mappings
446    let first_key = match &segments[0] {
447        PathSegment::Key(key) => key.as_str(),
448        PathSegment::Index(_) => return false, // Can't index into a mapping
449    };
450
451    if segments.len() == 1 {
452        // Base case: remove directly
453        return mapping.remove(first_key).is_some();
454    }
455
456    // Navigate to the parent mapping and recurse
457    if let Some(nested) = mapping.get_mapping(first_key) {
458        remove_path_from_mapping(&nested, &segments[1..])
459    } else {
460        false // Path doesn't exist
461    }
462}
463
464#[cfg(test)]
465mod tests {
466    use super::*;
467
468    #[test]
469    fn test_parse_path_basic() {
470        assert_eq!(parse_path(""), Vec::<PathSegment>::new());
471        assert_eq!(parse_path("key"), vec![PathSegment::Key("key".to_string())]);
472        assert_eq!(
473            parse_path("a.b"),
474            vec![
475                PathSegment::Key("a".to_string()),
476                PathSegment::Key("b".to_string())
477            ]
478        );
479        assert_eq!(
480            parse_path("a.b.c.d"),
481            vec![
482                PathSegment::Key("a".to_string()),
483                PathSegment::Key("b".to_string()),
484                PathSegment::Key("c".to_string()),
485                PathSegment::Key("d".to_string())
486            ]
487        );
488    }
489
490    #[test]
491    fn test_parse_path_with_array_indices() {
492        assert_eq!(
493            parse_path("items[0]"),
494            vec![PathSegment::Key("items".to_string()), PathSegment::Index(0)]
495        );
496        assert_eq!(
497            parse_path("items[0].name"),
498            vec![
499                PathSegment::Key("items".to_string()),
500                PathSegment::Index(0),
501                PathSegment::Key("name".to_string())
502            ]
503        );
504        assert_eq!(
505            parse_path("data.items[5].value"),
506            vec![
507                PathSegment::Key("data".to_string()),
508                PathSegment::Key("items".to_string()),
509                PathSegment::Index(5),
510                PathSegment::Key("value".to_string())
511            ]
512        );
513    }
514
515    #[test]
516    fn test_parse_path_with_numeric_indices() {
517        assert_eq!(
518            parse_path("items.0"),
519            vec![PathSegment::Key("items".to_string()), PathSegment::Index(0)]
520        );
521        assert_eq!(
522            parse_path("items.0.name"),
523            vec![
524                PathSegment::Key("items".to_string()),
525                PathSegment::Index(0),
526                PathSegment::Key("name".to_string())
527            ]
528        );
529    }
530
531    #[test]
532    fn test_parse_path_with_escaping() {
533        assert_eq!(
534            parse_path("key\\.with\\.dots"),
535            vec![PathSegment::Key("key.with.dots".to_string())]
536        );
537        assert_eq!(
538            parse_path("a.key\\.with\\.dots.b"),
539            vec![
540                PathSegment::Key("a".to_string()),
541                PathSegment::Key("key.with.dots".to_string()),
542                PathSegment::Key("b".to_string())
543            ]
544        );
545    }
546
547    #[test]
548    fn test_get_path_with_array_index() {
549        use crate::yaml::Document;
550        use std::str::FromStr;
551
552        let yaml = r#"
553items:
554  - name: first
555    value: 1
556  - name: second
557    value: 2
558"#;
559        let doc = Document::from_str(yaml).unwrap();
560
561        // Test bracket notation
562        let name = doc.get_path("items[0].name");
563        assert_eq!(
564            name.as_ref()
565                .and_then(|v| v.as_scalar())
566                .map(|s| s.as_string()),
567            Some("first".to_string())
568        );
569
570        let value = doc.get_path("items[1].value");
571        assert_eq!(
572            value
573                .as_ref()
574                .and_then(|v| v.as_scalar())
575                .map(|s| s.as_string()),
576            Some("2".to_string())
577        );
578    }
579
580    #[test]
581    fn test_get_path_with_numeric_index() {
582        use crate::yaml::Document;
583        use std::str::FromStr;
584
585        let yaml = r#"
586items:
587  - name: first
588    value: 1
589  - name: second
590    value: 2
591"#;
592        let doc = Document::from_str(yaml).unwrap();
593
594        // Test numeric dot notation
595        let name = doc.get_path("items.0.name");
596        assert_eq!(
597            name.as_ref()
598                .and_then(|v| v.as_scalar())
599                .map(|s| s.as_string()),
600            Some("first".to_string())
601        );
602
603        let value = doc.get_path("items.1.value");
604        assert_eq!(
605            value
606                .as_ref()
607                .and_then(|v| v.as_scalar())
608                .map(|s| s.as_string()),
609            Some("2".to_string())
610        );
611    }
612
613    #[test]
614    fn test_get_path_with_escaping() {
615        use crate::yaml::Document;
616
617        let doc = Document::new();
618        doc.set("key.with.dots", "test value");
619
620        // Without escaping - should not find it (looking for nested keys)
621        assert!(doc.get_path("key.with.dots").is_none());
622
623        // With escaping - should find it
624        let value = doc.get_path("key\\.with\\.dots");
625        assert_eq!(
626            value
627                .as_ref()
628                .and_then(|v| v.as_scalar())
629                .map(|s| s.as_string()),
630            Some("test value".to_string())
631        );
632    }
633
634    #[test]
635    fn test_get_path_array_only() {
636        use crate::yaml::Document;
637        use std::str::FromStr;
638
639        let yaml = r#"
640- first
641- second
642- third
643"#;
644        let doc = Document::from_str(yaml).unwrap();
645
646        // Get from root sequence
647        let item = doc.get_path("0");
648        assert_eq!(
649            item.as_ref()
650                .and_then(|v| v.as_scalar())
651                .map(|s| s.as_string()),
652            Some("first".to_string())
653        );
654
655        let item = doc.get_path("2");
656        assert_eq!(
657            item.as_ref()
658                .and_then(|v| v.as_scalar())
659                .map(|s| s.as_string()),
660            Some("third".to_string())
661        );
662    }
663
664    #[test]
665    fn test_remove_path_with_array_index() {
666        use crate::yaml::Document;
667        use std::str::FromStr;
668
669        let yaml = r#"
670items:
671  - name: first
672    nested:
673      key: value
674"#;
675        let doc = Document::from_str(yaml).unwrap();
676
677        // Remove nested key inside array element
678        assert!(doc.remove_path("items[0].nested.key"));
679        assert!(doc.get_path("items[0].nested.key").is_none());
680
681        // The nested mapping should still exist but be empty
682        assert!(doc.get_path("items[0].nested").is_some());
683    }
684
685    #[test]
686    fn test_mapping_get_path_with_indices() {
687        use crate::yaml::Document;
688        use std::str::FromStr;
689
690        let yaml = r#"
691config:
692  servers:
693    - host: server1.com
694      port: 8080
695    - host: server2.com
696      port: 9090
697"#;
698        let doc = Document::from_str(yaml).unwrap();
699        let mapping = doc.as_mapping().unwrap();
700
701        // Access through mapping using indices
702        let host = mapping.get_path("config.servers[0].host");
703        assert_eq!(
704            host.as_ref()
705                .and_then(|v| v.as_scalar())
706                .map(|s| s.as_string()),
707            Some("server1.com".to_string())
708        );
709
710        let port = mapping.get_path("config.servers.1.port");
711        assert_eq!(
712            port.as_ref()
713                .and_then(|v| v.as_scalar())
714                .map(|s| s.as_string()),
715            Some("9090".to_string())
716        );
717    }
718
719    #[test]
720    fn test_get_path_simple() {
721        use crate::yaml::Document;
722        use std::str::FromStr;
723
724        let yaml = Document::from_str("name: Alice\nage: 30\n").unwrap();
725
726        let name = yaml.get_path("name");
727        assert_eq!(
728            name.as_ref()
729                .and_then(|v| v.as_scalar())
730                .map(|s| s.to_string()),
731            Some("Alice".to_string())
732        );
733
734        let age = yaml.get_path("age");
735        assert_eq!(
736            age.as_ref()
737                .and_then(|v| v.as_scalar())
738                .map(|s| s.to_string()),
739            Some("30".to_string())
740        );
741    }
742
743    #[test]
744    fn test_get_path_nested() {
745        use crate::yaml::Document;
746        use std::str::FromStr;
747
748        let yaml = Document::from_str("server:\n  host: localhost\n  port: 8080\n").unwrap();
749
750        let host = yaml.get_path("server.host");
751        assert_eq!(
752            host.as_ref()
753                .and_then(|v| v.as_scalar())
754                .map(|s| s.to_string()),
755            Some("localhost".to_string())
756        );
757
758        let port = yaml.get_path("server.port");
759        assert_eq!(
760            port.as_ref()
761                .and_then(|v| v.as_scalar())
762                .map(|s| s.to_string()),
763            Some("8080".to_string())
764        );
765    }
766
767    #[test]
768    fn test_get_path_deeply_nested() {
769        use crate::yaml::Document;
770        use std::str::FromStr;
771
772        let yaml = Document::from_str(
773            "app:\n  database:\n    primary:\n      host: db.example.com\n      port: 5432\n",
774        )
775        .unwrap();
776
777        let host = yaml.get_path("app.database.primary.host");
778        assert_eq!(
779            host.as_ref()
780                .and_then(|v| v.as_scalar())
781                .map(|s| s.to_string()),
782            Some("db.example.com".to_string())
783        );
784
785        let port = yaml.get_path("app.database.primary.port");
786        assert_eq!(
787            port.as_ref()
788                .and_then(|v| v.as_scalar())
789                .map(|s| s.to_string()),
790            Some("5432".to_string())
791        );
792    }
793
794    #[test]
795    fn test_get_path_missing() {
796        use crate::yaml::Document;
797        use std::str::FromStr;
798
799        let yaml = Document::from_str("name: Alice\n").unwrap();
800
801        assert_eq!(yaml.get_path("missing"), None);
802        assert_eq!(yaml.get_path("name.nested"), None);
803        assert_eq!(yaml.get_path(""), None);
804    }
805
806    #[test]
807    fn test_set_path_existing_key() {
808        use crate::yaml::Document;
809        use std::str::FromStr;
810
811        let yaml = Document::from_str("name: Alice\nage: 30\n").unwrap();
812
813        yaml.set_path("name", "Bob");
814
815        assert_eq!(yaml.to_string(), "name: Bob\nage: 30\n");
816    }
817
818    #[test]
819    fn test_set_path_new_key() {
820        use crate::yaml::Document;
821        use std::str::FromStr;
822
823        let yaml = Document::from_str("name: Alice\n").unwrap();
824
825        yaml.set_path("age", 30);
826
827        assert_eq!(yaml.to_string(), "name: Alice\nage: 30\n");
828    }
829
830    #[test]
831    fn test_set_path_nested_existing() {
832        use crate::yaml::Document;
833        use std::str::FromStr;
834
835        let yaml = Document::from_str("server:\n  host: localhost\n  port: 8080\n").unwrap();
836
837        yaml.set_path("server.port", 9000);
838
839        assert_eq!(
840            yaml.to_string(),
841            "server:\n  host: localhost\n  port: 9000\n"
842        );
843    }
844
845    #[test]
846    fn test_set_path_nested_new() {
847        use crate::yaml::Document;
848        use std::str::FromStr;
849
850        let yaml = Document::from_str("server:\n  host: localhost\n").unwrap();
851
852        yaml.set_path("server.port", 8080);
853
854        assert_eq!(yaml.to_string(), "server:\n  host: localhost\nport: 8080\n");
855    }
856
857    #[test]
858    fn test_set_path_create_intermediate() {
859        use crate::yaml::Document;
860        use std::str::FromStr;
861
862        let yaml = Document::from_str("name: test\n").unwrap();
863
864        yaml.set_path("server.database.host", "localhost");
865
866        assert_eq!(
867            yaml.to_string(),
868            "name: test\nserver:\ndatabase:\nhost: localhost\n\n\n"
869        );
870
871        // Verify we can retrieve it
872        let host = yaml.get_path("server.database.host");
873        assert_eq!(
874            host.as_ref()
875                .and_then(|v| v.as_scalar())
876                .map(|s| s.to_string()),
877            Some("localhost".to_string())
878        );
879    }
880
881    #[test]
882    fn test_set_path_deeply_nested_create() {
883        use crate::yaml::Document;
884        use std::str::FromStr;
885
886        let yaml = Document::from_str("app: {}\n").unwrap();
887
888        yaml.set_path("app.database.primary.host", "db.example.com");
889        yaml.set_path("app.database.primary.port", 5432);
890
891        let host = yaml.get_path("app.database.primary.host");
892        assert_eq!(
893            host.as_ref()
894                .and_then(|v| v.as_scalar())
895                .map(|s| s.to_string()),
896            Some("db.example.com".to_string())
897        );
898
899        let port = yaml.get_path("app.database.primary.port");
900        assert_eq!(
901            port.as_ref()
902                .and_then(|v| v.as_scalar())
903                .map(|s| s.to_string()),
904            Some("5432".to_string())
905        );
906    }
907
908    #[test]
909    fn test_remove_path_simple() {
910        use crate::yaml::Document;
911        use std::str::FromStr;
912
913        let yaml = Document::from_str("name: Alice\nage: 30\n").unwrap();
914
915        let result = yaml.remove_path("age");
916        assert!(result);
917
918        assert_eq!(yaml.to_string(), "name: Alice");
919    }
920
921    #[test]
922    fn test_remove_path_nested() {
923        use crate::yaml::Document;
924        use std::str::FromStr;
925
926        let yaml = Document::from_str("server:\n  host: localhost\n  port: 8080\n").unwrap();
927
928        let result = yaml.remove_path("server.port");
929        assert!(result);
930
931        assert_eq!(yaml.to_string(), "server:\n  host: localhost  ");
932    }
933
934    #[test]
935    fn test_remove_path_missing() {
936        use crate::yaml::Document;
937        use std::str::FromStr;
938
939        let yaml = Document::from_str("name: Alice\n").unwrap();
940
941        let result = yaml.remove_path("missing");
942        assert!(!result);
943
944        let result = yaml.remove_path("name.nested");
945        assert!(!result);
946
947        // Document should be unchanged
948        assert_eq!(yaml.to_string(), "name: Alice\n");
949    }
950
951    #[test]
952    fn test_remove_path_deeply_nested() {
953        use crate::yaml::Document;
954        use std::str::FromStr;
955
956        let yaml = Document::from_str(
957            "app:\n  database:\n    primary:\n      host: db.example.com\n      port: 5432\n",
958        )
959        .unwrap();
960
961        let result = yaml.remove_path("app.database.primary.port");
962        assert!(result);
963
964        assert_eq!(
965            yaml.to_string(),
966            "app:\n  database:\n    primary:\n      host: db.example.com      "
967        );
968    }
969
970    #[test]
971    fn test_path_on_mapping_directly() {
972        use crate::yaml::Document;
973        use std::str::FromStr;
974
975        let yaml = Document::from_str("server:\n  host: localhost\n").unwrap();
976        let mapping = yaml.as_mapping().unwrap();
977
978        // Get from mapping
979        let host = mapping.get_path("server.host");
980        assert_eq!(
981            host.as_ref()
982                .and_then(|v| v.as_scalar())
983                .map(|s| s.to_string()),
984            Some("localhost".to_string())
985        );
986
987        // Set on mapping
988        mapping.set_path("server.port", 8080);
989        assert_eq!(yaml.to_string(), "server:\n  host: localhost\nport: 8080\n");
990
991        // Remove from mapping
992        let result = mapping.remove_path("server.port");
993        assert!(result);
994
995        // Try to remove non-existent path from mapping
996        let result_missing = mapping.remove_path("nonexistent.path");
997        assert!(!result_missing);
998    }
999
1000    #[test]
1001    fn test_set_path_preserves_formatting() {
1002        use crate::yaml::Document;
1003        use std::str::FromStr;
1004
1005        let yaml = Document::from_str("server:\n  host: localhost  # production server\n").unwrap();
1006
1007        yaml.set_path("server.host", "newhost");
1008
1009        assert_eq!(
1010            yaml.to_string(),
1011            "server:\n  host: newhost  # production server\n"
1012        );
1013    }
1014
1015    #[test]
1016    fn test_multiple_path_operations() {
1017        use crate::yaml::Document;
1018        use std::str::FromStr;
1019
1020        let yaml = Document::from_str("name: test\n").unwrap();
1021
1022        // Create nested structure
1023        yaml.set_path("server.host", "localhost");
1024        yaml.set_path("server.port", 8080);
1025        yaml.set_path("database.host", "db.local");
1026        yaml.set_path("database.port", 5432);
1027
1028        // Verify all values
1029        assert_eq!(
1030            yaml.get_path("server.host")
1031                .as_ref()
1032                .and_then(|v| v.as_scalar())
1033                .map(|s| s.to_string()),
1034            Some("localhost".to_string())
1035        );
1036        assert_eq!(
1037            yaml.get_path("server.port")
1038                .as_ref()
1039                .and_then(|v| v.as_scalar())
1040                .map(|s| s.to_string()),
1041            Some("8080".to_string())
1042        );
1043        assert_eq!(
1044            yaml.get_path("database.host")
1045                .as_ref()
1046                .and_then(|v| v.as_scalar())
1047                .map(|s| s.to_string()),
1048            Some("db.local".to_string())
1049        );
1050        assert_eq!(
1051            yaml.get_path("database.port")
1052                .as_ref()
1053                .and_then(|v| v.as_scalar())
1054                .map(|s| s.to_string()),
1055            Some("5432".to_string())
1056        );
1057
1058        // Remove some values
1059        yaml.remove_path("server.port");
1060        yaml.remove_path("database.host");
1061
1062        // Verify removals
1063        assert_eq!(yaml.get_path("server.port"), None);
1064        assert_eq!(yaml.get_path("database.host"), None);
1065
1066        // Verify remaining values still exist
1067        assert_eq!(
1068            yaml.get_path("server.host")
1069                .as_ref()
1070                .and_then(|v| v.as_scalar())
1071                .map(|s| s.to_string()),
1072            Some("localhost".to_string())
1073        );
1074        assert_eq!(
1075            yaml.get_path("database.port")
1076                .as_ref()
1077                .and_then(|v| v.as_scalar())
1078                .map(|s| s.to_string()),
1079            Some("5432".to_string())
1080        );
1081    }
1082}