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 {
260            let sc = self.as_scalar()?;
261            crate::as_yaml::YamlNode::Scalar(sc)
262        };
263
264        // Navigate through the path segments
265        navigate_path(root, &segments)
266    }
267
268    fn set_path(&self, path: &str, value: impl crate::AsYaml) {
269        let segments = parse_path(path);
270        if segments.is_empty() {
271            return;
272        }
273
274        // Get the root mapping (can only set paths on mappings at the root)
275        let mapping = match self.as_mapping() {
276            Some(m) => m,
277            None => return,
278        };
279
280        set_path_impl(&mapping, &segments, value);
281    }
282
283    fn remove_path(&self, path: &str) -> bool {
284        let segments = parse_path(path);
285        if segments.is_empty() {
286            return false;
287        }
288
289        // Start from document root
290        let root = if let Some(m) = self.as_mapping() {
291            crate::as_yaml::YamlNode::Mapping(m)
292        } else if let Some(s) = self.as_sequence() {
293            crate::as_yaml::YamlNode::Sequence(s)
294        } else {
295            return false;
296        };
297
298        remove_path_impl(root, &segments)
299    }
300}
301
302/// Set a value at a path, creating intermediate mappings as needed.
303///
304/// This is used by Document::set_path() to handle the full path navigation.
305fn set_path_impl<V: crate::AsYaml>(mapping: &Mapping, segments: &[PathSegment], value: V) {
306    set_path_on_mapping(mapping, segments, value);
307}
308
309/// Remove a value at a nested path.
310///
311/// This is used by Document::remove_path() to handle the full path navigation.
312fn remove_path_impl(root: crate::as_yaml::YamlNode, segments: &[PathSegment]) -> bool {
313    if segments.is_empty() {
314        return false;
315    }
316
317    if segments.len() == 1 {
318        // Base case: remove from the current node
319        match &segments[0] {
320            PathSegment::Key(key) => {
321                if let Some(mapping) = root.as_mapping() {
322                    return mapping.remove(key.as_str()).is_some();
323                }
324            }
325            PathSegment::Index(_) => {
326                // Removing by index from a sequence is not supported
327                // (would require shifting all subsequent elements)
328                return false;
329            }
330        }
331        return false;
332    }
333
334    // Navigate to the parent and recurse
335    match &segments[0] {
336        PathSegment::Key(key) => {
337            if let Some(mapping) = root.as_mapping() {
338                if let Some(nested) = mapping.get(key.as_str()) {
339                    return remove_path_impl(nested, &segments[1..]);
340                }
341            }
342        }
343        PathSegment::Index(index) => {
344            if let Some(sequence) = root.as_sequence() {
345                if let Some(nested) = sequence.get(*index) {
346                    return remove_path_impl(nested, &segments[1..]);
347                }
348            }
349        }
350    }
351
352    false
353}
354
355// Implementation for Mapping
356impl YamlPath for Mapping {
357    fn get_path(&self, path: &str) -> Option<crate::as_yaml::YamlNode> {
358        let segments = parse_path(path);
359        if segments.is_empty() {
360            return None;
361        }
362
363        // Start from the first segment (must be a key for mappings)
364        let first_key = match &segments[0] {
365            PathSegment::Key(key) => key.as_str(),
366            PathSegment::Index(_) => return None, // Can't index into a mapping directly
367        };
368
369        if segments.len() == 1 {
370            return self.get(first_key);
371        }
372
373        // Get the value at the first key and navigate the rest
374        let current = self.get(first_key)?;
375        navigate_path(current, &segments[1..])
376    }
377
378    fn set_path(&self, path: &str, value: impl crate::AsYaml) {
379        let segments = parse_path(path);
380        if segments.is_empty() {
381            return;
382        }
383
384        set_path_on_mapping(self, &segments, value);
385    }
386
387    fn remove_path(&self, path: &str) -> bool {
388        let segments = parse_path(path);
389        if segments.is_empty() {
390            return false;
391        }
392
393        remove_path_from_mapping(self, &segments)
394    }
395}
396
397/// Set a value at a path on a mapping, creating intermediate mappings as needed.
398///
399/// This function uses only the public API (get_mapping, set) and does NOT rebuild nodes.
400fn set_path_on_mapping<V: crate::AsYaml>(mapping: &Mapping, segments: &[PathSegment], value: V) {
401    if segments.is_empty() {
402        return;
403    }
404
405    // First segment must be a key for mappings
406    let first_key = match &segments[0] {
407        PathSegment::Key(key) => key.as_str(),
408        PathSegment::Index(_) => return, // Can't set by index on a mapping
409    };
410
411    if segments.len() == 1 {
412        // Base case: set directly
413        mapping.set(first_key, value);
414        return;
415    }
416
417    // Try to navigate to existing nested mapping
418    if let Some(nested) = mapping.get_mapping(first_key) {
419        // Nested mapping exists, recurse
420        set_path_on_mapping(&nested, &segments[1..], value);
421    } else {
422        // Need to create intermediate structure
423        let empty_mapping = MappingBuilder::new()
424            .build_document()
425            .as_mapping()
426            .expect("MappingBuilder always produces a mapping");
427        mapping.set(first_key, &empty_mapping);
428
429        // Retrieve and recurse into the newly created mapping
430        if let Some(nested) = mapping.get_mapping(first_key) {
431            set_path_on_mapping(&nested, &segments[1..], value);
432        }
433    }
434}
435
436/// Remove a value at a path from a mapping.
437///
438/// This function uses only the public API and does NOT rebuild nodes.
439fn remove_path_from_mapping(mapping: &Mapping, segments: &[PathSegment]) -> bool {
440    if segments.is_empty() {
441        return false;
442    }
443
444    // First segment must be a key for mappings
445    let first_key = match &segments[0] {
446        PathSegment::Key(key) => key.as_str(),
447        PathSegment::Index(_) => return false, // Can't index into a mapping
448    };
449
450    if segments.len() == 1 {
451        // Base case: remove directly
452        return mapping.remove(first_key).is_some();
453    }
454
455    // Navigate to the parent mapping and recurse
456    if let Some(nested) = mapping.get_mapping(first_key) {
457        remove_path_from_mapping(&nested, &segments[1..])
458    } else {
459        false // Path doesn't exist
460    }
461}
462
463#[cfg(test)]
464mod tests {
465    use super::*;
466
467    #[test]
468    fn test_parse_path_basic() {
469        assert_eq!(parse_path(""), Vec::<PathSegment>::new());
470        assert_eq!(parse_path("key"), vec![PathSegment::Key("key".to_string())]);
471        assert_eq!(
472            parse_path("a.b"),
473            vec![
474                PathSegment::Key("a".to_string()),
475                PathSegment::Key("b".to_string())
476            ]
477        );
478        assert_eq!(
479            parse_path("a.b.c.d"),
480            vec![
481                PathSegment::Key("a".to_string()),
482                PathSegment::Key("b".to_string()),
483                PathSegment::Key("c".to_string()),
484                PathSegment::Key("d".to_string())
485            ]
486        );
487    }
488
489    #[test]
490    fn test_parse_path_with_array_indices() {
491        assert_eq!(
492            parse_path("items[0]"),
493            vec![PathSegment::Key("items".to_string()), PathSegment::Index(0)]
494        );
495        assert_eq!(
496            parse_path("items[0].name"),
497            vec![
498                PathSegment::Key("items".to_string()),
499                PathSegment::Index(0),
500                PathSegment::Key("name".to_string())
501            ]
502        );
503        assert_eq!(
504            parse_path("data.items[5].value"),
505            vec![
506                PathSegment::Key("data".to_string()),
507                PathSegment::Key("items".to_string()),
508                PathSegment::Index(5),
509                PathSegment::Key("value".to_string())
510            ]
511        );
512    }
513
514    #[test]
515    fn test_parse_path_with_numeric_indices() {
516        assert_eq!(
517            parse_path("items.0"),
518            vec![PathSegment::Key("items".to_string()), PathSegment::Index(0)]
519        );
520        assert_eq!(
521            parse_path("items.0.name"),
522            vec![
523                PathSegment::Key("items".to_string()),
524                PathSegment::Index(0),
525                PathSegment::Key("name".to_string())
526            ]
527        );
528    }
529
530    #[test]
531    fn test_parse_path_with_escaping() {
532        assert_eq!(
533            parse_path("key\\.with\\.dots"),
534            vec![PathSegment::Key("key.with.dots".to_string())]
535        );
536        assert_eq!(
537            parse_path("a.key\\.with\\.dots.b"),
538            vec![
539                PathSegment::Key("a".to_string()),
540                PathSegment::Key("key.with.dots".to_string()),
541                PathSegment::Key("b".to_string())
542            ]
543        );
544    }
545
546    #[test]
547    fn test_get_path_with_array_index() {
548        use crate::yaml::Document;
549        use std::str::FromStr;
550
551        let yaml = r#"
552items:
553  - name: first
554    value: 1
555  - name: second
556    value: 2
557"#;
558        let doc = Document::from_str(yaml).unwrap();
559
560        // Test bracket notation
561        let name = doc.get_path("items[0].name");
562        assert_eq!(
563            name.as_ref()
564                .and_then(|v| v.as_scalar())
565                .map(|s| s.as_string()),
566            Some("first".to_string())
567        );
568
569        let value = doc.get_path("items[1].value");
570        assert_eq!(
571            value
572                .as_ref()
573                .and_then(|v| v.as_scalar())
574                .map(|s| s.as_string()),
575            Some("2".to_string())
576        );
577    }
578
579    #[test]
580    fn test_get_path_with_numeric_index() {
581        use crate::yaml::Document;
582        use std::str::FromStr;
583
584        let yaml = r#"
585items:
586  - name: first
587    value: 1
588  - name: second
589    value: 2
590"#;
591        let doc = Document::from_str(yaml).unwrap();
592
593        // Test numeric dot notation
594        let name = doc.get_path("items.0.name");
595        assert_eq!(
596            name.as_ref()
597                .and_then(|v| v.as_scalar())
598                .map(|s| s.as_string()),
599            Some("first".to_string())
600        );
601
602        let value = doc.get_path("items.1.value");
603        assert_eq!(
604            value
605                .as_ref()
606                .and_then(|v| v.as_scalar())
607                .map(|s| s.as_string()),
608            Some("2".to_string())
609        );
610    }
611
612    #[test]
613    fn test_get_path_with_escaping() {
614        use crate::yaml::Document;
615
616        let doc = Document::new();
617        doc.set("key.with.dots", "test value");
618
619        // Without escaping - should not find it (looking for nested keys)
620        assert!(doc.get_path("key.with.dots").is_none());
621
622        // With escaping - should find it
623        let value = doc.get_path("key\\.with\\.dots");
624        assert_eq!(
625            value
626                .as_ref()
627                .and_then(|v| v.as_scalar())
628                .map(|s| s.as_string()),
629            Some("test value".to_string())
630        );
631    }
632
633    #[test]
634    fn test_get_path_array_only() {
635        use crate::yaml::Document;
636        use std::str::FromStr;
637
638        let yaml = r#"
639- first
640- second
641- third
642"#;
643        let doc = Document::from_str(yaml).unwrap();
644
645        // Get from root sequence
646        let item = doc.get_path("0");
647        assert_eq!(
648            item.as_ref()
649                .and_then(|v| v.as_scalar())
650                .map(|s| s.as_string()),
651            Some("first".to_string())
652        );
653
654        let item = doc.get_path("2");
655        assert_eq!(
656            item.as_ref()
657                .and_then(|v| v.as_scalar())
658                .map(|s| s.as_string()),
659            Some("third".to_string())
660        );
661    }
662
663    #[test]
664    fn test_remove_path_with_array_index() {
665        use crate::yaml::Document;
666        use std::str::FromStr;
667
668        let yaml = r#"
669items:
670  - name: first
671    nested:
672      key: value
673"#;
674        let doc = Document::from_str(yaml).unwrap();
675
676        // Remove nested key inside array element
677        assert!(doc.remove_path("items[0].nested.key"));
678        assert!(doc.get_path("items[0].nested.key").is_none());
679
680        // The nested mapping should still exist but be empty
681        assert!(doc.get_path("items[0].nested").is_some());
682    }
683
684    #[test]
685    fn test_mapping_get_path_with_indices() {
686        use crate::yaml::Document;
687        use std::str::FromStr;
688
689        let yaml = r#"
690config:
691  servers:
692    - host: server1.com
693      port: 8080
694    - host: server2.com
695      port: 9090
696"#;
697        let doc = Document::from_str(yaml).unwrap();
698        let mapping = doc.as_mapping().unwrap();
699
700        // Access through mapping using indices
701        let host = mapping.get_path("config.servers[0].host");
702        assert_eq!(
703            host.as_ref()
704                .and_then(|v| v.as_scalar())
705                .map(|s| s.as_string()),
706            Some("server1.com".to_string())
707        );
708
709        let port = mapping.get_path("config.servers.1.port");
710        assert_eq!(
711            port.as_ref()
712                .and_then(|v| v.as_scalar())
713                .map(|s| s.as_string()),
714            Some("9090".to_string())
715        );
716    }
717
718    #[test]
719    fn test_get_path_simple() {
720        use crate::yaml::Document;
721        use std::str::FromStr;
722
723        let yaml = Document::from_str("name: Alice\nage: 30\n").unwrap();
724
725        let name = yaml.get_path("name");
726        assert_eq!(
727            name.as_ref()
728                .and_then(|v| v.as_scalar())
729                .map(|s| s.to_string()),
730            Some("Alice".to_string())
731        );
732
733        let age = yaml.get_path("age");
734        assert_eq!(
735            age.as_ref()
736                .and_then(|v| v.as_scalar())
737                .map(|s| s.to_string()),
738            Some("30".to_string())
739        );
740    }
741
742    #[test]
743    fn test_get_path_nested() {
744        use crate::yaml::Document;
745        use std::str::FromStr;
746
747        let yaml = Document::from_str("server:\n  host: localhost\n  port: 8080\n").unwrap();
748
749        let host = yaml.get_path("server.host");
750        assert_eq!(
751            host.as_ref()
752                .and_then(|v| v.as_scalar())
753                .map(|s| s.to_string()),
754            Some("localhost".to_string())
755        );
756
757        let port = yaml.get_path("server.port");
758        assert_eq!(
759            port.as_ref()
760                .and_then(|v| v.as_scalar())
761                .map(|s| s.to_string()),
762            Some("8080".to_string())
763        );
764    }
765
766    #[test]
767    fn test_get_path_deeply_nested() {
768        use crate::yaml::Document;
769        use std::str::FromStr;
770
771        let yaml = Document::from_str(
772            "app:\n  database:\n    primary:\n      host: db.example.com\n      port: 5432\n",
773        )
774        .unwrap();
775
776        let host = yaml.get_path("app.database.primary.host");
777        assert_eq!(
778            host.as_ref()
779                .and_then(|v| v.as_scalar())
780                .map(|s| s.to_string()),
781            Some("db.example.com".to_string())
782        );
783
784        let port = yaml.get_path("app.database.primary.port");
785        assert_eq!(
786            port.as_ref()
787                .and_then(|v| v.as_scalar())
788                .map(|s| s.to_string()),
789            Some("5432".to_string())
790        );
791    }
792
793    #[test]
794    fn test_get_path_missing() {
795        use crate::yaml::Document;
796        use std::str::FromStr;
797
798        let yaml = Document::from_str("name: Alice\n").unwrap();
799
800        assert_eq!(yaml.get_path("missing"), None);
801        assert_eq!(yaml.get_path("name.nested"), None);
802        assert_eq!(yaml.get_path(""), None);
803    }
804
805    #[test]
806    fn test_set_path_existing_key() {
807        use crate::yaml::Document;
808        use std::str::FromStr;
809
810        let yaml = Document::from_str("name: Alice\nage: 30\n").unwrap();
811
812        yaml.set_path("name", "Bob");
813
814        assert_eq!(yaml.to_string(), "name: Bob\nage: 30\n");
815    }
816
817    #[test]
818    fn test_set_path_new_key() {
819        use crate::yaml::Document;
820        use std::str::FromStr;
821
822        let yaml = Document::from_str("name: Alice\n").unwrap();
823
824        yaml.set_path("age", 30);
825
826        assert_eq!(yaml.to_string(), "name: Alice\nage: 30\n");
827    }
828
829    #[test]
830    fn test_set_path_nested_existing() {
831        use crate::yaml::Document;
832        use std::str::FromStr;
833
834        let yaml = Document::from_str("server:\n  host: localhost\n  port: 8080\n").unwrap();
835
836        yaml.set_path("server.port", 9000);
837
838        assert_eq!(
839            yaml.to_string(),
840            "server:\n  host: localhost\n  port: 9000\n"
841        );
842    }
843
844    #[test]
845    fn test_set_path_nested_new() {
846        use crate::yaml::Document;
847        use std::str::FromStr;
848
849        let yaml = Document::from_str("server:\n  host: localhost\n").unwrap();
850
851        yaml.set_path("server.port", 8080);
852
853        assert_eq!(
854            yaml.to_string(),
855            "server:\n  host: localhost\n  port: 8080\n"
856        );
857    }
858
859    #[test]
860    fn test_set_path_create_intermediate() {
861        use crate::yaml::Document;
862        use std::str::FromStr;
863
864        let yaml = Document::from_str("name: test\n").unwrap();
865
866        yaml.set_path("server.database.host", "localhost");
867
868        assert_eq!(
869            yaml.to_string(),
870            "name: test\nserver:\n  database:\n    host: localhost\n"
871        );
872
873        // Verify we can retrieve it
874        let host = yaml.get_path("server.database.host");
875        assert_eq!(
876            host.as_ref()
877                .and_then(|v| v.as_scalar())
878                .map(|s| s.to_string()),
879            Some("localhost".to_string())
880        );
881    }
882
883    #[test]
884    fn test_set_path_deeply_nested_create() {
885        use crate::yaml::Document;
886        use std::str::FromStr;
887
888        let yaml = Document::from_str("app: {}\n").unwrap();
889
890        yaml.set_path("app.database.primary.host", "db.example.com");
891        yaml.set_path("app.database.primary.port", 5432);
892
893        let host = yaml.get_path("app.database.primary.host");
894        assert_eq!(
895            host.as_ref()
896                .and_then(|v| v.as_scalar())
897                .map(|s| s.to_string()),
898            Some("db.example.com".to_string())
899        );
900
901        let port = yaml.get_path("app.database.primary.port");
902        assert_eq!(
903            port.as_ref()
904                .and_then(|v| v.as_scalar())
905                .map(|s| s.to_string()),
906            Some("5432".to_string())
907        );
908    }
909
910    #[test]
911    fn test_remove_path_simple() {
912        use crate::yaml::Document;
913        use std::str::FromStr;
914
915        let yaml = Document::from_str("name: Alice\nage: 30\n").unwrap();
916
917        let result = yaml.remove_path("age");
918        assert!(result);
919
920        assert_eq!(yaml.to_string(), "name: Alice");
921    }
922
923    #[test]
924    fn test_remove_path_nested() {
925        use crate::yaml::Document;
926        use std::str::FromStr;
927
928        let yaml = Document::from_str("server:\n  host: localhost\n  port: 8080\n").unwrap();
929
930        let result = yaml.remove_path("server.port");
931        assert!(result);
932
933        assert_eq!(yaml.to_string(), "server:\n  host: localhost  ");
934    }
935
936    #[test]
937    fn test_remove_path_missing() {
938        use crate::yaml::Document;
939        use std::str::FromStr;
940
941        let yaml = Document::from_str("name: Alice\n").unwrap();
942
943        let result = yaml.remove_path("missing");
944        assert!(!result);
945
946        let result = yaml.remove_path("name.nested");
947        assert!(!result);
948
949        // Document should be unchanged
950        assert_eq!(yaml.to_string(), "name: Alice\n");
951    }
952
953    #[test]
954    fn test_remove_path_deeply_nested() {
955        use crate::yaml::Document;
956        use std::str::FromStr;
957
958        let yaml = Document::from_str(
959            "app:\n  database:\n    primary:\n      host: db.example.com\n      port: 5432\n",
960        )
961        .unwrap();
962
963        let result = yaml.remove_path("app.database.primary.port");
964        assert!(result);
965
966        assert_eq!(
967            yaml.to_string(),
968            "app:\n  database:\n    primary:\n      host: db.example.com      "
969        );
970    }
971
972    #[test]
973    fn test_path_on_mapping_directly() {
974        use crate::yaml::Document;
975        use std::str::FromStr;
976
977        let yaml = Document::from_str("server:\n  host: localhost\n").unwrap();
978        let mapping = yaml.as_mapping().unwrap();
979
980        // Get from mapping
981        let host = mapping.get_path("server.host");
982        assert_eq!(
983            host.as_ref()
984                .and_then(|v| v.as_scalar())
985                .map(|s| s.to_string()),
986            Some("localhost".to_string())
987        );
988
989        // Set on mapping
990        mapping.set_path("server.port", 8080);
991        assert_eq!(
992            yaml.to_string(),
993            "server:\n  host: localhost\n  port: 8080\n"
994        );
995
996        // Remove from mapping
997        let result = mapping.remove_path("server.port");
998        assert!(result);
999
1000        // Try to remove non-existent path from mapping
1001        let result_missing = mapping.remove_path("nonexistent.path");
1002        assert!(!result_missing);
1003    }
1004
1005    #[test]
1006    fn test_mapping_remove_path_single_segment() {
1007        use crate::yaml::Document;
1008        use std::str::FromStr;
1009
1010        let yaml = Document::from_str("a: 1\nb: 2\n").unwrap();
1011        let mapping = yaml.as_mapping().unwrap();
1012
1013        // A single-segment path removes the key directly from the mapping.
1014        assert!(mapping.remove_path("a"));
1015        assert!(mapping.get_path("a").is_none());
1016        assert!(mapping.get_path("b").is_some());
1017
1018        // Removing a missing single-segment key returns false.
1019        assert!(!mapping.remove_path("missing"));
1020    }
1021
1022    #[test]
1023    fn test_set_path_preserves_formatting() {
1024        use crate::yaml::Document;
1025        use std::str::FromStr;
1026
1027        let yaml = Document::from_str("server:\n  host: localhost  # production server\n").unwrap();
1028
1029        yaml.set_path("server.host", "newhost");
1030
1031        assert_eq!(
1032            yaml.to_string(),
1033            "server:\n  host: newhost  # production server\n"
1034        );
1035    }
1036
1037    #[test]
1038    fn test_multiple_path_operations() {
1039        use crate::yaml::Document;
1040        use std::str::FromStr;
1041
1042        let yaml = Document::from_str("name: test\n").unwrap();
1043
1044        // Create nested structure
1045        yaml.set_path("server.host", "localhost");
1046        yaml.set_path("server.port", 8080);
1047        yaml.set_path("database.host", "db.local");
1048        yaml.set_path("database.port", 5432);
1049
1050        // Verify all values
1051        assert_eq!(
1052            yaml.get_path("server.host")
1053                .as_ref()
1054                .and_then(|v| v.as_scalar())
1055                .map(|s| s.to_string()),
1056            Some("localhost".to_string())
1057        );
1058        assert_eq!(
1059            yaml.get_path("server.port")
1060                .as_ref()
1061                .and_then(|v| v.as_scalar())
1062                .map(|s| s.to_string()),
1063            Some("8080".to_string())
1064        );
1065        assert_eq!(
1066            yaml.get_path("database.host")
1067                .as_ref()
1068                .and_then(|v| v.as_scalar())
1069                .map(|s| s.to_string()),
1070            Some("db.local".to_string())
1071        );
1072        assert_eq!(
1073            yaml.get_path("database.port")
1074                .as_ref()
1075                .and_then(|v| v.as_scalar())
1076                .map(|s| s.to_string()),
1077            Some("5432".to_string())
1078        );
1079
1080        // Remove some values
1081        yaml.remove_path("server.port");
1082        yaml.remove_path("database.host");
1083
1084        // Verify removals
1085        assert_eq!(yaml.get_path("server.port"), None);
1086        assert_eq!(yaml.get_path("database.host"), None);
1087
1088        // Verify remaining values still exist
1089        assert_eq!(
1090            yaml.get_path("server.host")
1091                .as_ref()
1092                .and_then(|v| v.as_scalar())
1093                .map(|s| s.to_string()),
1094            Some("localhost".to_string())
1095        );
1096        assert_eq!(
1097            yaml.get_path("database.port")
1098                .as_ref()
1099                .and_then(|v| v.as_scalar())
1100                .map(|s| s.to_string()),
1101            Some("5432".to_string())
1102        );
1103    }
1104}