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