Skip to main content

yaml_edit/
as_yaml.rs

1//! AsYaml trait and YamlNode for unified access to YAML values.
2//!
3//! ## Type hierarchy
4//!
5//! This library has two tiers:
6//!
7//! - **CST types** ([`Document`](crate::yaml::Document), [`Mapping`](crate::yaml::Mapping),
8//!   [`Sequence`](crate::nodes::sequence::Sequence), [`Scalar`](crate::yaml::Scalar),
9//!   [`TaggedNode`](crate::yaml::TaggedNode)) — format-preserving wrappers around the
10//!   concrete syntax tree.  Parse a file to get these; navigate and mutate them in place.
11//!
12//! - **Input types** (`&str`, `i64`, `bool`, [`MappingBuilder`](crate::builder::MappingBuilder), …)
13//!   — supply these to mutation methods such as
14//!   [`Mapping::set`](crate::yaml::Mapping::set).  They implement [`AsYaml`] but are
15//!   never returned from navigation methods.
16//!
17//! [`YamlNode`] is the type-erased return type for navigation: you get one back from
18//! [`Mapping::get`](crate::yaml::Mapping::get), iterator methods like
19//! [`Mapping::keys`](crate::yaml::Mapping::keys), etc.  It is always backed by a real
20//! CST node; match on it to get the concrete type.
21
22use crate::yaml::{Alias, Mapping, Scalar, Sequence, SyntaxNode, TaggedNode};
23use rowan::ast::AstNode;
24use std::borrow::Cow;
25use std::fmt;
26
27/// The kind of YAML value.
28#[derive(Debug, Clone, PartialEq, Eq)]
29pub enum YamlKind {
30    /// A mapping (object/dictionary)
31    Mapping,
32    /// A sequence (array/list)
33    Sequence,
34    /// A scalar value (string, number, boolean, null)
35    Scalar,
36    /// An alias reference (e.g. `*anchor_name`)
37    Alias,
38    /// A document (top-level container)
39    Document,
40    /// A tagged value (e.g. `!!set`, `!!omap`, `!!pairs`).
41    ///
42    /// Known built-in tags use a `'static` string; custom tags carry an owned string.
43    Tagged(Cow<'static, str>),
44}
45
46/// Trait for types that can be represented as YAML content.
47///
48/// Bridges the gap between CST nodes (which preserve formatting) and raw Rust
49/// values (which are convenient for constructing new content).
50///
51/// # Using `AsYaml` as a bound
52///
53/// Mutation methods such as [`Mapping::set`](crate::yaml::Mapping::set) accept
54/// `impl AsYaml`, so you can pass any of:
55///
56/// - A string literal (`"hello"`)
57/// - A number (`42_i64`, `3.14_f64`, …)
58/// - A boolean (`true`)
59/// - An existing CST node ([`Mapping`], [`Sequence`], [`Scalar`], [`YamlNode`], …)
60/// - A builder ([`MappingBuilder`](crate::builder::MappingBuilder), [`SequenceBuilder`](crate::builder::SequenceBuilder))
61pub trait AsYaml {
62    /// Returns a reference to the underlying `SyntaxNode` if one exists.
63    ///
64    /// CST wrappers (`Mapping`, `Sequence`, `Scalar`, `TaggedNode`, `YamlNode`)
65    /// return `Some`.  Raw Rust types (`i64`, `String`, etc.) return `None`.
66    fn as_node(&self) -> Option<&SyntaxNode>;
67
68    /// Returns the kind of YAML value this represents.
69    fn kind(&self) -> YamlKind;
70
71    /// Serialize this value into a `GreenNodeBuilder`.
72    ///
73    /// CST-backed types copy their node content; raw types synthesize an
74    /// appropriate CST structure.
75    ///
76    /// Returns `true` if the emitted content ends with a `NEWLINE` token
77    /// (used to avoid double newlines when nesting collections).
78    fn build_content(
79        &self,
80        builder: &mut rowan::GreenNodeBuilder,
81        indent: usize,
82        flow_context: bool,
83    ) -> bool;
84
85    /// Returns whether this value should be rendered on the same line as its key.
86    ///
87    /// `true` for scalars and empty collections; `false` for non-empty block
88    /// collections.
89    fn is_inline(&self) -> bool;
90}
91
92/// Compare two [`AsYaml`] values for semantic equality.
93///
94/// Semantic equality ignores formatting: `name`, `"name"`, and `'name'` all
95/// compare equal.  Both sides can be any combination of CST nodes and raw
96/// Rust values.
97pub fn yaml_eq<A, B>(a: &A, b: &B) -> bool
98where
99    A: AsYaml + ?Sized,
100    B: AsYaml + ?Sized,
101{
102    // If the left side has a backing node, dispatch on its concrete kind.
103    if let Some(node) = a.as_node() {
104        use crate::lex::SyntaxKind;
105        return match node.kind() {
106            SyntaxKind::SCALAR => Scalar::cast(node.clone()).is_some_and(|s| scalar_eq_rhs(&s, b)),
107            SyntaxKind::MAPPING => {
108                Mapping::cast(node.clone()).is_some_and(|m| mapping_eq_rhs(&m, b))
109            }
110            SyntaxKind::SEQUENCE => {
111                Sequence::cast(node.clone()).is_some_and(|s| sequence_eq_rhs(&s, b))
112            }
113            SyntaxKind::TAGGED_NODE => {
114                TaggedNode::cast(node.clone()).is_some_and(|t| tagged_eq_rhs(&t, b))
115            }
116            _ => false,
117        };
118    }
119
120    // Left side is raw — try the right side's node instead (symmetric).
121    if let Some(node) = b.as_node() {
122        use crate::lex::SyntaxKind;
123        return match node.kind() {
124            SyntaxKind::SCALAR => Scalar::cast(node.clone()).is_some_and(|s| scalar_eq_rhs(&s, a)),
125            SyntaxKind::MAPPING => {
126                Mapping::cast(node.clone()).is_some_and(|m| mapping_eq_rhs(&m, a))
127            }
128            SyntaxKind::SEQUENCE => {
129                Sequence::cast(node.clone()).is_some_and(|s| sequence_eq_rhs(&s, a))
130            }
131            SyntaxKind::TAGGED_NODE => {
132                TaggedNode::cast(node.clone()).is_some_and(|t| tagged_eq_rhs(&t, a))
133            }
134            _ => false,
135        };
136    }
137
138    // Both sides are raw — compare by kind, then decoded scalar string.
139    if a.kind() != b.kind() {
140        return false;
141    }
142    match (raw_scalar_str(a), raw_scalar_str(b)) {
143        (Some(sa), Some(sb)) => sa == sb,
144        _ => false,
145    }
146}
147
148/// Extract the decoded scalar string from a raw (no-node) `AsYaml` value.
149fn raw_scalar_str<T: AsYaml + ?Sized>(v: &T) -> Option<String> {
150    if v.as_node().is_some() || v.kind() != YamlKind::Scalar {
151        return None;
152    }
153    let mut builder = rowan::GreenNodeBuilder::new();
154    v.build_content(&mut builder, 0, false);
155    let green = builder.finish();
156    let node = rowan::SyntaxNode::<crate::yaml::Lang>::new_root(green);
157    Scalar::cast(node.clone())
158        .map(|s| s.as_string())
159        .or_else(|| Some(node.text().to_string()))
160}
161
162/// Get the semantic type and normalized value of a scalar for YAML-level comparison.
163///
164/// Returns (type_kind, normalized_value) where normalized values are comparable:
165/// - Different integer formats (123, 0x7B, 0o173) normalize to same i64
166/// - Different null representations (null, ~, Null) all become "null"
167/// - Different boolean cases (true, True, TRUE) normalize to lowercase
168fn scalar_semantic_value(scalar: &Scalar) -> Option<(crate::lex::SyntaxKind, String)> {
169    use crate::lex::SyntaxKind;
170    use crate::scalar::ScalarValue;
171
172    // Get the first token to determine the lexical type
173    let token = scalar.0.first_token()?;
174    let kind = token.kind();
175    let text = token.text();
176
177    let normalized = match kind {
178        SyntaxKind::INT => {
179            // Normalize all integer representations to their numeric value
180            ScalarValue::parse_integer(text)
181                .map(|v| v.to_string())
182                .unwrap_or_else(|| text.to_string())
183        }
184        SyntaxKind::FLOAT => {
185            // Normalize float representations
186            text.parse::<f64>()
187                .map(|v| v.to_string())
188                .unwrap_or_else(|_| text.to_string())
189        }
190        SyntaxKind::BOOL => {
191            // Normalize booleans to lowercase
192            text.to_lowercase()
193        }
194        SyntaxKind::NULL => {
195            // All null representations become "null"
196            "null".to_string()
197        }
198        SyntaxKind::STRING => {
199            // For strings, use the unescaped/unquoted value
200            scalar.as_string()
201        }
202        _ => {
203            // Block scalars, MERGE_KEY, etc. - use as_string()
204            scalar.as_string()
205        }
206    };
207
208    Some((kind, normalized))
209}
210
211fn scalar_eq_rhs<B: AsYaml + ?Sized>(lhs: &Scalar, rhs: &B) -> bool {
212    // Get or build the RHS scalar node
213    let rhs_scalar = if let Some(node) = rhs.as_node() {
214        let Some(scalar) = Scalar::cast(node.clone()) else {
215            return false;
216        };
217        scalar
218    } else {
219        // RHS is a raw AsYaml value (e.g., &str, &i64) - build it into a scalar
220        if rhs.kind() != YamlKind::Scalar {
221            return false;
222        }
223        let mut builder = rowan::GreenNodeBuilder::new();
224        rhs.build_content(&mut builder, 0, false);
225        let green = builder.finish();
226        let node = rowan::SyntaxNode::<crate::yaml::Lang>::new_root(green);
227        let Some(scalar) = Scalar::cast(node) else {
228            return false;
229        };
230        scalar
231    };
232
233    // Get semantic values for YAML-level comparison
234    let Some((lhs_kind, lhs_value)) = scalar_semantic_value(lhs) else {
235        return false;
236    };
237    let Some((rhs_kind, rhs_value)) = scalar_semantic_value(&rhs_scalar) else {
238        return false;
239    };
240
241    // For YAML semantic equality:
242    // - Types must match (STRING != INT even if text looks the same)
243    // - Normalized values must match (0x7B == 123, true == True)
244    lhs_kind == rhs_kind && lhs_value == rhs_value
245}
246
247fn mapping_eq_rhs<B: AsYaml + ?Sized>(lhs: &Mapping, rhs: &B) -> bool {
248    let Some(node) = rhs.as_node() else {
249        return false;
250    };
251    let Some(r) = Mapping::cast(node.clone()) else {
252        return false;
253    };
254    let lhs_pairs: Vec<_> = lhs.pairs().collect();
255    let rhs_pairs: Vec<_> = r.pairs().collect();
256    if lhs_pairs.len() != rhs_pairs.len() {
257        return false;
258    }
259    // pairs() yields raw KEY/VALUE wrapper nodes — peel them before comparing.
260    lhs_pairs
261        .iter()
262        .zip(rhs_pairs.iter())
263        .all(|((lk, lv), (rk, rv))| {
264            let Some(lk) = YamlNode::from_syntax_peeled(lk.clone()) else {
265                return false;
266            };
267            let Some(rk) = YamlNode::from_syntax_peeled(rk.clone()) else {
268                return false;
269            };
270            let Some(lv) = YamlNode::from_syntax_peeled(lv.clone()) else {
271                return false;
272            };
273            let Some(rv) = YamlNode::from_syntax_peeled(rv.clone()) else {
274                return false;
275            };
276            yaml_eq(&lk, &rk) && yaml_eq(&lv, &rv)
277        })
278}
279
280fn sequence_eq_rhs<B: AsYaml + ?Sized>(lhs: &Sequence, rhs: &B) -> bool {
281    let Some(node) = rhs.as_node() else {
282        return false;
283    };
284    let Some(r) = Sequence::cast(node.clone()) else {
285        return false;
286    };
287    let lhs_items: Vec<_> = lhs.items().collect();
288    let rhs_items: Vec<_> = r.items().collect();
289    if lhs_items.len() != rhs_items.len() {
290        return false;
291    }
292    // items() already yields peeled content nodes.
293    lhs_items.iter().zip(rhs_items.iter()).all(|(l, r)| {
294        match (
295            YamlNode::from_syntax(l.clone()),
296            YamlNode::from_syntax(r.clone()),
297        ) {
298            (Some(l), Some(r)) => yaml_eq(&l, &r),
299            _ => false,
300        }
301    })
302}
303
304fn tagged_eq_rhs<B: AsYaml + ?Sized>(lhs: &TaggedNode, rhs: &B) -> bool {
305    let Some(node) = rhs.as_node() else {
306        return false;
307    };
308    TaggedNode::cast(node.clone())
309        .is_some_and(|r| lhs.tag() == r.tag() && lhs.as_string() == r.as_string())
310}
311
312/// A type-erased handle to a CST node returned from navigation methods.
313///
314/// You get a `YamlNode` back from methods like
315/// [`Mapping::get`](crate::yaml::Mapping::get),
316/// [`Sequence::get`](crate::nodes::sequence::Sequence::get),
317/// [`Mapping::keys`](crate::yaml::Mapping::keys), etc.
318///
319/// Match on the variants to get the concrete type, or use the helper
320/// methods [`as_scalar`](Self::as_scalar), [`as_mapping`](Self::as_mapping), etc.
321///
322/// ```
323/// use yaml_edit::{Document, YamlNode};
324/// use std::str::FromStr;
325///
326/// let doc = Document::from_str("name: Alice\nage: 30").unwrap();
327///
328/// if let Some(node) = doc.get("name") {
329///     match node {
330///         YamlNode::Scalar(s) => println!("scalar: {}", s.as_string()),
331///         YamlNode::Mapping(m) => println!("mapping with {} keys", m.len()),
332///         YamlNode::Sequence(s) => println!("sequence with {} items", s.len()),
333///         YamlNode::Alias(a) => println!("alias: *{}", a.name()),
334///         YamlNode::TaggedNode(t) => println!("tagged: {:?}", t.tag()),
335///     }
336/// }
337/// ```
338#[derive(Debug, Clone, PartialEq)]
339pub enum YamlNode {
340    /// A scalar value (string, integer, float, boolean, null).
341    Scalar(Scalar),
342    /// A key-value mapping.
343    Mapping(Mapping),
344    /// An ordered sequence.
345    Sequence(Sequence),
346    /// An alias reference (e.g. `*anchor_name`).
347    Alias(Alias),
348    /// A tagged node (e.g. `!!set`, `!!omap`, `!!pairs`, or a custom tag).
349    TaggedNode(TaggedNode),
350}
351
352impl YamlNode {
353    /// Cast a `SyntaxNode` to a `YamlNode`.
354    ///
355    /// Returns `None` if the node's kind is not one of `SCALAR`, `MAPPING`,
356    /// `SEQUENCE`, `ALIAS`, or `TAGGED_NODE`.
357    pub fn from_syntax(node: SyntaxNode) -> Option<Self> {
358        use crate::lex::SyntaxKind;
359        match node.kind() {
360            SyntaxKind::SCALAR => Scalar::cast(node).map(YamlNode::Scalar),
361            SyntaxKind::MAPPING => Mapping::cast(node).map(YamlNode::Mapping),
362            SyntaxKind::SEQUENCE => Sequence::cast(node).map(YamlNode::Sequence),
363            SyntaxKind::ALIAS => Alias::cast(node).map(YamlNode::Alias),
364            SyntaxKind::TAGGED_NODE => TaggedNode::cast(node).map(YamlNode::TaggedNode),
365            _ => None,
366        }
367    }
368
369    /// Cast a `SyntaxNode` to a `YamlNode`, peeling any `KEY` or `VALUE`
370    /// wrapper first.
371    ///
372    /// Used internally where `mapping.pairs()` yields raw KEY/VALUE wrapper
373    /// nodes that need to be unwrapped before semantic comparison.
374    pub(crate) fn from_syntax_peeled(node: SyntaxNode) -> Option<Self> {
375        use crate::lex::SyntaxKind;
376        let inner = if matches!(node.kind(), SyntaxKind::KEY | SyntaxKind::VALUE) {
377            node.children().next()?
378        } else {
379            node
380        };
381        Self::from_syntax(inner)
382    }
383
384    /// Returns the kind of YAML value this node represents.
385    pub fn kind(&self) -> YamlKind {
386        match self {
387            YamlNode::Scalar(_) => YamlKind::Scalar,
388            YamlNode::Mapping(_) => YamlKind::Mapping,
389            YamlNode::Sequence(_) => YamlKind::Sequence,
390            YamlNode::Alias(_) => YamlKind::Alias,
391            YamlNode::TaggedNode(t) => t
392                .tag()
393                .map(|tag| YamlKind::Tagged(Cow::Owned(tag)))
394                .unwrap_or(YamlKind::Scalar),
395        }
396    }
397
398    /// Returns the underlying `SyntaxNode`.
399    pub(crate) fn syntax(&self) -> &SyntaxNode {
400        match self {
401            YamlNode::Scalar(s) => s.syntax(),
402            YamlNode::Mapping(m) => m.syntax(),
403            YamlNode::Sequence(s) => s.syntax(),
404            YamlNode::Alias(a) => a.syntax(),
405            YamlNode::TaggedNode(t) => t.syntax(),
406        }
407    }
408
409    /// Compare semantically with another value (ignores quoting/formatting).
410    pub fn yaml_eq<O: AsYaml>(&self, other: &O) -> bool {
411        yaml_eq(self, other)
412    }
413
414    /// If this node is a scalar, return a reference to it.
415    pub fn as_scalar(&self) -> Option<&Scalar> {
416        if let YamlNode::Scalar(s) = self {
417            Some(s)
418        } else {
419            None
420        }
421    }
422
423    /// If this node is a mapping, return a reference to it.
424    pub fn as_mapping(&self) -> Option<&Mapping> {
425        if let YamlNode::Mapping(m) = self {
426            Some(m)
427        } else {
428            None
429        }
430    }
431
432    /// If this node is a sequence, return a reference to it.
433    pub fn as_sequence(&self) -> Option<&Sequence> {
434        if let YamlNode::Sequence(s) = self {
435            Some(s)
436        } else {
437            None
438        }
439    }
440
441    /// If this node is a tagged node, return a reference to it.
442    pub fn as_tagged(&self) -> Option<&TaggedNode> {
443        if let YamlNode::TaggedNode(t) = self {
444            Some(t)
445        } else {
446            None
447        }
448    }
449
450    /// If this node is an alias, return a reference to it.
451    pub fn as_alias(&self) -> Option<&Alias> {
452        if let YamlNode::Alias(a) = self {
453            Some(a)
454        } else {
455            None
456        }
457    }
458
459    /// Returns `true` if this node is a scalar.
460    pub fn is_scalar(&self) -> bool {
461        matches!(self, YamlNode::Scalar(_))
462    }
463
464    /// Returns `true` if this node is a mapping.
465    pub fn is_mapping(&self) -> bool {
466        matches!(self, YamlNode::Mapping(_))
467    }
468
469    /// Returns `true` if this node is a sequence.
470    pub fn is_sequence(&self) -> bool {
471        matches!(self, YamlNode::Sequence(_))
472    }
473
474    /// Returns `true` if this node is a tagged node.
475    pub fn is_tagged(&self) -> bool {
476        matches!(self, YamlNode::TaggedNode(_))
477    }
478
479    /// Returns `true` if this node is an alias.
480    pub fn is_alias(&self) -> bool {
481        matches!(self, YamlNode::Alias(_))
482    }
483
484    /// If this node is a scalar, try to parse it as an `i64`.
485    ///
486    /// Returns `None` if this is not a scalar or cannot be parsed as an integer.
487    pub fn to_i64(&self) -> Option<i64> {
488        crate::scalar::ScalarValue::from_scalar(self.as_scalar()?).to_i64()
489    }
490
491    /// If this node is a scalar, try to parse it as an `f64`.
492    ///
493    /// Returns `None` if this is not a scalar or cannot be parsed as a float.
494    pub fn to_f64(&self) -> Option<f64> {
495        crate::scalar::ScalarValue::from_scalar(self.as_scalar()?).to_f64()
496    }
497
498    /// If this node is a scalar, try to parse it as a `bool`.
499    ///
500    /// Returns `None` if this is not a scalar or cannot be parsed as a boolean.
501    pub fn to_bool(&self) -> Option<bool> {
502        crate::scalar::ScalarValue::from_scalar(self.as_scalar()?).to_bool()
503    }
504
505    /// Get a value by key from a mapping node.
506    ///
507    /// Returns `None` if this node is not a mapping or the key is not found.
508    pub fn get(&self, key: impl crate::AsYaml) -> Option<YamlNode> {
509        self.as_mapping()?
510            .get_node(key)
511            .and_then(YamlNode::from_syntax)
512    }
513
514    /// Get a value by index from a sequence node.
515    ///
516    /// Returns `None` if this node is not a sequence or the index is out of bounds.
517    pub fn get_item(&self, index: usize) -> Option<YamlNode> {
518        self.as_sequence()?
519            .items()
520            .nth(index)
521            .and_then(YamlNode::from_syntax)
522    }
523}
524
525impl AsYaml for YamlNode {
526    fn as_node(&self) -> Option<&SyntaxNode> {
527        Some(self.syntax())
528    }
529
530    fn kind(&self) -> YamlKind {
531        YamlNode::kind(self)
532    }
533
534    fn build_content(
535        &self,
536        builder: &mut rowan::GreenNodeBuilder,
537        _indent: usize,
538        _flow_context: bool,
539    ) -> bool {
540        let node = self.syntax();
541        copy_node_content(builder, node);
542        node.last_token()
543            .map(|t| t.kind() == crate::lex::SyntaxKind::NEWLINE)
544            .unwrap_or(false)
545    }
546
547    fn is_inline(&self) -> bool {
548        use crate::yaml::ValueNode;
549        match self {
550            YamlNode::Scalar(_) => true,
551            YamlNode::Mapping(m) => ValueNode::is_inline(m),
552            YamlNode::Sequence(s) => ValueNode::is_inline(s),
553            YamlNode::Alias(_) => true,
554            YamlNode::TaggedNode(_) => true,
555        }
556    }
557}
558
559/// `yaml_node == "some_string"` — compares the node's semantic value.
560impl PartialEq<str> for YamlNode {
561    fn eq(&self, other: &str) -> bool {
562        yaml_eq(self, &other)
563    }
564}
565
566impl PartialEq<&str> for YamlNode {
567    fn eq(&self, other: &&str) -> bool {
568        yaml_eq(self, other)
569    }
570}
571
572impl PartialEq<String> for YamlNode {
573    fn eq(&self, other: &String) -> bool {
574        yaml_eq(self, &other.as_str())
575    }
576}
577
578impl fmt::Display for YamlNode {
579    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
580        write!(f, "{}", self.syntax().text())
581    }
582}
583
584/// Blanket impl: any reference to an `AsYaml` type also implements `AsYaml`.
585///
586/// The `?Sized` bound makes this work for trait objects (`&dyn AsYaml`),
587/// which lets [`MappingView`](crate::MappingView) — whose methods take
588/// `&dyn AsYaml` — forward keys to inherent methods that take
589/// `impl AsYaml`.
590impl<T: AsYaml + ?Sized> AsYaml for &T {
591    fn as_node(&self) -> Option<&SyntaxNode> {
592        (*self).as_node()
593    }
594
595    fn kind(&self) -> YamlKind {
596        (*self).kind()
597    }
598
599    fn build_content(
600        &self,
601        builder: &mut rowan::GreenNodeBuilder,
602        indent: usize,
603        flow_context: bool,
604    ) -> bool {
605        (*self).build_content(builder, indent, flow_context)
606    }
607
608    fn is_inline(&self) -> bool {
609        (*self).is_inline()
610    }
611}
612
613/// Recursively copy the children of `node` into `builder`.
614pub(crate) fn copy_node_content(builder: &mut rowan::GreenNodeBuilder, node: &SyntaxNode) {
615    for child in node.children_with_tokens() {
616        match child {
617            rowan::NodeOrToken::Node(n) => {
618                builder.start_node(n.kind().into());
619                copy_node_content(builder, &n);
620                builder.finish_node();
621            }
622            rowan::NodeOrToken::Token(t) => {
623                builder.token(t.kind().into(), t.text());
624            }
625        }
626    }
627}
628
629/// Recursively copy the children of `node` into `builder`, adjusting indentation.
630///
631/// When copying WHITESPACE or INDENT tokens that appear after a NEWLINE,
632/// replaces them with the specified `indent` amount. This is useful when
633/// inserting pre-built sequences/mappings into a different nesting context.
634pub(crate) fn copy_node_content_with_indent(
635    builder: &mut rowan::GreenNodeBuilder,
636    node: &SyntaxNode,
637    indent: usize,
638) {
639    use crate::lex::SyntaxKind;
640    let mut after_newline = false;
641
642    for child in node.children_with_tokens() {
643        match child {
644            rowan::NodeOrToken::Node(n) => {
645                // If this node follows a newline, add indent before starting the node
646                if after_newline && indent > 0 {
647                    builder.token(SyntaxKind::WHITESPACE.into(), &" ".repeat(indent));
648                }
649                builder.start_node(n.kind().into());
650                copy_node_content_with_indent(builder, &n, indent);
651                builder.finish_node();
652                after_newline = false;
653            }
654            rowan::NodeOrToken::Token(t) => {
655                match t.kind() {
656                    SyntaxKind::NEWLINE => {
657                        builder.token(t.kind().into(), t.text());
658                        after_newline = true;
659                    }
660                    SyntaxKind::WHITESPACE | SyntaxKind::INDENT => {
661                        // If this whitespace follows a newline, add our indent to the existing indent
662                        if after_newline && indent > 0 {
663                            let existing_indent = t.text().len();
664                            let total_indent = indent + existing_indent;
665                            builder.token(SyntaxKind::WHITESPACE.into(), &" ".repeat(total_indent));
666                        } else if after_newline {
667                            // indent == 0, skip the whitespace entirely
668                        } else {
669                            // Not after newline, keep as-is (e.g., space after dash)
670                            builder.token(t.kind().into(), t.text());
671                        }
672                        after_newline = false;
673                    }
674                    SyntaxKind::DASH => {
675                        // For sequence items: if DASH follows NEWLINE, add indent first
676                        if after_newline && indent > 0 {
677                            builder.token(SyntaxKind::WHITESPACE.into(), &" ".repeat(indent));
678                        }
679                        builder.token(t.kind().into(), t.text());
680                        after_newline = false;
681                    }
682                    _ => {
683                        // For any other token after a newline, add indent if needed
684                        if after_newline && indent > 0 {
685                            builder.token(SyntaxKind::WHITESPACE.into(), &" ".repeat(indent));
686                        }
687                        builder.token(t.kind().into(), t.text());
688                        after_newline = false;
689                    }
690                }
691            }
692        }
693    }
694}
695
696// AsYaml trait implementations for primitive types
697//
698// Uses macros to reduce boilerplate for the 12 numeric primitive types.
699
700/// Macro to implement AsYaml for integer types
701macro_rules! impl_as_yaml_int {
702    ($($ty:ty),+ $(,)?) => {
703        $(impl AsYaml for $ty {
704            fn as_node(&self) -> Option<&crate::yaml::SyntaxNode> {
705                None
706            }
707
708            fn kind(&self) -> YamlKind {
709                YamlKind::Scalar
710            }
711
712            fn build_content(&self, builder: &mut rowan::GreenNodeBuilder, _indent: usize, _flow_context: bool) -> bool {
713                use crate::lex::SyntaxKind;
714                builder.start_node(SyntaxKind::SCALAR.into());
715                builder.token(SyntaxKind::INT.into(), &self.to_string());
716                builder.finish_node();
717                false
718            }
719
720            fn is_inline(&self) -> bool {
721                true
722            }
723        })+
724    };
725}
726
727/// Macro to implement AsYaml for floating-point types
728macro_rules! impl_as_yaml_float {
729    ($($ty:ty),+ $(,)?) => {
730        $(impl AsYaml for $ty {
731            fn as_node(&self) -> Option<&crate::yaml::SyntaxNode> {
732                None
733            }
734
735            fn kind(&self) -> YamlKind {
736                YamlKind::Scalar
737            }
738
739            fn build_content(&self, builder: &mut rowan::GreenNodeBuilder, _indent: usize, _flow_context: bool) -> bool {
740                use crate::lex::SyntaxKind;
741                builder.start_node(SyntaxKind::SCALAR.into());
742                let s = self.to_string();
743                // Ensure floats always have a decimal point for YAML clarity
744                let float_str = if s.contains('.') || s.contains('e') || s.contains('E') {
745                    s
746                } else {
747                    format!("{}.0", s)
748                };
749                builder.token(SyntaxKind::FLOAT.into(), &float_str);
750                builder.finish_node();
751                false
752            }
753
754            fn is_inline(&self) -> bool {
755                true
756            }
757        })+
758    };
759}
760
761// Implement AsYaml for integer types
762impl_as_yaml_int!(i64, i32, i16, i8, isize, u64, u32, u16, u8, usize);
763
764// Implement AsYaml for floating-point types
765impl_as_yaml_float!(f64, f32);
766
767impl AsYaml for bool {
768    fn as_node(&self) -> Option<&crate::yaml::SyntaxNode> {
769        None
770    }
771
772    fn kind(&self) -> YamlKind {
773        YamlKind::Scalar
774    }
775
776    fn build_content(
777        &self,
778        builder: &mut rowan::GreenNodeBuilder,
779        _indent: usize,
780        _flow_context: bool,
781    ) -> bool {
782        use crate::lex::SyntaxKind;
783        builder.start_node(SyntaxKind::SCALAR.into());
784        builder.token(
785            SyntaxKind::BOOL.into(),
786            if *self { "true" } else { "false" },
787        );
788        builder.finish_node();
789        false
790    }
791
792    fn is_inline(&self) -> bool {
793        true
794    }
795}
796
797impl AsYaml for String {
798    fn as_node(&self) -> Option<&crate::yaml::SyntaxNode> {
799        None
800    }
801
802    fn kind(&self) -> YamlKind {
803        YamlKind::Scalar
804    }
805
806    fn build_content(
807        &self,
808        builder: &mut rowan::GreenNodeBuilder,
809        _indent: usize,
810        flow_context: bool,
811    ) -> bool {
812        self.as_str().build_content(builder, _indent, flow_context)
813    }
814
815    fn is_inline(&self) -> bool {
816        true
817    }
818}
819
820impl AsYaml for &str {
821    fn as_node(&self) -> Option<&crate::yaml::SyntaxNode> {
822        None
823    }
824
825    fn kind(&self) -> YamlKind {
826        YamlKind::Scalar
827    }
828
829    fn build_content(
830        &self,
831        builder: &mut rowan::GreenNodeBuilder,
832        _indent: usize,
833        flow_context: bool,
834    ) -> bool {
835        use crate::lex::SyntaxKind;
836        use crate::scalar::ScalarValue;
837
838        // In flow context (JSON), always use double-quoted strings for compatibility
839        // In block context (YAML), use standard quoting rules
840        let scalar = if flow_context {
841            ScalarValue::double_quoted(*self)
842        } else {
843            ScalarValue::string(*self)
844        };
845
846        let yaml_text = scalar.to_yaml_string();
847        // Both quoted and unquoted strings use STRING token kind;
848        // the token text includes any quotes needed for disambiguation.
849        builder.start_node(SyntaxKind::SCALAR.into());
850        builder.token(SyntaxKind::STRING.into(), &yaml_text);
851        builder.finish_node();
852        false
853    }
854
855    fn is_inline(&self) -> bool {
856        true
857    }
858}
859
860#[cfg(test)]
861mod tests {
862    use super::*;
863    use crate::yaml::Document;
864    use std::str::FromStr;
865
866    #[test]
867    fn test_yaml_eq_different_quoting_styles() {
868        let yaml = r#"
869plain: value
870single: 'value'
871double: "value"
872"#;
873
874        let doc = Document::from_str(yaml).unwrap();
875        let mapping = doc.as_mapping().unwrap();
876
877        let plain = mapping.get("plain").unwrap();
878        let single = mapping.get("single").unwrap();
879        let double = mapping.get("double").unwrap();
880
881        // All three should be equal (semantic equality ignores quoting)
882        assert!(yaml_eq(&plain, &single));
883        assert!(yaml_eq(&single, &double));
884        assert!(yaml_eq(&plain, &double));
885
886        // Compare with raw strings
887        assert!(yaml_eq(&plain, &"value"));
888        assert!(yaml_eq(&single, &"value"));
889        assert!(yaml_eq(&double, &"value"));
890    }
891
892    #[test]
893    fn test_yaml_eq_escape_sequences() {
894        let yaml = r#"
895newline1: "line1\nline2"
896newline2: "line1
897line2"
898tab1: "a\tb"
899tab2: "a	b"
900backslash1: "path\\file"
901backslash2: 'path\file'
902quote1: "say \"hi\""
903quote2: 'say "hi"'
904"#;
905
906        let doc = Document::from_str(yaml).unwrap();
907        let mapping = doc.as_mapping().unwrap();
908
909        let newline1 = mapping.get("newline1").unwrap();
910        let newline2 = mapping.get("newline2").unwrap();
911        let tab1 = mapping.get("tab1").unwrap();
912        let tab2 = mapping.get("tab2").unwrap();
913        let backslash1 = mapping.get("backslash1").unwrap();
914        let backslash2 = mapping.get("backslash2").unwrap();
915        let quote1 = mapping.get("quote1").unwrap();
916        let quote2 = mapping.get("quote2").unwrap();
917
918        // Escaped newlines should equal actual newlines
919        assert!(yaml_eq(&newline1, &newline2));
920
921        // Escaped tabs should equal actual tabs
922        assert!(yaml_eq(&tab1, &tab2));
923
924        // Backslash handling: single-quoted strings don't interpret backslashes as escapes
925        // So 'path\file' is literally "path\file" (one backslash)
926        // And "path\\file" escapes to "path\file" (one backslash)
927        // Therefore they ARE equal!
928        assert!(yaml_eq(&backslash1, &backslash2));
929        assert!(yaml_eq(&backslash1, &"path\\file"));
930        assert!(yaml_eq(&backslash2, &"path\\file"));
931
932        // Quote handling
933        assert!(yaml_eq(&quote1, &quote2));
934        assert!(yaml_eq(&quote1, &r#"say "hi""#));
935    }
936
937    #[test]
938    fn test_yaml_eq_single_quote_escaping() {
939        let yaml = r#"
940single1: 'can''t'
941single2: "can't"
942"#;
943
944        let doc = Document::from_str(yaml).unwrap();
945        let mapping = doc.as_mapping().unwrap();
946
947        let single1 = mapping.get("single1").unwrap();
948        let single2 = mapping.get("single2").unwrap();
949
950        // Single-quoted '' should equal double-quoted '
951        assert!(yaml_eq(&single1, &single2));
952        assert!(yaml_eq(&single1, &"can't"));
953    }
954
955    #[test]
956    fn test_yaml_eq_unicode_escapes() {
957        let yaml = r#"
958unicode1: "hello\x20world"
959unicode2: "hello world"
960unicode3: "smiley\u0020face"
961unicode4: "smiley face"
962"#;
963
964        let doc = Document::from_str(yaml).unwrap();
965        let mapping = doc.as_mapping().unwrap();
966
967        let unicode1 = mapping.get("unicode1").unwrap();
968        let unicode2 = mapping.get("unicode2").unwrap();
969        let unicode3 = mapping.get("unicode3").unwrap();
970        let unicode4 = mapping.get("unicode4").unwrap();
971
972        // \x20 is space
973        assert!(yaml_eq(&unicode1, &unicode2));
974        assert!(yaml_eq(&unicode1, &"hello world"));
975
976        // \u0020 is also space
977        assert!(yaml_eq(&unicode3, &unicode4));
978        assert!(yaml_eq(&unicode3, &"smiley face"));
979    }
980
981    #[test]
982    fn test_yaml_eq_with_comments() {
983        let yaml1 = r#"
984# This is a comment
985key: value # inline comment
986"#;
987
988        let yaml2 = r#"
989key: value
990"#;
991
992        let doc1 = Document::from_str(yaml1).unwrap();
993        let doc2 = Document::from_str(yaml2).unwrap();
994
995        let mapping1 = doc1.as_mapping().unwrap();
996        let mapping2 = doc2.as_mapping().unwrap();
997
998        // Mappings with and without comments should be equal (semantic equality)
999        assert!(yaml_eq(&mapping1, &mapping2));
1000
1001        let value1 = mapping1.get("key").unwrap();
1002        let value2 = mapping2.get("key").unwrap();
1003
1004        assert!(yaml_eq(&value1, &value2));
1005        assert!(yaml_eq(&value1, &"value"));
1006    }
1007
1008    #[test]
1009    fn test_yaml_eq_mappings() {
1010        let yaml1 = r#"
1011a: 1
1012b: 2
1013c: 3
1014"#;
1015
1016        let yaml2 = r#"
1017a: 1
1018b: 2
1019c: 3
1020"#;
1021
1022        let yaml3 = r#"
1023a: 1
1024c: 3
1025b: 2
1026"#;
1027
1028        let doc1 = Document::from_str(yaml1).unwrap();
1029        let doc2 = Document::from_str(yaml2).unwrap();
1030        let doc3 = Document::from_str(yaml3).unwrap();
1031
1032        let mapping1 = doc1.as_mapping().unwrap();
1033        let mapping2 = doc2.as_mapping().unwrap();
1034        let mapping3 = doc3.as_mapping().unwrap();
1035
1036        // Same mappings should be equal
1037        assert!(yaml_eq(&mapping1, &mapping2));
1038
1039        // Different order should NOT be equal (order matters for yaml_eq)
1040        assert!(!yaml_eq(&mapping1, &mapping3));
1041    }
1042
1043    #[test]
1044    fn test_yaml_eq_sequences() {
1045        let yaml1 = r#"
1046- one
1047- two
1048- three
1049"#;
1050
1051        let yaml2 = r#"
1052- one
1053- two
1054- three
1055"#;
1056
1057        let yaml3 = r#"
1058- one
1059- three
1060- two
1061"#;
1062
1063        let doc1 = Document::from_str(yaml1).unwrap();
1064        let doc2 = Document::from_str(yaml2).unwrap();
1065        let doc3 = Document::from_str(yaml3).unwrap();
1066
1067        let seq1 = doc1.as_sequence().unwrap();
1068        let seq2 = doc2.as_sequence().unwrap();
1069        let seq3 = doc3.as_sequence().unwrap();
1070
1071        // Same sequences should be equal
1072        assert!(yaml_eq(&seq1, &seq2));
1073
1074        // Different order should NOT be equal
1075        assert!(!yaml_eq(&seq1, &seq3));
1076    }
1077
1078    #[test]
1079    fn test_yaml_eq_nested_structures() {
1080        let yaml1 = r#"
1081outer:
1082  inner:
1083    key: "value"
1084"#;
1085
1086        let yaml2 = r#"
1087outer:
1088  inner:
1089    key: 'value'
1090"#;
1091
1092        let doc1 = Document::from_str(yaml1).unwrap();
1093        let doc2 = Document::from_str(yaml2).unwrap();
1094
1095        let mapping1 = doc1.as_mapping().unwrap();
1096        let mapping2 = doc2.as_mapping().unwrap();
1097
1098        // Nested structures with different quoting should be equal
1099        assert!(yaml_eq(&mapping1, &mapping2));
1100    }
1101
1102    #[test]
1103    fn test_yaml_eq_special_characters() {
1104        let yaml = r#"
1105bell: "\a"
1106escape: "\e"
1107"null": "\0"
1108backspace: "\b"
1109formfeed: "\f"
1110carriagereturn: "\r"
1111verticaltab: "\v"
1112"#;
1113
1114        let doc = Document::from_str(yaml).unwrap();
1115        let mapping = doc.as_mapping().unwrap();
1116
1117        // Note: These all compare STRING values with escaped chars against raw Rust strings
1118        // They should be equal because both sides are STRING type
1119        assert!(yaml_eq(&mapping.get("bell").unwrap(), &"\x07"));
1120        assert!(yaml_eq(&mapping.get("escape").unwrap(), &"\x1B"));
1121        assert!(yaml_eq(&mapping.get("null").unwrap(), &"\0"));
1122        assert!(yaml_eq(&mapping.get("backspace").unwrap(), &"\x08"));
1123        assert!(yaml_eq(&mapping.get("formfeed").unwrap(), &"\x0C"));
1124        assert!(yaml_eq(&mapping.get("carriagereturn").unwrap(), &"\r"));
1125        assert!(yaml_eq(&mapping.get("verticaltab").unwrap(), &"\x0B"));
1126    }
1127
1128    #[test]
1129    fn test_yaml_eq_empty_strings() {
1130        let yaml = r#"
1131empty1: ""
1132empty2: ''
1133"#;
1134
1135        let doc = Document::from_str(yaml).unwrap();
1136        let mapping = doc.as_mapping().unwrap();
1137
1138        let empty1 = mapping.get("empty1").unwrap();
1139        let empty2 = mapping.get("empty2").unwrap();
1140
1141        assert!(yaml_eq(&empty1, &empty2));
1142        assert!(yaml_eq(&empty1, &""));
1143    }
1144
1145    #[test]
1146    fn test_yaml_eq_whitespace_handling() {
1147        let yaml = r#"
1148spaces1: "  leading"
1149spaces2: "trailing  "
1150spaces3: "  both  "
1151plain: value
1152"#;
1153
1154        let doc = Document::from_str(yaml).unwrap();
1155        let mapping = doc.as_mapping().unwrap();
1156
1157        let spaces1 = mapping.get("spaces1").unwrap();
1158        let spaces2 = mapping.get("spaces2").unwrap();
1159        let spaces3 = mapping.get("spaces3").unwrap();
1160        let plain = mapping.get("plain").unwrap();
1161
1162        // Whitespace should be preserved exactly
1163        assert!(yaml_eq(&spaces1, &"  leading"));
1164        assert!(yaml_eq(&spaces2, &"trailing  "));
1165        assert!(yaml_eq(&spaces3, &"  both  "));
1166        assert!(yaml_eq(&plain, &"value"));
1167
1168        // These should NOT be equal
1169        assert!(!yaml_eq(&spaces1, &"leading"));
1170        assert!(!yaml_eq(&spaces2, &"trailing"));
1171    }
1172
1173    #[test]
1174    fn test_yaml_eq_different_types() {
1175        let yaml = r#"
1176string: "123"
1177number: 123
1178sequence:
1179  - item
1180mapping:
1181  key: value
1182"#;
1183
1184        let doc = Document::from_str(yaml).unwrap();
1185        let mapping = doc.as_mapping().unwrap();
1186
1187        let string_val = mapping.get("string").unwrap();
1188        let number_val = mapping.get("number").unwrap();
1189        let sequence_val = mapping.get("sequence").unwrap();
1190        let mapping_val = mapping.get("mapping").unwrap();
1191
1192        // yaml_eq does YAML-level semantic comparison:
1193        // "123" (string) and 123 (integer) are DIFFERENT types
1194        assert!(!yaml_eq(&string_val, &number_val));
1195        assert!(!yaml_eq(&string_val, &sequence_val));
1196        assert!(!yaml_eq(&string_val, &mapping_val));
1197        assert!(!yaml_eq(&number_val, &sequence_val));
1198        assert!(!yaml_eq(&sequence_val, &mapping_val));
1199    }
1200
1201    #[test]
1202    fn test_yaml_eq_line_folding_escapes() {
1203        let yaml = r#"
1204folded1: "line1\
1205 line2"
1206folded2: "line1 line2"
1207"#;
1208
1209        let doc = Document::from_str(yaml).unwrap();
1210        let mapping = doc.as_mapping().unwrap();
1211
1212        let folded1 = mapping.get("folded1").unwrap();
1213        let folded2 = mapping.get("folded2").unwrap();
1214
1215        // Per YAML spec, backslash at end of line folds to a space
1216        assert!(yaml_eq(&folded1, &folded2));
1217        assert!(yaml_eq(&folded1, &"line1 line2"));
1218    }
1219
1220    #[test]
1221    fn test_yaml_eq_complex_unicode() {
1222        let yaml = r#"
1223emoji1: "😀"
1224emoji2: "\U0001F600"
1225"#;
1226
1227        let doc = Document::from_str(yaml).unwrap();
1228        let mapping = doc.as_mapping().unwrap();
1229
1230        let emoji1 = mapping.get("emoji1").unwrap();
1231        let emoji2 = mapping.get("emoji2").unwrap();
1232
1233        // Unicode escape for emoji should equal literal emoji
1234        assert!(yaml_eq(&emoji1, &emoji2));
1235        assert!(yaml_eq(&emoji1, &"😀"));
1236    }
1237
1238    #[test]
1239    fn test_yaml_eq_block_scalars() {
1240        let yaml1 = "literal1: |\n  line1\n  line2\n";
1241        let yaml2 = "literal2: |\n  line1\n  line2\n";
1242
1243        let doc1 = Document::from_str(yaml1).unwrap();
1244        let doc2 = Document::from_str(yaml2).unwrap();
1245
1246        let mapping1 = doc1.as_mapping().unwrap();
1247        let mapping2 = doc2.as_mapping().unwrap();
1248
1249        let literal1 = mapping1.get("literal1").unwrap();
1250        let literal2 = mapping2.get("literal2").unwrap();
1251
1252        // Same literal block scalars should be equal
1253        assert!(yaml_eq(&literal1, &literal2));
1254    }
1255
1256    #[test]
1257    fn test_yaml_eq_null_and_special_values() {
1258        let yaml = r#"
1259null1: null
1260null2: null
1261null3:
1262empty: ""
1263"#;
1264
1265        let doc = Document::from_str(yaml).unwrap();
1266        let mapping = doc.as_mapping().unwrap();
1267
1268        let null1 = mapping.get("null1").unwrap();
1269        let null2 = mapping.get("null2").unwrap();
1270        let null3 = mapping.get("null3").unwrap();
1271        let empty = mapping.get("empty").unwrap();
1272
1273        // Same null representations should be equal (both are NULL type)
1274        assert!(yaml_eq(&null1, &null2));
1275
1276        // null3 (implicit null via empty value) should also be equal to explicit null
1277        // Per YAML spec, "key:" is semantically identical to "key: null"
1278        assert!(yaml_eq(&null3, &null1));
1279        assert!(yaml_eq(&null3, &null2));
1280
1281        // NULL type != STRING type (even though empty might look like null)
1282        assert!(!yaml_eq(&null1, &empty));
1283
1284        // NULL scalar != STRING "null" (different types)
1285        assert!(!yaml_eq(&null1, &"null"));
1286    }
1287
1288    #[test]
1289    fn test_yaml_eq_boolean_representations() {
1290        let yaml = r#"
1291true1: true
1292true2: True
1293true3: TRUE
1294false1: false
1295false2: False
1296"#;
1297
1298        let doc = Document::from_str(yaml).unwrap();
1299        let mapping = doc.as_mapping().unwrap();
1300
1301        let true1 = mapping.get("true1").unwrap();
1302        let true2 = mapping.get("true2").unwrap();
1303        let true3 = mapping.get("true3").unwrap();
1304        let false1 = mapping.get("false1").unwrap();
1305        let false2 = mapping.get("false2").unwrap();
1306
1307        // Different case booleans ARE semantically equal (normalized to lowercase)
1308        assert!(yaml_eq(&true1, &true2));
1309        assert!(yaml_eq(&true1, &true3));
1310        assert!(yaml_eq(&false1, &false2));
1311
1312        // BOOL scalars do NOT equal STRING scalars
1313        assert!(!yaml_eq(&true1, &"true"));
1314        assert!(!yaml_eq(&false1, &"false"));
1315    }
1316
1317    #[test]
1318    fn test_yaml_eq_numeric_formats() {
1319        let yaml = r#"
1320decimal: 123
1321octal: 0o173
1322hex: 0x7B
1323"#;
1324
1325        let doc = Document::from_str(yaml).unwrap();
1326        let mapping = doc.as_mapping().unwrap();
1327
1328        let decimal = mapping.get("decimal").unwrap();
1329        let octal = mapping.get("octal").unwrap();
1330        let hex = mapping.get("hex").unwrap();
1331
1332        // Different numeric formats ARE semantically equal (all equal 123)
1333        assert!(yaml_eq(&decimal, &octal));
1334        assert!(yaml_eq(&decimal, &hex));
1335        assert!(yaml_eq(&octal, &hex));
1336
1337        // INT scalars do NOT equal STRING scalars (even if text is same)
1338        assert!(!yaml_eq(&decimal, &"123"));
1339
1340        // But they DO equal raw integer values
1341        assert!(yaml_eq(&decimal, &123));
1342        assert!(yaml_eq(&octal, &123));
1343        assert!(yaml_eq(&hex, &123));
1344    }
1345
1346    #[test]
1347    fn test_yaml_eq_with_anchors() {
1348        let yaml = r#"
1349original: &anchor value
1350duplicate: value
1351"#;
1352
1353        let doc = Document::from_str(yaml).unwrap();
1354        let mapping = doc.as_mapping().unwrap();
1355
1356        let original = mapping.get("original").unwrap();
1357        let duplicate = mapping.get("duplicate").unwrap();
1358
1359        // Values with anchors should equal plain values (anchor syntax is ignored)
1360        assert!(yaml_eq(&original, &duplicate));
1361        assert!(yaml_eq(&original, &"value"));
1362    }
1363
1364    #[test]
1365    fn test_yaml_eq_flow_vs_block_collections() {
1366        let yaml = r#"
1367flow_seq: [1, 2, 3]
1368block_seq:
1369  - 1
1370  - 2
1371  - 3
1372flow_map: {a: 1, b: 2}
1373block_map:
1374  a: 1
1375  b: 2
1376"#;
1377
1378        let doc = Document::from_str(yaml).unwrap();
1379        let mapping = doc.as_mapping().unwrap();
1380
1381        let flow_seq = mapping.get("flow_seq").unwrap();
1382        let block_seq = mapping.get("block_seq").unwrap();
1383        let flow_map = mapping.get("flow_map").unwrap();
1384        let block_map = mapping.get("block_map").unwrap();
1385
1386        // Flow and block styles should be semantically equal
1387        assert!(yaml_eq(&flow_seq, &block_seq));
1388        assert!(yaml_eq(&flow_map, &block_map));
1389    }
1390}