Skip to main content

yaml_edit/nodes/
document.rs

1use super::{Lang, Mapping, Scalar, Sequence, SyntaxNode, TaggedNode};
2use crate::as_yaml::{AsYaml, YamlKind};
3use crate::error::YamlResult;
4use crate::lex::SyntaxKind;
5use crate::yaml::YamlFile;
6use rowan::ast::AstNode;
7use rowan::GreenNodeBuilder;
8use std::path::Path;
9
10ast_node!(Document, DOCUMENT, "A single YAML document");
11
12impl Document {
13    /// Create a new document
14    pub fn new() -> Document {
15        let mut builder = GreenNodeBuilder::new();
16        builder.start_node(SyntaxKind::DOCUMENT.into());
17        // Add the document start marker "---"
18        builder.token(SyntaxKind::DOC_START.into(), "---");
19        builder.token(SyntaxKind::WHITESPACE.into(), "\n");
20        builder.finish_node();
21        Document(SyntaxNode::new_root_mut(builder.finish()))
22    }
23
24    /// Create a new document with an empty mapping
25    pub fn new_mapping() -> Document {
26        let mut builder = GreenNodeBuilder::new();
27        builder.start_node(SyntaxKind::DOCUMENT.into());
28        // Don't add document start marker "---" for programmatically created documents
29        builder.start_node(SyntaxKind::MAPPING.into());
30        builder.finish_node(); // End MAPPING
31        builder.finish_node(); // End DOCUMENT
32        Document(SyntaxNode::new_root_mut(builder.finish()))
33    }
34
35    /// Load a document from a file
36    ///
37    /// Returns an error if the file contains multiple documents.
38    /// For multi-document YAML files, parse manually with `YamlFile::from_str()`.
39    ///
40    /// This follows the standard Rust naming convention of `from_file()`
41    /// to pair with `to_file()`.
42    pub fn from_file<P: AsRef<Path>>(path: P) -> YamlResult<Document> {
43        use std::str::FromStr;
44        let content = std::fs::read_to_string(path)?;
45        Self::from_str(&content)
46    }
47
48    /// Parse a YAML string, allowing syntax errors.
49    ///
50    /// Returns the first document even if there are parse errors, along with
51    /// a list of error messages. This allows for error-resilient tooling
52    /// that can work with partial or invalid input.
53    ///
54    /// Unlike [`Document::from_str`], this does not return an error if the
55    /// input contains multiple documents — only the first document is returned.
56    ///
57    /// # Example
58    /// ```
59    /// use yaml_edit::Document;
60    ///
61    /// let (doc, errors) = Document::from_str_relaxed("key: [unclosed");
62    /// // Tree is usable even with errors
63    /// assert!(!errors.is_empty());
64    /// assert!(doc.as_mapping().is_some());
65    /// ```
66    pub fn from_str_relaxed(s: &str) -> (Document, Vec<String>) {
67        let parsed = YamlFile::parse(s);
68        let errors = parsed.errors();
69        let first = parsed.tree().documents().next().unwrap_or_default();
70        (first, errors)
71    }
72
73    /// Load a document from a file, allowing syntax errors.
74    ///
75    /// Returns the first document even if there are parse errors, along with
76    /// a list of error messages. I/O errors are still returned as `Err`.
77    pub fn from_file_relaxed<P: AsRef<Path>>(
78        path: P,
79    ) -> Result<(Document, Vec<String>), std::io::Error> {
80        let content = std::fs::read_to_string(path)?;
81        Ok(Self::from_str_relaxed(&content))
82    }
83
84    /// Write the document to a file, creating directories as needed
85    ///
86    /// This follows the standard Rust naming convention of `to_file()`
87    /// to pair with `from_file()`.
88    pub fn to_file<P: AsRef<Path>>(&self, path: P) -> YamlResult<()> {
89        let path = path.as_ref();
90        if let Some(parent) = path.parent() {
91            std::fs::create_dir_all(parent)?;
92        }
93        let mut content = self.to_string();
94        // Ensure the file ends with a newline
95        if !content.ends_with('\n') {
96            content.push('\n');
97        }
98        std::fs::write(path, content)?;
99        Ok(())
100    }
101
102    /// Get the root node of this document (could be mapping, sequence, scalar, or alias)
103    pub(crate) fn root_node(&self) -> Option<SyntaxNode> {
104        self.0.children().find(|child| {
105            matches!(
106                child.kind(),
107                SyntaxKind::MAPPING
108                    | SyntaxKind::SEQUENCE
109                    | SyntaxKind::SCALAR
110                    | SyntaxKind::ALIAS
111                    | SyntaxKind::TAGGED_NODE
112            )
113        })
114    }
115
116    /// Get this document as a mapping, if it is one.
117    ///
118    /// Note: `Mapping` supports mutation even though this method takes `&self`.
119    /// Mutations are applied directly to the underlying syntax tree via rowan's
120    /// persistent data structures. All references to the tree will see the changes.
121    pub fn as_mapping(&self) -> Option<Mapping> {
122        self.root_node().and_then(Mapping::cast)
123    }
124
125    /// Get this document's root value as a sequence, or `None` if it isn't one.
126    pub fn as_sequence(&self) -> Option<Sequence> {
127        self.root_node().and_then(Sequence::cast)
128    }
129
130    /// Get this document's root value as a scalar, or `None` if it isn't one.
131    pub fn as_scalar(&self) -> Option<Scalar> {
132        self.root_node().and_then(Scalar::cast)
133    }
134
135    /// Returns `true` if this document is a mapping that contains the given key.
136    pub fn contains_key(&self, key: impl crate::AsYaml) -> bool {
137        self.as_mapping().is_some_and(|m| m.contains_key(key))
138    }
139
140    /// Get the value for a key from the document's root mapping.
141    ///
142    /// Returns `None` if the document is not a mapping or the key doesn't exist.
143    pub fn get(&self, key: impl crate::AsYaml) -> Option<crate::as_yaml::YamlNode> {
144        self.as_mapping().and_then(|m| m.get(key))
145    }
146
147    /// Get the raw syntax node for a key in the document's root mapping.
148    ///
149    /// Returns `None` if the document is not a mapping or the key doesn't exist.
150    /// Prefer [`get`](Self::get) for most use cases; this is for advanced CST access.
151    pub(crate) fn get_node(&self, key: impl crate::AsYaml) -> Option<SyntaxNode> {
152        self.as_mapping().and_then(|m| m.get_node(key))
153    }
154
155    /// Set a scalar value in the document (assumes document is a mapping)
156    pub fn set(&self, key: impl crate::AsYaml, value: impl crate::AsYaml) {
157        if let Some(mapping) = self.as_mapping() {
158            mapping.set(key, value);
159            // Changes are applied directly via splice_children, no need to replace
160        } else {
161            // If document is not a mapping, create one and add it to the document
162            let mapping = Mapping::new();
163            mapping.set(key, value);
164
165            // Add the mapping node directly to the document
166            let child_count = self.0.children_with_tokens().count();
167            self.0
168                .splice_children(child_count..child_count, vec![mapping.0.into()]);
169        }
170    }
171
172    /// Set a key-value pair with field ordering support.
173    ///
174    /// If the key exists, updates its value. If the key doesn't exist, inserts it
175    /// at the correct position based on the provided field order.
176    /// Fields not in the order list are placed at the end.
177    pub fn set_with_field_order<I, K>(
178        &self,
179        key: impl crate::AsYaml,
180        value: impl crate::AsYaml,
181        field_order: I,
182    ) where
183        I: IntoIterator<Item = K>,
184        K: crate::AsYaml,
185    {
186        // Collect so we can pass to both branches if needed.
187        let field_order: Vec<K> = field_order.into_iter().collect();
188        if let Some(mapping) = self.as_mapping() {
189            mapping.set_with_field_order(key, value, field_order);
190            // Changes are applied directly via splice_children, no need to replace
191        } else {
192            // If document is not a mapping, create one and splice it in.
193            let mapping = Mapping::new();
194            mapping.set_with_field_order(key, value, field_order);
195            let child_count = self.0.children_with_tokens().count();
196            self.0
197                .splice_children(child_count..child_count, vec![mapping.0.into()]);
198        }
199    }
200
201    /// Remove a key from the document (assumes document is a mapping).
202    ///
203    /// Returns `Some(entry)` if the key existed and was removed, or `None` if
204    /// the key was not found or the document is not a mapping. The returned
205    /// [`MappingEntry`](super::MappingEntry) is detached from the tree; callers can inspect its key
206    /// and value or re-insert it elsewhere.
207    pub fn remove(&self, key: impl crate::AsYaml) -> Option<super::MappingEntry> {
208        self.as_mapping()?.remove(key)
209    }
210
211    /// Get all key nodes from the document (assumes document is a mapping)
212    pub(crate) fn key_nodes(&self) -> impl Iterator<Item = SyntaxNode> + '_ {
213        self.as_mapping()
214            .into_iter()
215            .flat_map(|m| m.key_nodes().collect::<Vec<_>>())
216    }
217
218    /// Iterate over all keys in the document as [`YamlNode`](crate::as_yaml::YamlNode)s.
219    ///
220    /// Each key is a [`YamlNode`](crate::as_yaml::YamlNode) wrapping the
221    /// underlying CST node.  Assumes the document is a mapping; yields nothing
222    /// if it is not.
223    pub fn keys(&self) -> impl Iterator<Item = crate::as_yaml::YamlNode> + '_ {
224        self.key_nodes().filter_map(|key_node| {
225            key_node
226                .children()
227                .next()
228                .and_then(crate::as_yaml::YamlNode::from_syntax)
229        })
230    }
231
232    /// Check if the document is empty
233    pub fn is_empty(&self) -> bool {
234        self.as_mapping().map_or(true, |m| m.is_empty())
235    }
236
237    /// Create a document from a mapping
238    pub fn from_mapping(mapping: Mapping) -> Self {
239        // Create a document directly from the mapping syntax node to avoid recursion
240        let mut builder = GreenNodeBuilder::new();
241        builder.start_node(SyntaxKind::DOCUMENT.into());
242        // Add the document start marker "---"
243        builder.token(SyntaxKind::DOC_START.into(), "---");
244        builder.token(SyntaxKind::WHITESPACE.into(), "\n");
245
246        // Add the mapping with all its children, except trailing newline
247        builder.start_node(SyntaxKind::MAPPING.into());
248        let mapping_green = mapping.0.green();
249        let children: Vec<_> = mapping_green.children().collect();
250
251        // Check if last child is a newline token and skip it
252        let end_index = if let Some(rowan::NodeOrToken::Token(t)) = children.last() {
253            if t.kind() == SyntaxKind::NEWLINE.into() {
254                children.len() - 1
255            } else {
256                children.len()
257            }
258        } else {
259            children.len()
260        };
261
262        for child in &children[..end_index] {
263            match child {
264                rowan::NodeOrToken::Node(n) => {
265                    builder.start_node(n.kind());
266                    Self::add_green_node_children(&mut builder, n);
267                    builder.finish_node();
268                }
269                rowan::NodeOrToken::Token(t) => {
270                    builder.token(t.kind(), t.text());
271                }
272            }
273        }
274
275        // Don't add a trailing newline - entries already have their own newlines
276        builder.finish_node(); // End MAPPING
277        builder.finish_node(); // End DOCUMENT
278
279        Document(SyntaxNode::new_root_mut(builder.finish()))
280    }
281
282    /// Helper method to recursively add green node children to the builder
283    fn add_green_node_children(builder: &mut GreenNodeBuilder, node: &rowan::GreenNodeData) {
284        for child in node.children() {
285            match child {
286                rowan::NodeOrToken::Node(n) => {
287                    builder.start_node(n.kind());
288                    Self::add_green_node_children(builder, n);
289                    builder.finish_node();
290                }
291                rowan::NodeOrToken::Token(t) => {
292                    builder.token(t.kind(), t.text());
293                }
294            }
295        }
296    }
297
298    /// Insert a key-value pair immediately after `after_key` in this document's mapping.
299    ///
300    /// If `key` already exists, its value is updated in-place and it stays at its
301    /// current position (it is **not** moved). Returns `false` if `after_key` is not
302    /// found or the document is not a mapping.
303    ///
304    /// Use [`move_after`](Self::move_after) if you want an existing entry to be
305    /// removed from its current position and re-inserted after `after_key`.
306    pub fn insert_after(
307        &self,
308        after_key: impl crate::AsYaml,
309        key: impl crate::AsYaml,
310        value: impl crate::AsYaml,
311    ) -> bool {
312        if let Some(mapping) = self.as_mapping() {
313            mapping.insert_after(after_key, key, value)
314        } else {
315            false
316        }
317    }
318
319    /// Move a key-value pair to immediately after `after_key` in this document's mapping.
320    ///
321    /// If `key` already exists, it is **removed** from its current position and
322    /// re-inserted after `after_key` with the new value. Returns `false` if
323    /// `after_key` is not found or the document is not a mapping.
324    ///
325    /// Use [`insert_after`](Self::insert_after) if you want an existing entry to be
326    /// updated in-place rather than moved.
327    pub fn move_after(
328        &self,
329        after_key: impl crate::AsYaml,
330        key: impl crate::AsYaml,
331        value: impl crate::AsYaml,
332    ) -> bool {
333        if let Some(mapping) = self.as_mapping() {
334            mapping.move_after(after_key, key, value)
335        } else {
336            false
337        }
338    }
339
340    /// Insert a key-value pair immediately before `before_key` in this document's mapping.
341    ///
342    /// If `key` already exists, its value is updated in-place and it stays at its
343    /// current position (it is **not** moved). Returns `false` if `before_key` is not
344    /// found or the document is not a mapping.
345    ///
346    /// Use [`move_before`](Self::move_before) if you want an existing entry to be
347    /// removed from its current position and re-inserted before `before_key`.
348    pub fn insert_before(
349        &self,
350        before_key: impl crate::AsYaml,
351        key: impl crate::AsYaml,
352        value: impl crate::AsYaml,
353    ) -> bool {
354        if let Some(mapping) = self.as_mapping() {
355            mapping.insert_before(before_key, key, value)
356        } else {
357            false
358        }
359    }
360
361    /// Move a key-value pair to immediately before `before_key` in this document's mapping.
362    ///
363    /// If `key` already exists, it is **removed** from its current position and
364    /// re-inserted before `before_key` with the new value. Returns `false` if
365    /// `before_key` is not found or the document is not a mapping.
366    ///
367    /// Use [`insert_before`](Self::insert_before) if you want an existing entry to be
368    /// updated in-place rather than moved.
369    pub fn move_before(
370        &self,
371        before_key: impl crate::AsYaml,
372        key: impl crate::AsYaml,
373        value: impl crate::AsYaml,
374    ) -> bool {
375        if let Some(mapping) = self.as_mapping() {
376            mapping.move_before(before_key, key, value)
377        } else {
378            false
379        }
380    }
381
382    /// Helper to build a VALUE wrapper node around any AsYaml value
383    pub(crate) fn build_value_content(
384        builder: &mut GreenNodeBuilder,
385        value: impl crate::AsYaml,
386        indent: usize,
387    ) {
388        builder.start_node(SyntaxKind::VALUE.into());
389        value.build_content(builder, indent, false);
390        builder.finish_node(); // VALUE
391    }
392
393    /// Insert a key-value pair at a specific index (assumes document is a mapping).
394    ///
395    /// If the document already contains a mapping, delegates to
396    /// [`Mapping::insert_at_index`]. If no mapping exists yet, creates one and
397    /// uses [`Mapping::insert_at_index_preserving`].
398    pub fn insert_at_index(
399        &self,
400        index: usize,
401        key: impl crate::AsYaml,
402        value: impl crate::AsYaml,
403    ) {
404        // Delegate to Mapping::insert_at_index if we have a mapping
405        if let Some(mapping) = self.as_mapping() {
406            mapping.insert_at_index(index, key, value);
407            return;
408        }
409
410        // No mapping exists yet: create one and replace document contents
411        let mapping = Mapping::new();
412        mapping.insert_at_index_preserving(index, key, value);
413        let new_doc = Self::from_mapping(mapping);
414        let new_children: Vec<_> = new_doc.0.children_with_tokens().collect();
415        let child_count = self.0.children_with_tokens().count();
416        self.0.splice_children(0..child_count, new_children);
417    }
418
419    /// Get the scalar value for `key` as a decoded `String`.
420    ///
421    /// Returns `None` if the key does not exist or its value is not a scalar
422    /// (i.e. the value is a sequence or mapping). Quotes are stripped and
423    /// escape sequences are processed (e.g. `\"` → `"`, `\n` → newline).
424    /// For tagged scalars (e.g. `!!str foo`) the tag is ignored and the
425    /// value part is returned.
426    pub fn get_string(&self, key: impl crate::AsYaml) -> Option<String> {
427        // get_node() returns the content node (SCALAR/MAPPING/SEQUENCE/TAGGED_NODE),
428        // already unwrapped from the VALUE wrapper.
429        let content = self.get_node(key)?;
430        if let Some(tagged_node) = TaggedNode::cast(content.clone()) {
431            tagged_node.value().map(|s| s.as_string())
432        } else {
433            // Returns None if content is a sequence or mapping (not a scalar).
434            Scalar::cast(content).map(|s| s.as_string())
435        }
436    }
437
438    /// Get the number of top-level key-value pairs in this document.
439    ///
440    /// Returns `0` if the document is not a mapping or is empty.
441    pub fn len(&self) -> usize {
442        self.as_mapping().map(|m| m.len()).unwrap_or(0)
443    }
444
445    /// Get a nested mapping value for a key.
446    ///
447    /// Returns `None` if the key doesn't exist or its value is not a mapping.
448    pub fn get_mapping(&self, key: impl crate::AsYaml) -> Option<Mapping> {
449        self.get(key).and_then(|n| n.as_mapping().cloned())
450    }
451
452    /// Get a nested sequence value for a key.
453    ///
454    /// Returns `None` if the key doesn't exist or its value is not a sequence.
455    pub fn get_sequence(&self, key: impl crate::AsYaml) -> Option<Sequence> {
456        self.get(key).and_then(|n| n.as_sequence().cloned())
457    }
458
459    /// Rename a top-level key while preserving its value and formatting.
460    ///
461    /// The new key is automatically escaped/quoted as needed. Returns `true` if
462    /// the key was found and renamed, `false` if `old_key` does not exist.
463    pub fn rename_key(&self, old_key: impl crate::AsYaml, new_key: impl crate::AsYaml) -> bool {
464        self.as_mapping()
465            .map(|m| m.rename_key(old_key, new_key))
466            .unwrap_or(false)
467    }
468
469    /// Returns `true` if `key` exists and its value is a sequence.
470    pub fn is_sequence(&self, key: impl crate::AsYaml) -> bool {
471        self.get(key)
472            .map(|node| node.as_sequence().is_some())
473            .unwrap_or(false)
474    }
475
476    /// Reorder fields in the document's root mapping according to the specified order.
477    ///
478    /// Fields not in the order list will appear after the ordered fields, in their
479    /// original relative order. Has no effect if the document is not a mapping.
480    pub fn reorder_fields<I, K>(&self, order: I)
481    where
482        I: IntoIterator<Item = K>,
483        K: crate::AsYaml,
484    {
485        if let Some(mapping) = self.as_mapping() {
486            mapping.reorder_fields(order);
487        }
488    }
489
490    /// Validate this document against a YAML schema
491    ///
492    /// # Examples
493    ///
494    /// ```rust
495    /// use yaml_edit::{Document, Schema, SchemaValidator};
496    ///
497    /// let yaml = r#"
498    /// name: "John"
499    /// age: 30
500    /// active: true
501    /// "#;
502    ///
503    /// let parsed = yaml.parse::<yaml_edit::YamlFile>().unwrap();
504    /// let doc = parsed.document().unwrap();
505    ///
506    /// // Validate against JSON schema
507    /// let validator = SchemaValidator::json();
508    /// match doc.validate_schema(&validator) {
509    ///     Ok(_) => println!("Valid JSON schema"),
510    ///     Err(errors) => {
511    ///         for error in errors {
512    ///             println!("Validation error: {}", error);
513    ///         }
514    ///     }
515    /// }
516    /// ```
517    pub fn validate_schema(
518        &self,
519        validator: &crate::schema::SchemaValidator,
520    ) -> crate::schema::ValidationResult<()> {
521        validator.validate(self)
522    }
523
524    /// Get the byte offset range of this document in the source text.
525    ///
526    /// Returns the start and end byte offsets as a `TextPosition`.
527    ///
528    /// # Examples
529    ///
530    /// ```
531    /// use yaml_edit::Document;
532    /// use std::str::FromStr;
533    ///
534    /// let text = "name: Alice\nage: 30";
535    /// let doc = Document::from_str(text).unwrap();
536    /// let range = doc.byte_range();
537    /// assert_eq!(range.start, 0);
538    /// ```
539    pub fn byte_range(&self) -> crate::TextPosition {
540        self.0.text_range().into()
541    }
542
543    /// Get the line and column where this document starts.
544    ///
545    /// Requires the original source text to calculate line/column from byte offsets.
546    /// Line and column numbers are 1-indexed.
547    ///
548    /// # Arguments
549    ///
550    /// * `source_text` - The original YAML source text
551    ///
552    /// # Examples
553    ///
554    /// ```
555    /// use yaml_edit::Document;
556    /// use std::str::FromStr;
557    ///
558    /// let text = "name: Alice";
559    /// let doc = Document::from_str(text).unwrap();
560    /// let pos = doc.start_position(text);
561    /// assert_eq!(pos.line, 1);
562    /// assert_eq!(pos.column, 1);
563    /// ```
564    pub fn start_position(&self, source_text: &str) -> crate::LineColumn {
565        let range = self.byte_range();
566        crate::byte_offset_to_line_column(source_text, range.start as usize)
567    }
568
569    /// Get the line and column where this document ends.
570    ///
571    /// Requires the original source text to calculate line/column from byte offsets.
572    /// Line and column numbers are 1-indexed.
573    ///
574    /// # Arguments
575    ///
576    /// * `source_text` - The original YAML source text
577    pub fn end_position(&self, source_text: &str) -> crate::LineColumn {
578        let range = self.byte_range();
579        crate::byte_offset_to_line_column(source_text, range.end as usize)
580    }
581}
582
583impl Default for Document {
584    fn default() -> Self {
585        Self::new()
586    }
587}
588
589impl std::str::FromStr for Document {
590    type Err = crate::error::YamlError;
591
592    /// Parse a document from a YAML string.
593    ///
594    /// Returns an error if the string contains multiple documents.
595    /// For multi-document YAML, use `YamlFile::from_str()` instead.
596    ///
597    /// # Example
598    /// ```
599    /// use yaml_edit::Document;
600    /// use std::str::FromStr;
601    ///
602    /// let doc = Document::from_str("key: value").unwrap();
603    /// assert!(doc.as_mapping().is_some());
604    /// ```
605    fn from_str(s: &str) -> Result<Self, Self::Err> {
606        let parsed = YamlFile::parse(s);
607
608        if !parsed.positioned_errors().is_empty() {
609            let first_error = &parsed.positioned_errors()[0];
610            let lc = crate::byte_offset_to_line_column(s, first_error.range.start as usize);
611            return Err(crate::error::YamlError::Parse {
612                message: first_error.message.clone(),
613                line: Some(lc.line),
614                column: Some(lc.column),
615            });
616        }
617
618        let mut docs = parsed.tree().documents();
619        let first = docs.next().unwrap_or_default();
620
621        if docs.next().is_some() {
622            return Err(crate::error::YamlError::InvalidOperation {
623                operation: "Document::from_str".to_string(),
624                reason: "Input contains multiple YAML documents. Use YamlFile::from_str() for multi-document YAML.".to_string(),
625            });
626        }
627
628        Ok(first)
629    }
630}
631
632impl AsYaml for Document {
633    fn as_node(&self) -> Option<&SyntaxNode> {
634        Some(&self.0)
635    }
636
637    fn kind(&self) -> YamlKind {
638        YamlKind::Document
639    }
640
641    fn build_content(
642        &self,
643        builder: &mut rowan::GreenNodeBuilder,
644        _indent: usize,
645        _flow_context: bool,
646    ) -> bool {
647        crate::as_yaml::copy_node_content(builder, &self.0);
648        self.0
649            .last_token()
650            .map(|t| t.kind() == SyntaxKind::NEWLINE)
651            .unwrap_or(false)
652    }
653
654    fn is_inline(&self) -> bool {
655        // Documents are never inline
656        false
657    }
658}
659#[cfg(test)]
660mod tests {
661    use super::*;
662    use crate::builder::{MappingBuilder, SequenceBuilder};
663    use crate::yaml::YamlFile;
664    use std::str::FromStr;
665
666    #[test]
667    fn test_document_stream_features() {
668        // Test 1: Multi-document with end markers
669        let yaml1 = "---\ndoc1: first\n---\ndoc2: second\n...\n";
670        let parsed1 = YamlFile::from_str(yaml1).unwrap();
671        assert_eq!(parsed1.documents().count(), 2);
672        assert_eq!(parsed1.to_string(), yaml1);
673
674        // Test 2: Single document with explicit markers
675        let yaml2 = "---\nkey: value\n...\n";
676        let parsed2 = YamlFile::from_str(yaml2).unwrap();
677        assert_eq!(parsed2.documents().count(), 1);
678        assert_eq!(parsed2.to_string(), yaml2);
679
680        // Test 3: Document with only end marker
681        let yaml3 = "key: value\n...\n";
682        let parsed3 = YamlFile::from_str(yaml3).unwrap();
683        assert_eq!(parsed3.documents().count(), 1);
684        assert_eq!(parsed3.to_string(), yaml3);
685    }
686    #[test]
687    fn test_document_level_directives() {
688        // Test document-level directives with multi-document stream
689        let yaml = "%YAML 1.2\n%TAG ! tag:example.com,2000:app/\n---\nfirst: doc\n...\n%YAML 1.2\n---\nsecond: doc\n...\n";
690        let parsed = YamlFile::from_str(yaml).unwrap();
691        assert_eq!(parsed.documents().count(), 2);
692        assert_eq!(parsed.to_string(), yaml);
693    }
694    #[test]
695    fn test_document_schema_validation_api() {
696        // Test the new Document API methods for schema validation
697
698        // JSON-compatible document
699        let json_yaml = r#"
700name: "John"
701age: 30
702active: true
703items:
704  - "apple"
705  - 42
706  - true
707"#;
708        let doc = YamlFile::from_str(json_yaml).unwrap().document().unwrap();
709
710        // Test JSON schema validation - should pass
711        assert!(
712            crate::schema::SchemaValidator::json()
713                .validate(&doc)
714                .is_ok(),
715            "JSON-compatible document should pass JSON validation"
716        );
717
718        // Test Core schema validation - should pass
719        assert!(
720            crate::schema::SchemaValidator::core()
721                .validate(&doc)
722                .is_ok(),
723            "Valid document should pass Core validation"
724        );
725
726        // Test Failsafe schema validation - should fail due to numbers and booleans (in strict mode)
727        // Note: Non-strict failsafe might allow coercion, so test strict mode
728        let failsafe_strict = crate::schema::SchemaValidator::failsafe().strict();
729        assert!(
730            doc.validate_schema(&failsafe_strict).is_err(),
731            "Document with numbers and booleans should fail strict Failsafe validation"
732        );
733
734        // YAML-specific document
735        let yaml_specific = r#"
736name: "Test"
737created: 2023-12-25T10:30:45Z
738pattern: !!regex '\d+'
739data: !!binary "SGVsbG8="
740"#;
741        let yaml_doc = YamlFile::from_str(yaml_specific)
742            .unwrap()
743            .document()
744            .unwrap();
745
746        // Test Core schema - should pass
747        assert!(
748            crate::schema::SchemaValidator::core()
749                .validate(&yaml_doc)
750                .is_ok(),
751            "YAML-specific types should pass Core validation"
752        );
753
754        // Test JSON schema - should fail due to timestamp, regex, binary
755        assert!(
756            crate::schema::SchemaValidator::json()
757                .validate(&yaml_doc)
758                .is_err(),
759            "YAML-specific types should fail JSON validation"
760        );
761
762        // Test Failsafe schema - should fail
763        assert!(
764            crate::schema::SchemaValidator::failsafe()
765                .validate(&yaml_doc)
766                .is_err(),
767            "YAML-specific types should fail Failsafe validation"
768        );
769
770        // String-only document
771        let string_only = r#"
772name: hello
773message: world
774items:
775  - apple
776  - banana
777nested:
778  key: value
779"#;
780        let str_doc = YamlFile::from_str(string_only).unwrap().document().unwrap();
781
782        // All schemas should pass (strings are allowed in all schemas)
783        assert!(
784            crate::schema::SchemaValidator::failsafe()
785                .validate(&str_doc)
786                .is_ok(),
787            "String-only document should pass Failsafe validation"
788        );
789        assert!(
790            crate::schema::SchemaValidator::json()
791                .validate(&str_doc)
792                .is_ok(),
793            "String-only document should pass JSON validation"
794        );
795        assert!(
796            crate::schema::SchemaValidator::core()
797                .validate(&str_doc)
798                .is_ok(),
799            "String-only document should pass Core validation"
800        );
801    }
802    #[test]
803    fn test_document_set_preserves_position() {
804        // Test that Document::set() (not just Mapping::set()) preserves position
805        let yaml = r#"Name: original
806Version: 1.0
807Author: Someone
808"#;
809        let parsed = YamlFile::from_str(yaml).unwrap();
810        let doc = parsed.document().expect("Should have a document");
811
812        // Update Version - should stay in middle
813        doc.set("Version", 2.0);
814
815        let output = doc.to_string();
816        let expected = r#"Name: original
817Version: 2.0
818Author: Someone
819"#;
820        assert_eq!(output, expected);
821    }
822    #[test]
823    fn test_document_schema_coercion_api() {
824        // Test the coercion API
825        let coercion_yaml = r#"
826count: "42"
827enabled: "true"
828rate: "3.14"
829items:
830  - "100"
831  - "false"
832"#;
833        let doc = YamlFile::from_str(coercion_yaml)
834            .unwrap()
835            .document()
836            .unwrap();
837        let json_validator = crate::schema::SchemaValidator::json();
838
839        // Test coercion - should pass because strings can be coerced to numbers/booleans
840        assert!(
841            json_validator.can_coerce(&doc).is_ok(),
842            "Strings that look like numbers/booleans should be coercible to JSON types"
843        );
844
845        // Test with non-coercible types
846        let non_coercible = r#"
847timestamp: !!timestamp "2023-01-01"
848pattern: !!regex '\d+'
849"#;
850        let non_coer_doc = YamlFile::from_str(non_coercible)
851            .unwrap()
852            .document()
853            .unwrap();
854
855        // Should fail coercion to JSON schema
856        assert!(
857            json_validator.can_coerce(&non_coer_doc).is_err(),
858            "YAML-specific types should not be coercible to JSON schema"
859        );
860    }
861    #[test]
862    fn test_document_schema_validation_errors() {
863        // Test that error messages contain useful path information
864        let nested_yaml = r#"
865users:
866  - name: "Alice"
867    age: 25
868    metadata:
869      created: !!timestamp "2023-01-01"
870      active: true
871  - name: "Bob"
872    score: 95.5
873"#;
874        let doc = YamlFile::from_str(nested_yaml).unwrap().document().unwrap();
875
876        // Test Failsafe validation with detailed error checking (strict mode)
877        let failsafe_strict = crate::schema::SchemaValidator::failsafe().strict();
878        let failsafe_result = doc.validate_schema(&failsafe_strict);
879        assert!(
880            failsafe_result.is_err(),
881            "Nested document with numbers should fail strict Failsafe validation"
882        );
883
884        let errors = failsafe_result.unwrap_err();
885        assert!(!errors.is_empty());
886
887        // Check that all errors have path information
888        for error in &errors {
889            assert!(!error.path.is_empty(), "Error should have path: {}", error);
890        }
891
892        // Test JSON validation
893        let json_result = crate::schema::SchemaValidator::json().validate(&doc);
894        assert!(
895            json_result.is_err(),
896            "Document with timestamp should fail JSON validation"
897        );
898
899        let json_errors = json_result.unwrap_err();
900        assert!(!json_errors.is_empty());
901        // Verify at least one error is from json schema validation
902        assert!(
903            json_errors.iter().any(|e| e.schema_name == "json"),
904            "Should have JSON schema validation error"
905        );
906    }
907    #[test]
908    fn test_document_schema_validation_with_custom_validator() {
909        // Test using the general validate_schema method
910        let yaml = r#"
911name: "HelloWorld"
912count: 42
913active: true
914"#;
915        let doc = YamlFile::from_str(yaml).unwrap().document().unwrap();
916
917        // Create different validators and test
918        let json_validator = crate::schema::SchemaValidator::json();
919        let core_validator = crate::schema::SchemaValidator::core();
920
921        // Test with custom validators
922        assert!(
923            doc.validate_schema(&core_validator).is_ok(),
924            "Should pass Core validation"
925        );
926        assert!(
927            doc.validate_schema(&json_validator).is_ok(),
928            "Should pass JSON validation"
929        );
930        // Non-strict failsafe might allow coercion, so test strict mode
931        let failsafe_strict = crate::schema::SchemaValidator::failsafe().strict();
932        assert!(
933            doc.validate_schema(&failsafe_strict).is_err(),
934            "Should fail strict Failsafe validation"
935        );
936
937        // Test strict mode
938        let strict_json = crate::schema::SchemaValidator::json().strict();
939        // The document contains integers and booleans which are valid in JSON schema
940        assert!(
941            doc.validate_schema(&strict_json).is_ok(),
942            "Should pass strict JSON validation (integers and booleans are JSON-compatible)"
943        );
944
945        let strict_failsafe = crate::schema::SchemaValidator::failsafe().strict();
946        assert!(
947            doc.validate_schema(&strict_failsafe).is_err(),
948            "Should fail strict Failsafe validation"
949        );
950    }
951    #[test]
952    fn test_document_level_insertion_with_complex_types() {
953        // Test the Document-level API with complex types separately to avoid chaining issues
954
955        // Test Document.insert_after with sequence
956        let doc1 = Document::new();
957        doc1.set("name", "project");
958        let features = SequenceBuilder::new()
959            .item("auth")
960            .item("api")
961            .item("web")
962            .build_document()
963            .as_sequence()
964            .unwrap();
965        let success = doc1.insert_after("name", "features", features);
966        assert!(success);
967        let output1 = doc1.to_string();
968        assert_eq!(
969            output1,
970            "---\nname: project\nfeatures:\n  - auth\n  - api\n  - web\n"
971        );
972
973        // Test Document.insert_before with mapping
974        let doc2 = Document::new();
975        doc2.set("name", "project");
976        doc2.set("version", "1.0.0");
977        let database = MappingBuilder::new()
978            .pair("host", "localhost")
979            .pair("port", 5432)
980            .build_document()
981            .as_mapping()
982            .unwrap();
983        let success = doc2.insert_before("version", "database", database);
984        assert!(success);
985        let output2 = doc2.to_string();
986        assert_eq!(
987            output2,
988            "---\nname: project\ndatabase:\n  host: localhost\n  port: 5432\nversion: 1.0.0\n"
989        );
990
991        // Test Document.insert_at_index with set
992        let doc3 = Document::new();
993        doc3.set("name", "project");
994        // TODO: migrate away from YamlValue once !!set has a non-YamlValue AsYaml impl
995        #[allow(clippy::disallowed_types)]
996        let tag_set = {
997            use crate::value::YamlValue;
998            let mut tags = std::collections::BTreeSet::new();
999            tags.insert("production".to_string());
1000            tags.insert("database".to_string());
1001            YamlValue::from_set(tags)
1002        };
1003        doc3.insert_at_index(1, "tags", tag_set);
1004        let output3 = doc3.to_string();
1005        assert_eq!(
1006            output3,
1007            "---\nname: project\ntags: !!set\n  database: null\n  production: null\n"
1008        );
1009
1010        // Verify all are valid YAML by parsing
1011        assert!(
1012            YamlFile::from_str(&output1).is_ok(),
1013            "Sequence output should be valid YAML"
1014        );
1015        assert!(
1016            YamlFile::from_str(&output2).is_ok(),
1017            "Mapping output should be valid YAML"
1018        );
1019        assert!(
1020            YamlFile::from_str(&output3).is_ok(),
1021            "Set output should be valid YAML"
1022        );
1023    }
1024
1025    #[test]
1026    fn test_document_api_usage() -> crate::error::YamlResult<()> {
1027        // Create a new document
1028        let doc = Document::new();
1029
1030        // Check and modify fields
1031        assert!(!doc.contains_key("Repository"));
1032        doc.set("Repository", "https://github.com/user/repo.git");
1033        assert!(doc.contains_key("Repository"));
1034
1035        // Test get_string
1036        assert_eq!(
1037            doc.get_string("Repository"),
1038            Some("https://github.com/user/repo.git".to_string())
1039        );
1040
1041        // Test is_empty
1042        assert!(!doc.is_empty());
1043
1044        // Test keys
1045        let keys: Vec<_> = doc.keys().collect();
1046        assert_eq!(keys.len(), 1);
1047
1048        // Test remove
1049        assert!(doc.remove("Repository").is_some());
1050        assert!(!doc.contains_key("Repository"));
1051        assert!(doc.is_empty());
1052
1053        Ok(())
1054    }
1055
1056    #[test]
1057    fn test_field_ordering() {
1058        let doc = Document::new();
1059
1060        // Add fields in random order
1061        doc.set("Repository-Browse", "https://github.com/user/repo");
1062        doc.set("Name", "MyProject");
1063        doc.set("Bug-Database", "https://github.com/user/repo/issues");
1064        doc.set("Repository", "https://github.com/user/repo.git");
1065
1066        // Reorder fields
1067        doc.reorder_fields(["Name", "Bug-Database", "Repository", "Repository-Browse"]);
1068
1069        // Check that fields are in the expected order
1070        let keys: Vec<_> = doc.keys().collect();
1071        assert_eq!(keys.len(), 4);
1072        assert_eq!(
1073            keys[0].as_scalar().map(|s| s.as_string()),
1074            Some("Name".to_string())
1075        );
1076        assert_eq!(
1077            keys[1].as_scalar().map(|s| s.as_string()),
1078            Some("Bug-Database".to_string())
1079        );
1080        assert_eq!(
1081            keys[2].as_scalar().map(|s| s.as_string()),
1082            Some("Repository".to_string())
1083        );
1084        assert_eq!(
1085            keys[3].as_scalar().map(|s| s.as_string()),
1086            Some("Repository-Browse".to_string())
1087        );
1088    }
1089
1090    #[test]
1091    fn test_array_detection() {
1092        use crate::scalar::ScalarValue;
1093
1094        // Create a test with array values using set_value
1095        let doc = Document::new();
1096
1097        // Set an array value
1098        let array_value = SequenceBuilder::new()
1099            .item(ScalarValue::string("https://github.com/user/repo.git"))
1100            .item(ScalarValue::string("https://gitlab.com/user/repo.git"))
1101            .build_document()
1102            .as_sequence()
1103            .unwrap();
1104        doc.set("Repository", &array_value);
1105
1106        // Test array detection
1107        assert!(doc.is_sequence("Repository"));
1108        // The sequence is inserted but getting the first element may not work as expected
1109        // due to how the sequence is constructed. Let's just verify the sequence exists.
1110        assert!(doc.get_sequence("Repository").is_some());
1111    }
1112
1113    #[test]
1114    fn test_file_io() -> crate::error::YamlResult<()> {
1115        use std::fs;
1116
1117        // Create a test file path
1118        let test_path = "/tmp/test_yaml_edit.yaml";
1119
1120        // Create and save a document
1121        let doc = Document::new();
1122        doc.set("Name", "TestProject");
1123        doc.set("Repository", "https://example.com/repo.git");
1124
1125        doc.to_file(test_path)?;
1126
1127        // Load it back
1128        let loaded_doc = Document::from_file(test_path)?;
1129
1130        assert_eq!(
1131            loaded_doc.get_string("Name"),
1132            Some("TestProject".to_string())
1133        );
1134        assert_eq!(
1135            loaded_doc.get_string("Repository"),
1136            Some("https://example.com/repo.git".to_string())
1137        );
1138
1139        // Clean up
1140        let _ = fs::remove_file(test_path);
1141
1142        Ok(())
1143    }
1144
1145    #[test]
1146    fn test_document_from_str_single_document() {
1147        // Test parsing a single-document YAML
1148        let yaml = "key: value\nport: 8080";
1149        let doc = Document::from_str(yaml).unwrap();
1150
1151        assert_eq!(doc.get_string("key"), Some("value".to_string()));
1152        assert!(doc.contains_key("port"));
1153    }
1154
1155    #[test]
1156    fn test_document_from_str_multiple_documents_error() {
1157        // Test that multiple documents return an error
1158        let yaml = "---\nkey: value\n---\nother: data";
1159        let result = Document::from_str(yaml);
1160
1161        assert!(result.is_err());
1162        let err = result.unwrap_err();
1163        match err {
1164            crate::error::YamlError::InvalidOperation { operation, reason } => {
1165                assert_eq!(operation, "Document::from_str");
1166                assert_eq!(
1167                    reason,
1168                    "Input contains multiple YAML documents. Use YamlFile::from_str() for multi-document YAML."
1169                );
1170            }
1171            _ => panic!("Expected InvalidOperation error, got {:?}", err),
1172        }
1173    }
1174
1175    #[test]
1176    fn test_document_from_str_empty() {
1177        // Test parsing an empty document
1178        let yaml = "";
1179        let doc = Document::from_str(yaml).unwrap();
1180
1181        // Empty document should be valid
1182        assert!(doc.is_empty());
1183    }
1184
1185    #[test]
1186    fn test_document_from_str_bare_document() {
1187        // Test parsing without explicit document markers
1188        let yaml = "name: test\nversion: 1.0";
1189        let doc = Document::from_str(yaml).unwrap();
1190
1191        assert_eq!(doc.get_string("name"), Some("test".to_string()));
1192        assert_eq!(doc.get_string("version"), Some("1.0".to_string()));
1193    }
1194
1195    #[test]
1196    fn test_document_from_str_with_explicit_marker() {
1197        // Test parsing with explicit --- marker
1198        let yaml = "---\nkey: value";
1199        let doc = Document::from_str(yaml).unwrap();
1200
1201        assert_eq!(doc.get_string("key"), Some("value".to_string()));
1202    }
1203
1204    #[test]
1205    fn test_document_from_str_complex_structure() {
1206        // Test parsing complex nested structure
1207        let yaml = r#"
1208database:
1209  host: localhost
1210  port: 5432
1211  credentials:
1212    username: admin
1213    password: secret
1214features:
1215  - auth
1216  - logging
1217  - metrics
1218"#;
1219        let doc = Document::from_str(yaml).unwrap();
1220
1221        // Verify we can access the structure
1222        assert!(doc.contains_key("database"));
1223        assert!(doc.contains_key("features"));
1224
1225        // Get nested mapping
1226        let db = doc.get("database").unwrap();
1227        if let Some(db_map) = db.as_mapping() {
1228            assert!(db_map.contains_key("host"));
1229        } else {
1230            panic!("Expected mapping for database");
1231        }
1232    }
1233
1234    #[test]
1235    fn test_is_sequence_block() {
1236        let doc = Document::from_str("tags:\n  - alpha\n  - beta\n  - gamma").unwrap();
1237        assert!(doc.is_sequence("tags"));
1238        assert_eq!(
1239            doc.get_sequence("tags")
1240                .and_then(|s| s.get(0))
1241                .and_then(|v| v.as_scalar().map(|s| s.as_string())),
1242            Some("alpha".to_string())
1243        );
1244    }
1245
1246    #[test]
1247    fn test_is_sequence_flow() {
1248        let doc = Document::from_str("tags: [alpha, beta, gamma]").unwrap();
1249        assert!(doc.is_sequence("tags"));
1250        assert_eq!(
1251            doc.get_sequence("tags")
1252                .and_then(|s| s.get(0))
1253                .and_then(|v| v.as_scalar().map(|s| s.as_string())),
1254            Some("alpha".to_string())
1255        );
1256    }
1257
1258    #[test]
1259    fn test_sequence_first_element_flow_quoted_with_comma() {
1260        // Previously the text-splitting approach broke on commas inside quoted values
1261        let doc = Document::from_str("tags: [\"hello, world\", beta]").unwrap();
1262        assert_eq!(
1263            doc.get_sequence("tags")
1264                .and_then(|s| s.get(0))
1265                .and_then(|v| v.as_scalar().map(|s| s.as_string())),
1266            Some("hello, world".to_string())
1267        );
1268    }
1269
1270    #[test]
1271    fn test_is_sequence_missing_key() {
1272        let doc = Document::from_str("name: test").unwrap();
1273        assert!(!doc.is_sequence("tags"));
1274    }
1275
1276    #[test]
1277    fn test_is_sequence_not_sequence() {
1278        let doc = Document::from_str("name: test").unwrap();
1279        assert!(!doc.is_sequence("name"));
1280    }
1281
1282    #[test]
1283    fn test_is_sequence_empty_sequence() {
1284        let doc = Document::from_str("tags: []").unwrap();
1285        assert!(doc.is_sequence("tags"));
1286        assert_eq!(doc.get_sequence("tags").map(|s| s.len()), Some(0));
1287    }
1288
1289    #[test]
1290    fn test_get_string_plain_scalar() {
1291        let doc = Document::from_str("key: hello").unwrap();
1292        assert_eq!(doc.get_string("key"), Some("hello".to_string()));
1293    }
1294
1295    #[test]
1296    fn test_get_string_double_quoted_with_escapes() {
1297        let doc = Document::from_str(r#"key: "hello \"world\"""#).unwrap();
1298        assert_eq!(doc.get_string("key"), Some(r#"hello "world""#.to_string()));
1299    }
1300
1301    #[test]
1302    fn test_get_string_single_quoted() {
1303        let doc = Document::from_str("key: 'it''s fine'").unwrap();
1304        assert_eq!(doc.get_string("key"), Some("it's fine".to_string()));
1305    }
1306
1307    #[test]
1308    fn test_get_string_missing_key() {
1309        let doc = Document::from_str("other: value").unwrap();
1310        assert_eq!(doc.get_string("key"), None);
1311    }
1312
1313    #[test]
1314    fn test_get_string_sequence_value_returns_none() {
1315        let doc = Document::from_str("key:\n  - a\n  - b").unwrap();
1316        assert_eq!(doc.get_string("key"), None);
1317    }
1318
1319    #[test]
1320    fn test_get_string_mapping_value_returns_none() {
1321        let doc = Document::from_str("key:\n  nested: value").unwrap();
1322        assert_eq!(doc.get_string("key"), None);
1323    }
1324
1325    #[test]
1326    fn test_insert_after_preserves_newline() {
1327        // Test with the examples from the bug report
1328        let yaml = "---\nBug-Database: https://github.com/example/example/issues\nBug-Submit: https://github.com/example/example/issues/new\n";
1329        let yaml_obj = YamlFile::from_str(yaml).unwrap();
1330
1331        // For now, test using Document directly since YamlFile::insert_after was removed
1332        if let Some(doc) = yaml_obj.document() {
1333            let result = doc.insert_after(
1334                "Bug-Submit",
1335                "Repository",
1336                "https://github.com/example/example.git",
1337            );
1338            assert!(result, "insert_after should return true when key is found");
1339
1340            // Check the document output directly
1341            let output = doc.to_string();
1342
1343            let expected = "---
1344Bug-Database: https://github.com/example/example/issues
1345Bug-Submit: https://github.com/example/example/issues/new
1346Repository: https://github.com/example/example.git
1347";
1348            assert_eq!(output, expected);
1349        }
1350    }
1351
1352    #[test]
1353    fn test_insert_after_without_trailing_newline() {
1354        // Test the specific bug case - YAML without trailing newline
1355        let yaml = "---\nBug-Database: https://github.com/example/example/issues\nBug-Submit: https://github.com/example/example/issues/new";
1356        let yaml_obj = YamlFile::from_str(yaml).unwrap();
1357
1358        if let Some(doc) = yaml_obj.document() {
1359            let result = doc.insert_after(
1360                "Bug-Submit",
1361                "Repository",
1362                "https://github.com/example/example.git",
1363            );
1364            assert!(result, "insert_after should return true when key is found");
1365
1366            let output = doc.to_string();
1367
1368            let expected = "---
1369Bug-Database: https://github.com/example/example/issues
1370Bug-Submit: https://github.com/example/example/issues/new
1371Repository: https://github.com/example/example.git
1372";
1373            assert_eq!(output, expected);
1374        }
1375    }
1376
1377    #[test]
1378    fn test_from_str_relaxed_valid() {
1379        let (doc, errors) = Document::from_str_relaxed("key: value\n");
1380        assert!(errors.is_empty());
1381        let mapping = doc.as_mapping().unwrap();
1382        assert_eq!(
1383            mapping.get("key").unwrap().as_scalar().unwrap().to_string(),
1384            "value"
1385        );
1386    }
1387
1388    #[test]
1389    fn test_from_str_relaxed_with_errors() {
1390        let (doc, errors) = Document::from_str_relaxed("key: [unclosed");
1391        assert!(!errors.is_empty());
1392        // Tree is still usable
1393        assert!(doc.as_mapping().is_some());
1394    }
1395
1396    #[test]
1397    fn test_from_str_relaxed_multi_document() {
1398        // Unlike from_str, from_str_relaxed does not error on multiple documents
1399        let (doc, errors) = Document::from_str_relaxed("a: 1\n---\nb: 2\n");
1400        assert!(errors.is_empty());
1401        let mapping = doc.as_mapping().unwrap();
1402        assert_eq!(
1403            mapping.get("a").unwrap().as_scalar().unwrap().to_string(),
1404            "1"
1405        );
1406    }
1407
1408    #[test]
1409    fn test_from_str_relaxed_empty_input() {
1410        let (doc, errors) = Document::from_str_relaxed("");
1411        assert!(errors.is_empty());
1412        // Default document when nothing is parsed
1413        assert!(doc.as_mapping().is_none());
1414    }
1415}