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`.
585impl<T: AsYaml> AsYaml for &T {
586    fn as_node(&self) -> Option<&SyntaxNode> {
587        (*self).as_node()
588    }
589
590    fn kind(&self) -> YamlKind {
591        (*self).kind()
592    }
593
594    fn build_content(
595        &self,
596        builder: &mut rowan::GreenNodeBuilder,
597        indent: usize,
598        flow_context: bool,
599    ) -> bool {
600        (*self).build_content(builder, indent, flow_context)
601    }
602
603    fn is_inline(&self) -> bool {
604        (*self).is_inline()
605    }
606}
607
608/// Recursively copy the children of `node` into `builder`.
609pub(crate) fn copy_node_content(builder: &mut rowan::GreenNodeBuilder, node: &SyntaxNode) {
610    for child in node.children_with_tokens() {
611        match child {
612            rowan::NodeOrToken::Node(n) => {
613                builder.start_node(n.kind().into());
614                copy_node_content(builder, &n);
615                builder.finish_node();
616            }
617            rowan::NodeOrToken::Token(t) => {
618                builder.token(t.kind().into(), t.text());
619            }
620        }
621    }
622}
623
624/// Recursively copy the children of `node` into `builder`, adjusting indentation.
625///
626/// When copying WHITESPACE or INDENT tokens that appear after a NEWLINE,
627/// replaces them with the specified `indent` amount. This is useful when
628/// inserting pre-built sequences/mappings into a different nesting context.
629pub(crate) fn copy_node_content_with_indent(
630    builder: &mut rowan::GreenNodeBuilder,
631    node: &SyntaxNode,
632    indent: usize,
633) {
634    use crate::lex::SyntaxKind;
635    let mut after_newline = false;
636
637    for child in node.children_with_tokens() {
638        match child {
639            rowan::NodeOrToken::Node(n) => {
640                // If this node follows a newline, add indent before starting the node
641                if after_newline && indent > 0 {
642                    builder.token(SyntaxKind::WHITESPACE.into(), &" ".repeat(indent));
643                }
644                builder.start_node(n.kind().into());
645                copy_node_content_with_indent(builder, &n, indent);
646                builder.finish_node();
647                after_newline = false;
648            }
649            rowan::NodeOrToken::Token(t) => {
650                match t.kind() {
651                    SyntaxKind::NEWLINE => {
652                        builder.token(t.kind().into(), t.text());
653                        after_newline = true;
654                    }
655                    SyntaxKind::WHITESPACE | SyntaxKind::INDENT => {
656                        // If this whitespace follows a newline, add our indent to the existing indent
657                        if after_newline && indent > 0 {
658                            let existing_indent = t.text().len();
659                            let total_indent = indent + existing_indent;
660                            builder.token(SyntaxKind::WHITESPACE.into(), &" ".repeat(total_indent));
661                        } else if after_newline {
662                            // indent == 0, skip the whitespace entirely
663                        } else {
664                            // Not after newline, keep as-is (e.g., space after dash)
665                            builder.token(t.kind().into(), t.text());
666                        }
667                        after_newline = false;
668                    }
669                    SyntaxKind::DASH => {
670                        // For sequence items: if DASH follows NEWLINE, add indent first
671                        if after_newline && indent > 0 {
672                            builder.token(SyntaxKind::WHITESPACE.into(), &" ".repeat(indent));
673                        }
674                        builder.token(t.kind().into(), t.text());
675                        after_newline = false;
676                    }
677                    _ => {
678                        // For any other token after a newline, add indent if needed
679                        if after_newline && indent > 0 {
680                            builder.token(SyntaxKind::WHITESPACE.into(), &" ".repeat(indent));
681                        }
682                        builder.token(t.kind().into(), t.text());
683                        after_newline = false;
684                    }
685                }
686            }
687        }
688    }
689}
690
691// AsYaml trait implementations for primitive types
692//
693// Uses macros to reduce boilerplate for the 12 numeric primitive types.
694
695/// Macro to implement AsYaml for integer types
696macro_rules! impl_as_yaml_int {
697    ($($ty:ty),+ $(,)?) => {
698        $(impl AsYaml for $ty {
699            fn as_node(&self) -> Option<&crate::yaml::SyntaxNode> {
700                None
701            }
702
703            fn kind(&self) -> YamlKind {
704                YamlKind::Scalar
705            }
706
707            fn build_content(&self, builder: &mut rowan::GreenNodeBuilder, _indent: usize, _flow_context: bool) -> bool {
708                use crate::lex::SyntaxKind;
709                builder.start_node(SyntaxKind::SCALAR.into());
710                builder.token(SyntaxKind::INT.into(), &self.to_string());
711                builder.finish_node();
712                false
713            }
714
715            fn is_inline(&self) -> bool {
716                true
717            }
718        })+
719    };
720}
721
722/// Macro to implement AsYaml for floating-point types
723macro_rules! impl_as_yaml_float {
724    ($($ty:ty),+ $(,)?) => {
725        $(impl AsYaml for $ty {
726            fn as_node(&self) -> Option<&crate::yaml::SyntaxNode> {
727                None
728            }
729
730            fn kind(&self) -> YamlKind {
731                YamlKind::Scalar
732            }
733
734            fn build_content(&self, builder: &mut rowan::GreenNodeBuilder, _indent: usize, _flow_context: bool) -> bool {
735                use crate::lex::SyntaxKind;
736                builder.start_node(SyntaxKind::SCALAR.into());
737                let s = self.to_string();
738                // Ensure floats always have a decimal point for YAML clarity
739                let float_str = if s.contains('.') || s.contains('e') || s.contains('E') {
740                    s
741                } else {
742                    format!("{}.0", s)
743                };
744                builder.token(SyntaxKind::FLOAT.into(), &float_str);
745                builder.finish_node();
746                false
747            }
748
749            fn is_inline(&self) -> bool {
750                true
751            }
752        })+
753    };
754}
755
756// Implement AsYaml for integer types
757impl_as_yaml_int!(i64, i32, i16, i8, isize, u64, u32, u16, u8, usize);
758
759// Implement AsYaml for floating-point types
760impl_as_yaml_float!(f64, f32);
761
762impl AsYaml for bool {
763    fn as_node(&self) -> Option<&crate::yaml::SyntaxNode> {
764        None
765    }
766
767    fn kind(&self) -> YamlKind {
768        YamlKind::Scalar
769    }
770
771    fn build_content(
772        &self,
773        builder: &mut rowan::GreenNodeBuilder,
774        _indent: usize,
775        _flow_context: bool,
776    ) -> bool {
777        use crate::lex::SyntaxKind;
778        builder.start_node(SyntaxKind::SCALAR.into());
779        builder.token(
780            SyntaxKind::BOOL.into(),
781            if *self { "true" } else { "false" },
782        );
783        builder.finish_node();
784        false
785    }
786
787    fn is_inline(&self) -> bool {
788        true
789    }
790}
791
792impl AsYaml for String {
793    fn as_node(&self) -> Option<&crate::yaml::SyntaxNode> {
794        None
795    }
796
797    fn kind(&self) -> YamlKind {
798        YamlKind::Scalar
799    }
800
801    fn build_content(
802        &self,
803        builder: &mut rowan::GreenNodeBuilder,
804        _indent: usize,
805        flow_context: bool,
806    ) -> bool {
807        self.as_str().build_content(builder, _indent, flow_context)
808    }
809
810    fn is_inline(&self) -> bool {
811        true
812    }
813}
814
815impl AsYaml for &str {
816    fn as_node(&self) -> Option<&crate::yaml::SyntaxNode> {
817        None
818    }
819
820    fn kind(&self) -> YamlKind {
821        YamlKind::Scalar
822    }
823
824    fn build_content(
825        &self,
826        builder: &mut rowan::GreenNodeBuilder,
827        _indent: usize,
828        flow_context: bool,
829    ) -> bool {
830        use crate::lex::SyntaxKind;
831        use crate::scalar::ScalarValue;
832
833        // In flow context (JSON), always use double-quoted strings for compatibility
834        // In block context (YAML), use standard quoting rules
835        let scalar = if flow_context {
836            ScalarValue::double_quoted(*self)
837        } else {
838            ScalarValue::string(*self)
839        };
840
841        let yaml_text = scalar.to_yaml_string();
842        // Both quoted and unquoted strings use STRING token kind;
843        // the token text includes any quotes needed for disambiguation.
844        builder.start_node(SyntaxKind::SCALAR.into());
845        builder.token(SyntaxKind::STRING.into(), &yaml_text);
846        builder.finish_node();
847        false
848    }
849
850    fn is_inline(&self) -> bool {
851        true
852    }
853}
854
855#[cfg(test)]
856mod tests {
857    use super::*;
858    use crate::yaml::Document;
859    use std::str::FromStr;
860
861    #[test]
862    fn test_yaml_eq_different_quoting_styles() {
863        let yaml = r#"
864plain: value
865single: 'value'
866double: "value"
867"#;
868
869        let doc = Document::from_str(yaml).unwrap();
870        let mapping = doc.as_mapping().unwrap();
871
872        let plain = mapping.get("plain").unwrap();
873        let single = mapping.get("single").unwrap();
874        let double = mapping.get("double").unwrap();
875
876        // All three should be equal (semantic equality ignores quoting)
877        assert!(yaml_eq(&plain, &single));
878        assert!(yaml_eq(&single, &double));
879        assert!(yaml_eq(&plain, &double));
880
881        // Compare with raw strings
882        assert!(yaml_eq(&plain, &"value"));
883        assert!(yaml_eq(&single, &"value"));
884        assert!(yaml_eq(&double, &"value"));
885    }
886
887    #[test]
888    fn test_yaml_eq_escape_sequences() {
889        let yaml = r#"
890newline1: "line1\nline2"
891newline2: "line1
892line2"
893tab1: "a\tb"
894tab2: "a	b"
895backslash1: "path\\file"
896backslash2: 'path\file'
897quote1: "say \"hi\""
898quote2: 'say "hi"'
899"#;
900
901        let doc = Document::from_str(yaml).unwrap();
902        let mapping = doc.as_mapping().unwrap();
903
904        let newline1 = mapping.get("newline1").unwrap();
905        let newline2 = mapping.get("newline2").unwrap();
906        let tab1 = mapping.get("tab1").unwrap();
907        let tab2 = mapping.get("tab2").unwrap();
908        let backslash1 = mapping.get("backslash1").unwrap();
909        let backslash2 = mapping.get("backslash2").unwrap();
910        let quote1 = mapping.get("quote1").unwrap();
911        let quote2 = mapping.get("quote2").unwrap();
912
913        // Escaped newlines should equal actual newlines
914        assert!(yaml_eq(&newline1, &newline2));
915
916        // Escaped tabs should equal actual tabs
917        assert!(yaml_eq(&tab1, &tab2));
918
919        // Backslash handling: single-quoted strings don't interpret backslashes as escapes
920        // So 'path\file' is literally "path\file" (one backslash)
921        // And "path\\file" escapes to "path\file" (one backslash)
922        // Therefore they ARE equal!
923        assert!(yaml_eq(&backslash1, &backslash2));
924        assert!(yaml_eq(&backslash1, &"path\\file"));
925        assert!(yaml_eq(&backslash2, &"path\\file"));
926
927        // Quote handling
928        assert!(yaml_eq(&quote1, &quote2));
929        assert!(yaml_eq(&quote1, &r#"say "hi""#));
930    }
931
932    #[test]
933    fn test_yaml_eq_single_quote_escaping() {
934        let yaml = r#"
935single1: 'can''t'
936single2: "can't"
937"#;
938
939        let doc = Document::from_str(yaml).unwrap();
940        let mapping = doc.as_mapping().unwrap();
941
942        let single1 = mapping.get("single1").unwrap();
943        let single2 = mapping.get("single2").unwrap();
944
945        // Single-quoted '' should equal double-quoted '
946        assert!(yaml_eq(&single1, &single2));
947        assert!(yaml_eq(&single1, &"can't"));
948    }
949
950    #[test]
951    fn test_yaml_eq_unicode_escapes() {
952        let yaml = r#"
953unicode1: "hello\x20world"
954unicode2: "hello world"
955unicode3: "smiley\u0020face"
956unicode4: "smiley face"
957"#;
958
959        let doc = Document::from_str(yaml).unwrap();
960        let mapping = doc.as_mapping().unwrap();
961
962        let unicode1 = mapping.get("unicode1").unwrap();
963        let unicode2 = mapping.get("unicode2").unwrap();
964        let unicode3 = mapping.get("unicode3").unwrap();
965        let unicode4 = mapping.get("unicode4").unwrap();
966
967        // \x20 is space
968        assert!(yaml_eq(&unicode1, &unicode2));
969        assert!(yaml_eq(&unicode1, &"hello world"));
970
971        // \u0020 is also space
972        assert!(yaml_eq(&unicode3, &unicode4));
973        assert!(yaml_eq(&unicode3, &"smiley face"));
974    }
975
976    #[test]
977    fn test_yaml_eq_with_comments() {
978        let yaml1 = r#"
979# This is a comment
980key: value # inline comment
981"#;
982
983        let yaml2 = r#"
984key: value
985"#;
986
987        let doc1 = Document::from_str(yaml1).unwrap();
988        let doc2 = Document::from_str(yaml2).unwrap();
989
990        let mapping1 = doc1.as_mapping().unwrap();
991        let mapping2 = doc2.as_mapping().unwrap();
992
993        // Mappings with and without comments should be equal (semantic equality)
994        assert!(yaml_eq(&mapping1, &mapping2));
995
996        let value1 = mapping1.get("key").unwrap();
997        let value2 = mapping2.get("key").unwrap();
998
999        assert!(yaml_eq(&value1, &value2));
1000        assert!(yaml_eq(&value1, &"value"));
1001    }
1002
1003    #[test]
1004    fn test_yaml_eq_mappings() {
1005        let yaml1 = r#"
1006a: 1
1007b: 2
1008c: 3
1009"#;
1010
1011        let yaml2 = r#"
1012a: 1
1013b: 2
1014c: 3
1015"#;
1016
1017        let yaml3 = r#"
1018a: 1
1019c: 3
1020b: 2
1021"#;
1022
1023        let doc1 = Document::from_str(yaml1).unwrap();
1024        let doc2 = Document::from_str(yaml2).unwrap();
1025        let doc3 = Document::from_str(yaml3).unwrap();
1026
1027        let mapping1 = doc1.as_mapping().unwrap();
1028        let mapping2 = doc2.as_mapping().unwrap();
1029        let mapping3 = doc3.as_mapping().unwrap();
1030
1031        // Same mappings should be equal
1032        assert!(yaml_eq(&mapping1, &mapping2));
1033
1034        // Different order should NOT be equal (order matters for yaml_eq)
1035        assert!(!yaml_eq(&mapping1, &mapping3));
1036    }
1037
1038    #[test]
1039    fn test_yaml_eq_sequences() {
1040        let yaml1 = r#"
1041- one
1042- two
1043- three
1044"#;
1045
1046        let yaml2 = r#"
1047- one
1048- two
1049- three
1050"#;
1051
1052        let yaml3 = r#"
1053- one
1054- three
1055- two
1056"#;
1057
1058        let doc1 = Document::from_str(yaml1).unwrap();
1059        let doc2 = Document::from_str(yaml2).unwrap();
1060        let doc3 = Document::from_str(yaml3).unwrap();
1061
1062        let seq1 = doc1.as_sequence().unwrap();
1063        let seq2 = doc2.as_sequence().unwrap();
1064        let seq3 = doc3.as_sequence().unwrap();
1065
1066        // Same sequences should be equal
1067        assert!(yaml_eq(&seq1, &seq2));
1068
1069        // Different order should NOT be equal
1070        assert!(!yaml_eq(&seq1, &seq3));
1071    }
1072
1073    #[test]
1074    fn test_yaml_eq_nested_structures() {
1075        let yaml1 = r#"
1076outer:
1077  inner:
1078    key: "value"
1079"#;
1080
1081        let yaml2 = r#"
1082outer:
1083  inner:
1084    key: 'value'
1085"#;
1086
1087        let doc1 = Document::from_str(yaml1).unwrap();
1088        let doc2 = Document::from_str(yaml2).unwrap();
1089
1090        let mapping1 = doc1.as_mapping().unwrap();
1091        let mapping2 = doc2.as_mapping().unwrap();
1092
1093        // Nested structures with different quoting should be equal
1094        assert!(yaml_eq(&mapping1, &mapping2));
1095    }
1096
1097    #[test]
1098    fn test_yaml_eq_special_characters() {
1099        let yaml = r#"
1100bell: "\a"
1101escape: "\e"
1102"null": "\0"
1103backspace: "\b"
1104formfeed: "\f"
1105carriagereturn: "\r"
1106verticaltab: "\v"
1107"#;
1108
1109        let doc = Document::from_str(yaml).unwrap();
1110        let mapping = doc.as_mapping().unwrap();
1111
1112        // Note: These all compare STRING values with escaped chars against raw Rust strings
1113        // They should be equal because both sides are STRING type
1114        assert!(yaml_eq(&mapping.get("bell").unwrap(), &"\x07"));
1115        assert!(yaml_eq(&mapping.get("escape").unwrap(), &"\x1B"));
1116        assert!(yaml_eq(&mapping.get("null").unwrap(), &"\0"));
1117        assert!(yaml_eq(&mapping.get("backspace").unwrap(), &"\x08"));
1118        assert!(yaml_eq(&mapping.get("formfeed").unwrap(), &"\x0C"));
1119        assert!(yaml_eq(&mapping.get("carriagereturn").unwrap(), &"\r"));
1120        assert!(yaml_eq(&mapping.get("verticaltab").unwrap(), &"\x0B"));
1121    }
1122
1123    #[test]
1124    fn test_yaml_eq_empty_strings() {
1125        let yaml = r#"
1126empty1: ""
1127empty2: ''
1128"#;
1129
1130        let doc = Document::from_str(yaml).unwrap();
1131        let mapping = doc.as_mapping().unwrap();
1132
1133        let empty1 = mapping.get("empty1").unwrap();
1134        let empty2 = mapping.get("empty2").unwrap();
1135
1136        assert!(yaml_eq(&empty1, &empty2));
1137        assert!(yaml_eq(&empty1, &""));
1138    }
1139
1140    #[test]
1141    fn test_yaml_eq_whitespace_handling() {
1142        let yaml = r#"
1143spaces1: "  leading"
1144spaces2: "trailing  "
1145spaces3: "  both  "
1146plain: value
1147"#;
1148
1149        let doc = Document::from_str(yaml).unwrap();
1150        let mapping = doc.as_mapping().unwrap();
1151
1152        let spaces1 = mapping.get("spaces1").unwrap();
1153        let spaces2 = mapping.get("spaces2").unwrap();
1154        let spaces3 = mapping.get("spaces3").unwrap();
1155        let plain = mapping.get("plain").unwrap();
1156
1157        // Whitespace should be preserved exactly
1158        assert!(yaml_eq(&spaces1, &"  leading"));
1159        assert!(yaml_eq(&spaces2, &"trailing  "));
1160        assert!(yaml_eq(&spaces3, &"  both  "));
1161        assert!(yaml_eq(&plain, &"value"));
1162
1163        // These should NOT be equal
1164        assert!(!yaml_eq(&spaces1, &"leading"));
1165        assert!(!yaml_eq(&spaces2, &"trailing"));
1166    }
1167
1168    #[test]
1169    fn test_yaml_eq_different_types() {
1170        let yaml = r#"
1171string: "123"
1172number: 123
1173sequence:
1174  - item
1175mapping:
1176  key: value
1177"#;
1178
1179        let doc = Document::from_str(yaml).unwrap();
1180        let mapping = doc.as_mapping().unwrap();
1181
1182        let string_val = mapping.get("string").unwrap();
1183        let number_val = mapping.get("number").unwrap();
1184        let sequence_val = mapping.get("sequence").unwrap();
1185        let mapping_val = mapping.get("mapping").unwrap();
1186
1187        // yaml_eq does YAML-level semantic comparison:
1188        // "123" (string) and 123 (integer) are DIFFERENT types
1189        assert!(!yaml_eq(&string_val, &number_val));
1190        assert!(!yaml_eq(&string_val, &sequence_val));
1191        assert!(!yaml_eq(&string_val, &mapping_val));
1192        assert!(!yaml_eq(&number_val, &sequence_val));
1193        assert!(!yaml_eq(&sequence_val, &mapping_val));
1194    }
1195
1196    #[test]
1197    fn test_yaml_eq_line_folding_escapes() {
1198        let yaml = r#"
1199folded1: "line1\
1200 line2"
1201folded2: "line1 line2"
1202"#;
1203
1204        let doc = Document::from_str(yaml).unwrap();
1205        let mapping = doc.as_mapping().unwrap();
1206
1207        let folded1 = mapping.get("folded1").unwrap();
1208        let folded2 = mapping.get("folded2").unwrap();
1209
1210        // Per YAML spec, backslash at end of line folds to a space
1211        assert!(yaml_eq(&folded1, &folded2));
1212        assert!(yaml_eq(&folded1, &"line1 line2"));
1213    }
1214
1215    #[test]
1216    fn test_yaml_eq_complex_unicode() {
1217        let yaml = r#"
1218emoji1: "😀"
1219emoji2: "\U0001F600"
1220"#;
1221
1222        let doc = Document::from_str(yaml).unwrap();
1223        let mapping = doc.as_mapping().unwrap();
1224
1225        let emoji1 = mapping.get("emoji1").unwrap();
1226        let emoji2 = mapping.get("emoji2").unwrap();
1227
1228        // Unicode escape for emoji should equal literal emoji
1229        assert!(yaml_eq(&emoji1, &emoji2));
1230        assert!(yaml_eq(&emoji1, &"😀"));
1231    }
1232
1233    #[test]
1234    fn test_yaml_eq_block_scalars() {
1235        let yaml1 = "literal1: |\n  line1\n  line2\n";
1236        let yaml2 = "literal2: |\n  line1\n  line2\n";
1237
1238        let doc1 = Document::from_str(yaml1).unwrap();
1239        let doc2 = Document::from_str(yaml2).unwrap();
1240
1241        let mapping1 = doc1.as_mapping().unwrap();
1242        let mapping2 = doc2.as_mapping().unwrap();
1243
1244        let literal1 = mapping1.get("literal1").unwrap();
1245        let literal2 = mapping2.get("literal2").unwrap();
1246
1247        // Same literal block scalars should be equal
1248        assert!(yaml_eq(&literal1, &literal2));
1249    }
1250
1251    #[test]
1252    fn test_yaml_eq_null_and_special_values() {
1253        let yaml = r#"
1254null1: null
1255null2: null
1256null3:
1257empty: ""
1258"#;
1259
1260        let doc = Document::from_str(yaml).unwrap();
1261        let mapping = doc.as_mapping().unwrap();
1262
1263        let null1 = mapping.get("null1").unwrap();
1264        let null2 = mapping.get("null2").unwrap();
1265        let null3 = mapping.get("null3").unwrap();
1266        let empty = mapping.get("empty").unwrap();
1267
1268        // Same null representations should be equal (both are NULL type)
1269        assert!(yaml_eq(&null1, &null2));
1270
1271        // null3 (implicit null via empty value) should also be equal to explicit null
1272        // Per YAML spec, "key:" is semantically identical to "key: null"
1273        assert!(yaml_eq(&null3, &null1));
1274        assert!(yaml_eq(&null3, &null2));
1275
1276        // NULL type != STRING type (even though empty might look like null)
1277        assert!(!yaml_eq(&null1, &empty));
1278
1279        // NULL scalar != STRING "null" (different types)
1280        assert!(!yaml_eq(&null1, &"null"));
1281    }
1282
1283    #[test]
1284    fn test_yaml_eq_boolean_representations() {
1285        let yaml = r#"
1286true1: true
1287true2: True
1288true3: TRUE
1289false1: false
1290false2: False
1291"#;
1292
1293        let doc = Document::from_str(yaml).unwrap();
1294        let mapping = doc.as_mapping().unwrap();
1295
1296        let true1 = mapping.get("true1").unwrap();
1297        let true2 = mapping.get("true2").unwrap();
1298        let true3 = mapping.get("true3").unwrap();
1299        let false1 = mapping.get("false1").unwrap();
1300        let false2 = mapping.get("false2").unwrap();
1301
1302        // Different case booleans ARE semantically equal (normalized to lowercase)
1303        assert!(yaml_eq(&true1, &true2));
1304        assert!(yaml_eq(&true1, &true3));
1305        assert!(yaml_eq(&false1, &false2));
1306
1307        // BOOL scalars do NOT equal STRING scalars
1308        assert!(!yaml_eq(&true1, &"true"));
1309        assert!(!yaml_eq(&false1, &"false"));
1310    }
1311
1312    #[test]
1313    fn test_yaml_eq_numeric_formats() {
1314        let yaml = r#"
1315decimal: 123
1316octal: 0o173
1317hex: 0x7B
1318"#;
1319
1320        let doc = Document::from_str(yaml).unwrap();
1321        let mapping = doc.as_mapping().unwrap();
1322
1323        let decimal = mapping.get("decimal").unwrap();
1324        let octal = mapping.get("octal").unwrap();
1325        let hex = mapping.get("hex").unwrap();
1326
1327        // Different numeric formats ARE semantically equal (all equal 123)
1328        assert!(yaml_eq(&decimal, &octal));
1329        assert!(yaml_eq(&decimal, &hex));
1330        assert!(yaml_eq(&octal, &hex));
1331
1332        // INT scalars do NOT equal STRING scalars (even if text is same)
1333        assert!(!yaml_eq(&decimal, &"123"));
1334
1335        // But they DO equal raw integer values
1336        assert!(yaml_eq(&decimal, &123));
1337        assert!(yaml_eq(&octal, &123));
1338        assert!(yaml_eq(&hex, &123));
1339    }
1340
1341    #[test]
1342    fn test_yaml_eq_with_anchors() {
1343        let yaml = r#"
1344original: &anchor value
1345duplicate: value
1346"#;
1347
1348        let doc = Document::from_str(yaml).unwrap();
1349        let mapping = doc.as_mapping().unwrap();
1350
1351        let original = mapping.get("original").unwrap();
1352        let duplicate = mapping.get("duplicate").unwrap();
1353
1354        // Values with anchors should equal plain values (anchor syntax is ignored)
1355        assert!(yaml_eq(&original, &duplicate));
1356        assert!(yaml_eq(&original, &"value"));
1357    }
1358
1359    #[test]
1360    fn test_yaml_eq_flow_vs_block_collections() {
1361        let yaml = r#"
1362flow_seq: [1, 2, 3]
1363block_seq:
1364  - 1
1365  - 2
1366  - 3
1367flow_map: {a: 1, b: 2}
1368block_map:
1369  a: 1
1370  b: 2
1371"#;
1372
1373        let doc = Document::from_str(yaml).unwrap();
1374        let mapping = doc.as_mapping().unwrap();
1375
1376        let flow_seq = mapping.get("flow_seq").unwrap();
1377        let block_seq = mapping.get("block_seq").unwrap();
1378        let flow_map = mapping.get("flow_map").unwrap();
1379        let block_map = mapping.get("block_map").unwrap();
1380
1381        // Flow and block styles should be semantically equal
1382        assert!(yaml_eq(&flow_seq, &block_seq));
1383        assert!(yaml_eq(&flow_map, &block_map));
1384    }
1385}