Skip to main content

yaml_edit/
value.rs

1//! Value wrapper that can represent any YAML value type (scalar, sequence, mapping).
2
3use crate::scalar::ScalarValue;
4use std::collections::{BTreeMap, BTreeSet};
5use std::fmt;
6
7pub(crate) mod convert;
8
9/// Represents any YAML value - scalar, sequence, mapping, or special collections.
10///
11/// **Deprecated:** Prefer using syntax tree types (`Mapping`, `Sequence`,
12/// `Scalar`) and the `AsYaml` trait instead - they preserve formatting and
13/// work with the CST directly. `YamlValue` keeps tags and scalar styles, but
14/// loses comments, collection styles and other formatting.
15#[derive(Debug, Clone, PartialEq, Eq)]
16pub enum YamlValue {
17    /// A scalar value (string, number, boolean, null)
18    Scalar(ScalarValue),
19    /// A sequence of values (list/array)
20    Sequence(Vec<YamlValue>),
21    /// A mapping of key-value pairs
22    ///
23    /// Converted from a document, each key is held by its string form:
24    /// the canonical value of a scalar (`0x10` is `"16"`, `~` is
25    /// `"null"`) and a flow rendering of a collection, with mapping
26    /// entries sorted (`{y: 1, x: 2}` is `"{x: 2, y: 1}"`). Equal keys
27    /// have the same string form, however they are written.
28    Mapping(BTreeMap<String, YamlValue>),
29    /// A set of unique values (!!set)
30    Set(BTreeSet<String>),
31    /// An ordered mapping preserving key order (!!omap)
32    OrderedMapping(Vec<(String, YamlValue)>),
33    /// A sequence of key-value pairs allowing duplicates (!!pairs)
34    Pairs(Vec<(String, YamlValue)>),
35    /// A sequence or mapping with an explicit tag, such as `!!map` or
36    /// `!local`, other than the ones with their own variant above.
37    ///
38    /// Tags on scalars are kept on the [`ScalarValue`] instead; use
39    /// [`YamlValue::tagged`] to attach a tag to either.
40    Tagged(String, Box<YamlValue>),
41}
42
43impl YamlValue {
44    /// Convert a syntax node to a YamlValue.
45    ///
46    /// Returns `Ok(None)` if `node` is not a YAML value, such as a
47    /// [`Document`](crate::Document) node. A `KEY` or `VALUE` wrapper is
48    /// converted as its content.
49    ///
50    /// Content that a YamlValue cannot hold is an error rather than being
51    /// dropped: an alias, which this cannot resolve without the document's
52    /// anchors ([`YamlValue::from_document`] and
53    /// [`DocumentResolvedExt::get_resolved`] resolve them), a mapping with
54    /// the same key twice, distinct mapping keys with the same string form, such as `1` and
55    /// `'1'`, and `!!set`, `!!omap` or `!!pairs` content of the wrong shape.
56    /// A merge key (`<<`) is kept as an ordinary key.
57    ///
58    /// [`DocumentResolvedExt::get_resolved`]: crate::anchor_resolution::DocumentResolvedExt::get_resolved
59    ///
60    /// ```
61    /// use std::str::FromStr;
62    /// use rowan::ast::AstNode;
63    /// use yaml_edit::{Document, YamlValue};
64    /// use yaml_edit::anchor_resolution::ResolveError;
65    ///
66    /// let doc = Document::from_str("a: &x 1\nb: [*x, 2]\n").unwrap();
67    /// let b = doc.as_mapping().unwrap().get("b").unwrap();
68    /// assert_eq!(
69    ///     YamlValue::cast(b.as_sequence().unwrap().syntax().clone()),
70    ///     Err(ResolveError::UnresolvedAlias { alias: "x".to_string() })
71    /// );
72    /// ```
73    pub fn cast(
74        node: rowan::SyntaxNode<crate::yaml::Lang>,
75    ) -> Result<Option<Self>, crate::anchor_resolution::ResolveError> {
76        crate::as_yaml::YamlNode::from_syntax_peeled(node).map_or(Ok(None), |node| {
77            convert::resolved_value(node, None, &mut Vec::new())
78        })
79    }
80
81    /// Create a scalar value
82    pub fn scalar(value: impl Into<ScalarValue>) -> Self {
83        YamlValue::Scalar(value.into())
84    }
85
86    /// Create an empty sequence
87    pub const fn sequence() -> Self {
88        YamlValue::Sequence(Vec::new())
89    }
90
91    /// Create a sequence from a vector
92    pub const fn from_sequence(items: Vec<YamlValue>) -> Self {
93        YamlValue::Sequence(items)
94    }
95
96    /// Create an empty mapping
97    pub const fn mapping() -> Self {
98        YamlValue::Mapping(BTreeMap::new())
99    }
100
101    /// Parse a raw YAML string into a YamlValue
102    /// This handles both simple scalars and complex structures
103    ///
104    /// Input without a document, such as an empty string, is null.
105    ///
106    /// Fails with [`YamlError::Parse`](crate::YamlError::Parse) if
107    /// `yaml_str` does not parse, with
108    /// [`YamlError::InvalidOperation`](crate::YamlError::InvalidOperation)
109    /// if it holds more than one document, and with
110    /// [`YamlError::Resolve`](crate::YamlError::Resolve) where
111    /// [`YamlValue::from_document`] fails. Convert each document of a
112    /// multi-document stream with [`YamlValue::from_document`] instead.
113    pub fn parse_raw(yaml_str: &str) -> crate::YamlResult<Self> {
114        use std::str::FromStr;
115
116        let file = crate::YamlFile::from_str(yaml_str)?;
117        let mut docs = file.documents();
118        let first = docs.next();
119        if docs.next().is_some() {
120            return Err(crate::YamlError::InvalidOperation {
121                operation: "YamlValue::parse_raw".to_string(),
122                reason: "Input contains multiple YAML documents. Use YamlFile::from_str() and convert each with YamlValue::from_document().".to_string(),
123            });
124        }
125        match first {
126            Some(doc) => Ok(Self::from_document(&doc)?),
127            None => Ok(YamlValue::Scalar(ScalarValue::null())),
128        }
129    }
130
131    /// Convert a Document to a YamlValue
132    ///
133    /// Unlike [`YamlValue::cast`], this resolves aliases against the
134    /// document's anchors and applies merge keys (`<<`), as
135    /// [`DocumentResolvedExt::get_resolved`] does. Merge keys are a YAML 1.1
136    /// type that YAML 1.2 does not define, but are still widely used.
137    /// An empty document is null.
138    ///
139    /// Fails if an alias is undefined or recursive, a merge key operand is
140    /// not a mapping, or on other content [`YamlValue::cast`] reports.
141    ///
142    /// [`DocumentResolvedExt::get_resolved`]: crate::anchor_resolution::DocumentResolvedExt::get_resolved
143    pub fn from_document(
144        doc: &crate::yaml::Document,
145    ) -> Result<Self, crate::anchor_resolution::ResolveError> {
146        use rowan::ast::AstNode;
147        let doc = crate::yaml::Document::cast(crate::nodes::immutable_copy(doc.syntax()))
148            .expect("a copy of a document is a document");
149        let registry = crate::anchor_resolution::AnchorRegistry::from_document(&doc);
150        for child in doc.syntax().children() {
151            let Some(node) = crate::as_yaml::YamlNode::from_syntax_peeled(child) else {
152                continue;
153            };
154            if let Some(value) = convert::resolved_value(node, Some(&registry), &mut Vec::new())? {
155                return Ok(value);
156            }
157        }
158        // An empty node is null.
159        Ok(YamlValue::Scalar(ScalarValue::null()))
160    }
161
162    /// Create a mapping from a BTreeMap
163    pub const fn from_mapping(map: BTreeMap<String, YamlValue>) -> Self {
164        YamlValue::Mapping(map)
165    }
166
167    /// Create an empty set
168    pub const fn set() -> Self {
169        YamlValue::Set(BTreeSet::new())
170    }
171
172    /// Create a set from a BTreeSet
173    pub const fn from_set(set: BTreeSet<String>) -> Self {
174        YamlValue::Set(set)
175    }
176
177    /// Create an empty ordered mapping
178    pub const fn ordered_mapping() -> Self {
179        YamlValue::OrderedMapping(Vec::new())
180    }
181
182    /// Create an ordered mapping from a vector of pairs
183    pub const fn from_ordered_mapping(pairs: Vec<(String, YamlValue)>) -> Self {
184        YamlValue::OrderedMapping(pairs)
185    }
186
187    /// Create empty pairs
188    pub const fn pairs() -> Self {
189        YamlValue::Pairs(Vec::new())
190    }
191
192    /// Create pairs from a vector of pairs
193    pub const fn from_pairs(pairs: Vec<(String, YamlValue)>) -> Self {
194        YamlValue::Pairs(pairs)
195    }
196
197    /// Attach an explicit tag to `value`.
198    ///
199    /// A scalar keeps the tag itself (see [`ScalarValue::with_tag`]), any
200    /// other value is wrapped in [`YamlValue::Tagged`].
201    ///
202    /// ```
203    /// use yaml_edit::YamlValue;
204    ///
205    /// let v = YamlValue::tagged("!point", YamlValue::from(vec![1, 2]));
206    /// assert_eq!(v.tag(), Some("!point"));
207    /// assert_eq!(v.to_yaml_string(0), "!point\n  - 1\n  - 2");
208    /// ```
209    pub fn tagged(tag: impl Into<String>, value: YamlValue) -> Self {
210        match value {
211            YamlValue::Scalar(s) => YamlValue::Scalar(s.with_tag(tag)),
212            v => YamlValue::Tagged(tag.into(), Box::new(v)),
213        }
214    }
215
216    /// The explicit tag on this value, if any.
217    ///
218    /// `!!set`, `!!omap` and `!!pairs` values report their tag too.
219    pub fn tag(&self) -> Option<&str> {
220        match self {
221            YamlValue::Scalar(s) => s.tag(),
222            YamlValue::Sequence(_) | YamlValue::Mapping(_) => None,
223            YamlValue::Set(_) => Some(crate::tag::SET),
224            YamlValue::OrderedMapping(_) => Some(crate::tag::OMAP),
225            YamlValue::Pairs(_) => Some(crate::tag::PAIRS),
226            YamlValue::Tagged(tag, _) => Some(tag),
227        }
228    }
229
230    /// Return `true` if this value is a scalar (string, integer, float, bool, null, etc.).
231    ///
232    /// Use [`as_scalar`](Self::as_scalar) to access the inner [`ScalarValue`].
233    #[inline]
234    pub fn is_scalar(&self) -> bool {
235        matches!(self, YamlValue::Scalar(_))
236    }
237
238    /// Return `true` if this value is a sequence (YAML list / array).
239    ///
240    /// Use [`as_sequence`](Self::as_sequence) to access the inner sequence.
241    #[inline]
242    pub fn is_sequence(&self) -> bool {
243        matches!(self, YamlValue::Sequence(_))
244    }
245
246    /// Return `true` if this value is a plain mapping (`key: value` pairs).
247    ///
248    /// Use [`as_mapping`](Self::as_mapping) to access the inner mapping.
249    #[inline]
250    pub fn is_mapping(&self) -> bool {
251        matches!(self, YamlValue::Mapping(_))
252    }
253
254    /// Return `true` if this value is a `!!set` tagged collection.
255    #[inline]
256    pub fn is_set(&self) -> bool {
257        matches!(self, YamlValue::Set(_))
258    }
259
260    /// Return `true` if this value is a `!!omap` (ordered mapping) tagged collection.
261    #[inline]
262    pub fn is_ordered_mapping(&self) -> bool {
263        matches!(self, YamlValue::OrderedMapping(_))
264    }
265
266    /// Return `true` if this value is a `!!pairs` tagged collection.
267    #[inline]
268    pub fn is_pairs(&self) -> bool {
269        matches!(self, YamlValue::Pairs(_))
270    }
271
272    /// Get as scalar if this is a scalar
273    pub fn as_scalar(&self) -> Option<&ScalarValue> {
274        match self {
275            YamlValue::Scalar(s) => Some(s),
276            _ => None,
277        }
278    }
279
280    /// Get as sequence if this is a sequence
281    pub fn as_sequence(&self) -> Option<&[YamlValue]> {
282        match self {
283            YamlValue::Sequence(seq) => Some(seq),
284            _ => None,
285        }
286    }
287
288    /// Get as mutable sequence if this is a sequence
289    pub fn as_sequence_mut(&mut self) -> Option<&mut Vec<YamlValue>> {
290        match self {
291            YamlValue::Sequence(seq) => Some(seq),
292            _ => None,
293        }
294    }
295
296    /// Get as mapping if this is a mapping
297    pub fn as_mapping(&self) -> Option<&BTreeMap<String, YamlValue>> {
298        match self {
299            YamlValue::Mapping(map) => Some(map),
300            _ => None,
301        }
302    }
303
304    /// Get as mutable mapping if this is a mapping
305    pub fn as_mapping_mut(&mut self) -> Option<&mut BTreeMap<String, YamlValue>> {
306        match self {
307            YamlValue::Mapping(map) => Some(map),
308            _ => None,
309        }
310    }
311
312    /// Get as set if this is a set
313    pub fn as_set(&self) -> Option<&BTreeSet<String>> {
314        match self {
315            YamlValue::Set(set) => Some(set),
316            _ => None,
317        }
318    }
319
320    /// Get as mutable set if this is a set
321    pub fn as_set_mut(&mut self) -> Option<&mut BTreeSet<String>> {
322        match self {
323            YamlValue::Set(set) => Some(set),
324            _ => None,
325        }
326    }
327
328    /// Get as ordered mapping if this is an ordered mapping
329    pub fn as_ordered_mapping(&self) -> Option<&[(String, YamlValue)]> {
330        match self {
331            YamlValue::OrderedMapping(pairs) => Some(pairs),
332            _ => None,
333        }
334    }
335
336    /// Get as mutable ordered mapping if this is an ordered mapping
337    pub fn as_ordered_mapping_mut(&mut self) -> Option<&mut Vec<(String, YamlValue)>> {
338        match self {
339            YamlValue::OrderedMapping(pairs) => Some(pairs),
340            _ => None,
341        }
342    }
343
344    /// Get as pairs if this is pairs
345    pub fn as_pairs(&self) -> Option<&[(String, YamlValue)]> {
346        match self {
347            YamlValue::Pairs(pairs) => Some(pairs),
348            _ => None,
349        }
350    }
351
352    /// Get as mutable pairs if this is pairs
353    pub fn as_pairs_mut(&mut self) -> Option<&mut Vec<(String, YamlValue)>> {
354        match self {
355            YamlValue::Pairs(pairs) => Some(pairs),
356            _ => None,
357        }
358    }
359
360    /// Get a reference to the string value if this is a scalar
361    ///
362    /// Returns the string representation of the scalar value.
363    /// Returns None for non-scalar types (mappings, sequences, etc).
364    /// Note: All scalars can be represented as strings, regardless of their type.
365    pub fn as_str(&self) -> Option<&str> {
366        match self {
367            YamlValue::Scalar(s) => Some(s.value()),
368            _ => None,
369        }
370    }
371
372    /// Try to convert this value to an i64
373    ///
374    /// Returns the integer value if this is a scalar with Integer type.
375    /// Reads the YAML 1.2 forms: decimal, hexadecimal `0x` and octal `0o`.
376    /// Returns None if this is not a scalar or not an integer type.
377    ///
378    /// Note: This respects YAML's type system - `42` (unquoted) is an integer,
379    /// but `"42"` (quoted) is a string and will return None.
380    pub fn to_i64(&self) -> Option<i64> {
381        self.as_scalar().and_then(ScalarValue::to_i64)
382    }
383
384    /// Try to convert this value to an f64
385    ///
386    /// Returns the float value if this is a scalar with Float type.
387    /// Returns None if this is not a scalar or not a float type.
388    ///
389    /// Note: This respects YAML's type system - `3.14` (unquoted) is a float,
390    /// but `"3.14"` (quoted) is a string and will return None.
391    pub fn to_f64(&self) -> Option<f64> {
392        self.as_scalar().and_then(ScalarValue::to_f64)
393    }
394
395    /// Try to convert this value to a bool
396    ///
397    /// Returns the boolean value if this is a scalar with Boolean type.
398    /// Recognizes: true, True, TRUE, false, False, FALSE.
399    /// Returns None if this is not a scalar or not a boolean type.
400    ///
401    /// Note: This respects YAML's type system - `false` (unquoted) is boolean,
402    /// but `"false"` (quoted) is a string and will return None.
403    pub fn to_bool(&self) -> Option<bool> {
404        self.as_scalar().and_then(ScalarValue::to_bool)
405    }
406
407    /// Convert to YAML string representation, written at column `indent`:
408    /// a block mapping or sequence has every line indented by `indent`, and
409    /// anything else has the lines after its first, such as the body of a
410    /// block scalar, indented past it.
411    ///
412    /// The text has no final line break, except where a block scalar ends
413    /// it with an empty line: that line needs its line break, or the value
414    /// would read back without it.
415    pub fn to_yaml_string(&self, indent: usize) -> String {
416        let mut builder = rowan::GreenNodeBuilder::new();
417        builder.start_node(crate::lex::SyntaxKind::ROOT.into());
418        build_value(self, &mut builder, indent, None);
419        builder.finish_node();
420        let built = SyntaxNode::new_root(builder.finish()).text().to_string();
421        let mut text = if self.is_block_collection() {
422            " ".repeat(indent)
423        } else {
424            String::new()
425        };
426        text.push_str(built.strip_suffix('\n').unwrap_or(&built));
427        if text.ends_with('\n') {
428            text.push('\n');
429        }
430        text
431    }
432
433    /// Whether this is written as a block collection, which has to start
434    /// on a line of its own.
435    fn is_block_collection(&self) -> bool {
436        match self {
437            YamlValue::Sequence(seq) => !seq.is_empty(),
438            YamlValue::Mapping(map) => !map.is_empty(),
439            _ => false,
440        }
441    }
442}
443
444impl fmt::Display for YamlValue {
445    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
446        write!(f, "{}", self.to_yaml_string(0))
447    }
448}
449
450// Convenience conversions from common types
451macro_rules! impl_from_scalar {
452    ($($t:ty),*) => {
453        $(
454            impl From<$t> for YamlValue {
455                fn from(value: $t) -> Self {
456                    YamlValue::Scalar(ScalarValue::from(value))
457                }
458            }
459        )*
460    };
461}
462
463impl_from_scalar!(String, &str, i32, i64, f32, f64, bool);
464
465impl From<ScalarValue> for YamlValue {
466    fn from(value: ScalarValue) -> Self {
467        YamlValue::Scalar(value)
468    }
469}
470
471impl From<&YamlValue> for YamlValue {
472    fn from(value: &YamlValue) -> Self {
473        value.clone()
474    }
475}
476
477impl<T> From<Vec<T>> for YamlValue
478where
479    T: Into<YamlValue>,
480{
481    fn from(vec: Vec<T>) -> Self {
482        let values: Vec<_> = vec.into_iter().map(Into::into).collect();
483        YamlValue::Sequence(values)
484    }
485}
486
487impl<K, V> From<BTreeMap<K, V>> for YamlValue
488where
489    K: Into<String>,
490    V: Into<YamlValue>,
491{
492    fn from(map: BTreeMap<K, V>) -> Self {
493        YamlValue::Mapping(map.into_iter().map(|(k, v)| (k.into(), v.into())).collect())
494    }
495}
496
497impl<T> From<BTreeSet<T>> for YamlValue
498where
499    T: Into<String>,
500{
501    fn from(set: BTreeSet<T>) -> Self {
502        YamlValue::Set(set.into_iter().map(Into::into).collect())
503    }
504}
505
506// AsYaml trait implementation for YamlValue
507use crate::as_yaml::{
508    build_in, build_mapping, build_sequence, build_tagged, AsYaml, FlowQuoting, YamlKind,
509};
510use crate::yaml::SyntaxNode;
511
512/// Emit a `!!omap` / `!!pairs` TAGGED_NODE wrapping a sequence of
513/// single-entry mappings. Returns whether the content ended with a newline.
514fn build_tagged_pair_seq(
515    builder: &mut rowan::GreenNodeBuilder,
516    tag: &str,
517    pairs: &[(String, YamlValue)],
518    indent: usize,
519    flow: Option<FlowQuoting>,
520) -> bool {
521    let block = flow.is_none() && !pairs.is_empty();
522    build_tagged(builder, tag, block, indent, |builder| {
523        let items = pairs
524            .iter()
525            .map(|(k, v)| YamlValue::Mapping(BTreeMap::from([(k.clone(), v.clone())])));
526        build_sequence(builder, items, indent + 2, flow)
527    })
528}
529
530/// Emit `value`, inside a flow collection quoting strings as `flow` says
531/// if it is set.
532fn build_value(
533    value: &YamlValue,
534    builder: &mut rowan::GreenNodeBuilder,
535    indent: usize,
536    flow: Option<FlowQuoting>,
537) -> bool {
538    match value {
539        YamlValue::Scalar(s) => build_in(s, builder, indent, flow),
540        YamlValue::Mapping(map) => build_mapping(builder, map.iter(), indent, flow),
541        YamlValue::Sequence(seq) => build_sequence(builder, seq.iter(), indent, flow),
542        YamlValue::Set(set) => crate::as_yaml::build_set_content(
543            builder,
544            set.iter().map(|m| ScalarValue::string(m.as_str())),
545            indent,
546            flow,
547        ),
548        YamlValue::OrderedMapping(pairs) => {
549            build_tagged_pair_seq(builder, crate::tag::OMAP, pairs, indent, flow)
550        }
551        YamlValue::Pairs(pairs) => {
552            build_tagged_pair_seq(builder, crate::tag::PAIRS, pairs, indent, flow)
553        }
554        YamlValue::Tagged(tag, value) => {
555            let block = value.is_block_collection() && flow.is_none();
556            let content_indent = if block { indent + 2 } else { indent };
557            build_tagged(builder, tag, block, indent, |builder| {
558                build_value(value, builder, content_indent, flow)
559            })
560        }
561    }
562}
563
564impl AsYaml for YamlValue {
565    fn as_node(&self) -> Option<&SyntaxNode> {
566        None
567    }
568
569    fn kind(&self) -> YamlKind {
570        match self {
571            YamlValue::Scalar(_) => YamlKind::Scalar,
572            YamlValue::Sequence(_) => YamlKind::Sequence,
573            YamlValue::Mapping(_) => YamlKind::Mapping,
574            YamlValue::Set(_) => YamlKind::Tagged(std::borrow::Cow::Borrowed(crate::tag::SET)),
575            YamlValue::OrderedMapping(_) => {
576                YamlKind::Tagged(std::borrow::Cow::Borrowed(crate::tag::OMAP))
577            }
578            YamlValue::Pairs(_) => YamlKind::Tagged(std::borrow::Cow::Borrowed(crate::tag::PAIRS)),
579            YamlValue::Tagged(tag, _) => YamlKind::Tagged(std::borrow::Cow::Owned(tag.clone())),
580        }
581    }
582
583    fn build_content(
584        &self,
585        builder: &mut rowan::GreenNodeBuilder,
586        indent: usize,
587        flow_context: bool,
588    ) -> bool {
589        build_value(
590            self,
591            builder,
592            indent,
593            flow_context.then_some(FlowQuoting::Minimal),
594        )
595    }
596
597    fn build_flow_content(
598        &self,
599        builder: &mut rowan::GreenNodeBuilder,
600        indent: usize,
601        quoting: FlowQuoting,
602    ) -> bool {
603        build_value(self, builder, indent, Some(quoting))
604    }
605
606    fn is_inline(&self) -> bool {
607        match self {
608            YamlValue::Scalar(_) => true,
609            YamlValue::Mapping(map) => map.is_empty(),
610            YamlValue::Sequence(seq) => seq.is_empty(),
611            // For programmatically created tagged collections, we use the common style:
612            // tag inline with key (e.g., "tags: !!set"), even when non-empty.
613            // Note: Parsed YAML may have tags on separate lines, which is also valid,
614            // but for YamlValue we choose this as the default formatting.
615            YamlValue::Set(_) => true,
616            YamlValue::OrderedMapping(_) => true,
617            YamlValue::Pairs(_) => true,
618            YamlValue::Tagged(_, _) => true,
619        }
620    }
621}
622
623impl AsYaml for BTreeMap<String, YamlValue> {
624    fn as_node(&self) -> Option<&SyntaxNode> {
625        None
626    }
627
628    fn kind(&self) -> YamlKind {
629        YamlKind::Mapping
630    }
631
632    fn build_content(
633        &self,
634        builder: &mut rowan::GreenNodeBuilder,
635        indent: usize,
636        flow_context: bool,
637    ) -> bool {
638        build_mapping(
639            builder,
640            self.iter(),
641            indent,
642            flow_context.then_some(FlowQuoting::Minimal),
643        )
644    }
645
646    fn build_flow_content(
647        &self,
648        builder: &mut rowan::GreenNodeBuilder,
649        indent: usize,
650        quoting: FlowQuoting,
651    ) -> bool {
652        build_mapping(builder, self.iter(), indent, Some(quoting))
653    }
654
655    fn is_inline(&self) -> bool {
656        self.is_empty()
657    }
658}
659
660impl AsYaml for Vec<YamlValue> {
661    fn as_node(&self) -> Option<&SyntaxNode> {
662        None
663    }
664
665    fn kind(&self) -> YamlKind {
666        YamlKind::Sequence
667    }
668
669    fn build_content(
670        &self,
671        builder: &mut rowan::GreenNodeBuilder,
672        indent: usize,
673        flow_context: bool,
674    ) -> bool {
675        build_sequence(
676            builder,
677            self.iter(),
678            indent,
679            flow_context.then_some(FlowQuoting::Minimal),
680        )
681    }
682
683    fn build_flow_content(
684        &self,
685        builder: &mut rowan::GreenNodeBuilder,
686        indent: usize,
687        quoting: FlowQuoting,
688    ) -> bool {
689        build_sequence(builder, self.iter(), indent, Some(quoting))
690    }
691
692    fn is_inline(&self) -> bool {
693        <&[YamlValue] as AsYaml>::is_inline(&self.as_slice())
694    }
695}
696
697impl AsYaml for &[YamlValue] {
698    fn as_node(&self) -> Option<&SyntaxNode> {
699        None
700    }
701
702    fn kind(&self) -> YamlKind {
703        YamlKind::Sequence
704    }
705
706    fn build_content(
707        &self,
708        builder: &mut rowan::GreenNodeBuilder,
709        indent: usize,
710        flow_context: bool,
711    ) -> bool {
712        build_sequence(
713            builder,
714            self.iter(),
715            indent,
716            flow_context.then_some(FlowQuoting::Minimal),
717        )
718    }
719
720    fn build_flow_content(
721        &self,
722        builder: &mut rowan::GreenNodeBuilder,
723        indent: usize,
724        quoting: FlowQuoting,
725    ) -> bool {
726        build_sequence(builder, self.iter(), indent, Some(quoting))
727    }
728
729    fn is_inline(&self) -> bool {
730        self.is_empty()
731    }
732}
733
734#[cfg(test)]
735mod tests {
736    use super::*;
737
738    use std::str::FromStr;
739
740    /// [`YamlValue::cast`] on the content of the single document in `yaml`.
741    fn cast_root(yaml: &str) -> Result<Option<YamlValue>, crate::anchor_resolution::ResolveError> {
742        use rowan::ast::AstNode;
743        let doc = crate::Document::from_str(yaml).unwrap();
744        let content = doc
745            .syntax()
746            .children()
747            .find(|n| crate::as_yaml::YamlNode::from_syntax_peeled(n.clone()).is_some())
748            .unwrap();
749        YamlValue::cast(content)
750    }
751
752    #[test]
753    fn test_cast_alias_is_error() {
754        use crate::anchor_resolution::ResolveError;
755        for yaml in [
756            "x: &x 1\ny: [*x, 2]\n",
757            "x: &x 1\ny: *x\n",
758            "&x a: 1\n*x : 2\n",
759            "x: &x a\ny: !!set {? *x}\n",
760            "x: &x {a: 1}\ny: !!omap [*x]\n",
761            "x: &x 1\ny: !!pairs [a: *x]\n",
762            "x: &x 1\ny: !local [*x]\n",
763            "x: &x {a: 1}\ny: {<<: *x}\n",
764        ] {
765            assert_eq!(
766                cast_root(yaml),
767                Err(ResolveError::UnresolvedAlias {
768                    alias: "x".to_string()
769                }),
770                "{yaml:?}"
771            );
772        }
773        assert_eq!(
774            ResolveError::UnresolvedAlias {
775                alias: "x".to_string()
776            }
777            .to_string(),
778            "alias \"x\" cannot be converted without resolving it; \
779             use get_resolved to resolve aliases"
780        );
781    }
782
783    #[test]
784    fn test_cast_key_collision_is_error() {
785        use crate::anchor_resolution::ResolveError;
786        assert_eq!(
787            cast_root("1: a\n'1': b\n"),
788            Err(ResolveError::KeyCollision {
789                first: "1".to_string(),
790                second: "'1'".to_string()
791            })
792        );
793    }
794
795    #[test]
796    fn test_cast_invalid_special_collections() {
797        use crate::anchor_resolution::ResolveError;
798        assert_eq!(
799            cast_root("!!set {? [a], ? b}\n"),
800            Err(ResolveError::InvalidSetMember {
801                member: "[a]".to_string()
802            })
803        );
804        assert_eq!(
805            cast_root("!!set [a, b]\n"),
806            Err(ResolveError::InvalidSetContent {
807                content: "[a, b]".to_string()
808            })
809        );
810        assert_eq!(
811            cast_root("!!omap {a: 1}\n"),
812            Err(ResolveError::InvalidPairsContent {
813                tag: "!!omap".to_string(),
814                content: "{a: 1}".to_string()
815            })
816        );
817        // A collection tag with no content tags an empty scalar.
818        for (yaml, expected) in [
819            (
820                "!!set\n",
821                ResolveError::InvalidSetContent {
822                    content: String::new(),
823                },
824            ),
825            (
826                "!!omap\n",
827                ResolveError::InvalidPairsContent {
828                    tag: "!!omap".to_string(),
829                    content: String::new(),
830                },
831            ),
832            (
833                "- !!pairs\n- a: 1\n",
834                ResolveError::InvalidPairsContent {
835                    tag: "!!pairs".to_string(),
836                    content: String::new(),
837                },
838            ),
839        ] {
840            let doc = crate::Document::from_str(yaml).unwrap();
841            assert_eq!(YamlValue::from_document(&doc), Err(expected), "{yaml:?}");
842        }
843        for (yaml, tag, item) in [
844            ("!!omap [a, d: 3]\n", "!!omap", "a"),
845            ("!!pairs [{b: 1, c: 2}]\n", "!!pairs", "{b: 1, c: 2}"),
846        ] {
847            assert_eq!(
848                cast_root(yaml),
849                Err(ResolveError::InvalidPairsItem {
850                    tag: tag.to_string(),
851                    item: item.to_string()
852                }),
853                "{yaml:?}"
854            );
855        }
856    }
857
858    #[test]
859    fn test_cast_duplicate_key_is_error() {
860        use crate::anchor_resolution::ResolveError;
861        let expected = ResolveError::DuplicateKey {
862            first: "a".to_string(),
863            second: "a".to_string(),
864        };
865        let yaml = "a: 1\nb: 2\na: 3\n";
866        assert_eq!(cast_root(yaml), Err(expected.clone()));
867        let doc = crate::Document::from_str(yaml).unwrap();
868        assert_eq!(YamlValue::from_document(&doc), Err(expected.clone()));
869        match YamlValue::parse_raw(yaml) {
870            Err(crate::YamlError::Resolve(err)) => assert_eq!(err, expected),
871            other => panic!("unexpected result: {other:?}"),
872        }
873        // Without a registry `<<` is an ordinary key, so two of them are
874        // duplicates.
875        assert_eq!(
876            cast_root("<<: {a: 1}\n<<: {b: 2}\n"),
877            Err(ResolveError::DuplicateKey {
878                first: "<<".to_string(),
879                second: "<<".to_string(),
880            })
881        );
882    }
883
884    #[test]
885    fn test_cast_keeps_merge_key() {
886        assert_eq!(
887            cast_root("<<: {a: 1}\nb: 2\n"),
888            Ok(Some(YamlValue::Mapping(BTreeMap::from([
889                (
890                    "<<".to_string(),
891                    YamlValue::Mapping(BTreeMap::from([("a".to_string(), YamlValue::from(1))]))
892                ),
893                ("b".to_string(), YamlValue::from(2)),
894            ]))))
895        );
896    }
897
898    #[test]
899    fn test_tagged_constructor() {
900        let v = YamlValue::tagged("!!str", YamlValue::from(1));
901        assert_eq!(v, YamlValue::Scalar(ScalarValue::from(1).with_tag("!!str")));
902        assert_eq!(v.tag(), Some("!!str"));
903        assert_eq!(YamlValue::from(1).tag(), None);
904        assert_eq!(YamlValue::set().tag(), Some("!!set"));
905    }
906
907    #[test]
908    fn test_predicates_and_accessors() {
909        let scalar = YamlValue::from("s");
910        let seq = YamlValue::Sequence(vec![YamlValue::from("a")]);
911        let map = {
912            let mut m = BTreeMap::new();
913            m.insert("k".to_string(), YamlValue::from("v"));
914            YamlValue::Mapping(m)
915        };
916        let set = YamlValue::from_set(BTreeSet::from(["x".to_string()]));
917        let omap = YamlValue::from_ordered_mapping(vec![("a".to_string(), YamlValue::from("b"))]);
918        let pairs = YamlValue::from_pairs(vec![("a".to_string(), YamlValue::from("b"))]);
919
920        // Each predicate is true only for its own variant.
921        assert!(scalar.is_scalar());
922        assert!(!seq.is_scalar());
923
924        assert!(seq.is_sequence());
925        assert!(!scalar.is_sequence());
926
927        assert!(map.is_mapping());
928        assert!(!seq.is_mapping());
929
930        assert!(set.is_set());
931        assert!(!map.is_set());
932
933        assert!(omap.is_ordered_mapping());
934        assert!(!map.is_ordered_mapping());
935
936        assert!(pairs.is_pairs());
937        assert!(!omap.is_pairs());
938
939        // Accessors return Some only for the matching variant, None otherwise.
940        assert!(scalar.as_scalar().is_some());
941        assert!(seq.as_scalar().is_none());
942
943        assert_eq!(seq.as_sequence(), Some([YamlValue::from("a")].as_slice()));
944        assert_eq!(scalar.as_sequence(), None);
945
946        assert_eq!(map.as_mapping().map(|m| m.len()), Some(1));
947        assert_eq!(scalar.as_mapping(), None);
948
949        assert_eq!(set.as_set().map(|s| s.len()), Some(1));
950        assert_eq!(map.as_set(), None);
951
952        assert_eq!(omap.as_ordered_mapping().map(|p| p.len()), Some(1));
953        assert_eq!(map.as_ordered_mapping(), None);
954
955        assert_eq!(pairs.as_pairs().map(|p| p.len()), Some(1));
956        assert_eq!(omap.as_pairs(), None);
957    }
958
959    #[test]
960    fn test_accessors_mut() {
961        let mut seq = YamlValue::Sequence(vec![YamlValue::from("a")]);
962        seq.as_sequence_mut().unwrap().push(YamlValue::from("b"));
963        assert_eq!(seq.as_sequence().map(|s| s.len()), Some(2));
964        assert!(YamlValue::from("x").as_sequence_mut().is_none());
965
966        let mut map = YamlValue::mapping();
967        map.as_mapping_mut()
968            .unwrap()
969            .insert("k".to_string(), YamlValue::from("v"));
970        assert_eq!(map.as_mapping().map(|m| m.len()), Some(1));
971        assert!(YamlValue::from("x").as_mapping_mut().is_none());
972
973        let mut set = YamlValue::set();
974        set.as_set_mut().unwrap().insert("a".to_string());
975        assert_eq!(set.as_set().map(|s| s.len()), Some(1));
976        assert!(YamlValue::from("x").as_set_mut().is_none());
977
978        let mut omap = YamlValue::ordered_mapping();
979        omap.as_ordered_mapping_mut()
980            .unwrap()
981            .push(("a".to_string(), YamlValue::from("b")));
982        assert_eq!(omap.as_ordered_mapping().map(|p| p.len()), Some(1));
983        assert!(YamlValue::from("x").as_ordered_mapping_mut().is_none());
984
985        let mut pairs = YamlValue::pairs();
986        pairs
987            .as_pairs_mut()
988            .unwrap()
989            .push(("a".to_string(), YamlValue::from("b")));
990        assert_eq!(pairs.as_pairs().map(|p| p.len()), Some(1));
991        assert!(YamlValue::from("x").as_pairs_mut().is_none());
992    }
993
994    #[test]
995    fn test_as_str() {
996        // Test with different scalar types - all should return Some
997        let string_val = YamlValue::from("hello");
998        assert_eq!(string_val.as_str(), Some("hello"));
999
1000        let int_val = YamlValue::from(42);
1001        assert_eq!(int_val.as_str(), Some("42"));
1002
1003        let float_val = YamlValue::from(1.5);
1004        assert_eq!(float_val.as_str(), Some("1.5"));
1005
1006        let bool_val = YamlValue::from(true);
1007        assert_eq!(bool_val.as_str(), Some("true"));
1008
1009        // Test with non-scalar types - should return None
1010        let mapping = YamlValue::mapping();
1011        assert_eq!(mapping.as_str(), None);
1012
1013        let sequence = YamlValue::sequence();
1014        assert_eq!(sequence.as_str(), None);
1015
1016        let set = YamlValue::set();
1017        assert_eq!(set.as_str(), None);
1018    }
1019
1020    #[test]
1021    fn test_to_i64() {
1022        use crate::scalar::ScalarValue;
1023
1024        // Unquoted integers should parse
1025        let int_val = YamlValue::Scalar(ScalarValue::parse("42"));
1026        assert_eq!(int_val.to_i64(), Some(42));
1027
1028        let neg_int = YamlValue::Scalar(ScalarValue::parse("-123"));
1029        assert_eq!(neg_int.to_i64(), Some(-123));
1030
1031        // Hexadecimal
1032        let hex_val = YamlValue::Scalar(ScalarValue::parse("0x2A"));
1033        assert_eq!(hex_val.to_i64(), Some(42));
1034
1035        // Binary is not a YAML 1.2 integer form
1036        let bin_val = YamlValue::Scalar(ScalarValue::parse("0b101010"));
1037        assert_eq!(bin_val.to_i64(), None);
1038
1039        // Octal
1040        let oct_val = YamlValue::Scalar(ScalarValue::parse("0o52"));
1041        assert_eq!(oct_val.to_i64(), Some(42));
1042
1043        // Quoted string "42" is a string, not an integer
1044        let string_val = YamlValue::from("42");
1045        assert_eq!(string_val.to_i64(), None);
1046
1047        // Boolean values should not convert to integers
1048        let bool_val = YamlValue::Scalar(ScalarValue::parse("true"));
1049        assert_eq!(bool_val.to_i64(), None);
1050
1051        let no_val = YamlValue::Scalar(ScalarValue::parse("no"));
1052        assert_eq!(no_val.to_i64(), None);
1053
1054        // Float values should not convert to integers
1055        let float_val = YamlValue::Scalar(ScalarValue::parse("3.14"));
1056        assert_eq!(float_val.to_i64(), None);
1057
1058        // Non-scalar types should return None
1059        let mapping = YamlValue::mapping();
1060        assert_eq!(mapping.to_i64(), None);
1061
1062        let sequence = YamlValue::sequence();
1063        assert_eq!(sequence.to_i64(), None);
1064
1065        // Random strings should not convert
1066        let random_string = YamlValue::from("blah");
1067        assert_eq!(random_string.to_i64(), None);
1068
1069        // Large integers
1070        let large_int = YamlValue::Scalar(ScalarValue::parse("23432"));
1071        assert_eq!(large_int.to_i64(), Some(23432));
1072
1073        // Numbers that look like booleans but are typed as integers should work
1074        let zero = YamlValue::Scalar(ScalarValue::parse("0"));
1075        assert_eq!(zero.to_i64(), Some(0));
1076
1077        let one = YamlValue::Scalar(ScalarValue::parse("1"));
1078        assert_eq!(one.to_i64(), Some(1));
1079
1080        // But quoted "1" and "0" are strings
1081        let quoted_one = YamlValue::from("1");
1082        assert_eq!(quoted_one.to_i64(), None);
1083
1084        let quoted_zero = YamlValue::from("0");
1085        assert_eq!(quoted_zero.to_i64(), None);
1086    }
1087
1088    #[test]
1089    fn test_to_f64() {
1090        use crate::scalar::ScalarValue;
1091
1092        // Unquoted floats should parse
1093        let float_val = YamlValue::Scalar(ScalarValue::parse("1.5"));
1094        assert_eq!(float_val.to_f64(), Some(1.5));
1095
1096        let neg_float = YamlValue::Scalar(ScalarValue::parse("-2.5"));
1097        assert_eq!(neg_float.to_f64(), Some(-2.5));
1098
1099        // Scientific notation
1100        let sci_val = YamlValue::Scalar(ScalarValue::parse("1.23e10"));
1101        assert_eq!(sci_val.to_f64(), Some(1.23e10));
1102
1103        // Quoted string "3.14" is a string, not a float
1104        let string_val = YamlValue::from("3.14");
1105        assert_eq!(string_val.to_f64(), None);
1106
1107        // Integer values should not convert to floats (type mismatch)
1108        let int_val = YamlValue::Scalar(ScalarValue::parse("42"));
1109        assert_eq!(int_val.to_f64(), None);
1110
1111        // Boolean values should not convert
1112        let bool_val = YamlValue::Scalar(ScalarValue::parse("true"));
1113        assert_eq!(bool_val.to_f64(), None);
1114
1115        // Non-scalar types should return None
1116        let mapping = YamlValue::mapping();
1117        assert_eq!(mapping.to_f64(), None);
1118
1119        let sequence = YamlValue::sequence();
1120        assert_eq!(sequence.to_f64(), None);
1121
1122        // Random strings should not convert
1123        let random_string = YamlValue::from("blah");
1124        assert_eq!(random_string.to_f64(), None);
1125    }
1126
1127    #[test]
1128    fn test_to_bool() {
1129        use crate::scalar::ScalarValue;
1130
1131        // Unquoted boolean values should parse
1132        let true_val = YamlValue::Scalar(ScalarValue::parse("true"));
1133        assert_eq!(true_val.to_bool(), Some(true));
1134
1135        let false_val = YamlValue::Scalar(ScalarValue::parse("false"));
1136        assert_eq!(false_val.to_bool(), Some(false));
1137
1138        // YAML 1.1 spellings are strings in YAML 1.2
1139        for text in ["yes", "no", "on", "off", "Yes", "tRUE"] {
1140            let value = YamlValue::Scalar(ScalarValue::parse(text));
1141            assert_eq!(value.to_bool(), None, "{text}");
1142        }
1143
1144        let true_upper = YamlValue::Scalar(ScalarValue::parse("TRUE"));
1145        assert_eq!(true_upper.to_bool(), Some(true));
1146
1147        let false_title = YamlValue::Scalar(ScalarValue::parse("False"));
1148        assert_eq!(false_title.to_bool(), Some(false));
1149
1150        // Quoted strings are NOT booleans
1151        let quoted_true = YamlValue::from("true");
1152        assert_eq!(quoted_true.to_bool(), None);
1153
1154        let quoted_false = YamlValue::from("false");
1155        assert_eq!(quoted_false.to_bool(), None);
1156
1157        let quoted_yes = YamlValue::from("yes");
1158        assert_eq!(quoted_yes.to_bool(), None);
1159
1160        let quoted_no = YamlValue::from("no");
1161        assert_eq!(quoted_no.to_bool(), None);
1162
1163        // Integers are NOT booleans (even 0 and 1)
1164        let zero = YamlValue::Scalar(ScalarValue::parse("0"));
1165        assert_eq!(zero.to_bool(), None);
1166
1167        let one = YamlValue::Scalar(ScalarValue::parse("1"));
1168        assert_eq!(one.to_bool(), None);
1169
1170        let large_int = YamlValue::Scalar(ScalarValue::parse("23432"));
1171        assert_eq!(large_int.to_bool(), None);
1172
1173        // Quoted "1" and "0" are strings, not booleans
1174        let quoted_one = YamlValue::from("1");
1175        assert_eq!(quoted_one.to_bool(), None);
1176
1177        let quoted_zero = YamlValue::from("0");
1178        assert_eq!(quoted_zero.to_bool(), None);
1179
1180        // Random strings should not convert
1181        let random_string = YamlValue::from("blah");
1182        assert_eq!(random_string.to_bool(), None);
1183
1184        // Non-scalar types should return None
1185        let mapping = YamlValue::mapping();
1186        assert_eq!(mapping.to_bool(), None);
1187
1188        let sequence = YamlValue::sequence();
1189        assert_eq!(sequence.to_bool(), None);
1190
1191        // Float values are not booleans
1192        let float_val = YamlValue::Scalar(ScalarValue::parse("3.14"));
1193        assert_eq!(float_val.to_bool(), None);
1194    }
1195
1196    #[test]
1197    fn test_tagged_alias_to_tagged_alias() {
1198        use std::str::FromStr;
1199        let doc = crate::Document::from_str("b: &b 1\na: &a !u *b\nk: !t *a\n").unwrap();
1200        let value = YamlValue::from_document(&doc).unwrap();
1201        let YamlValue::Mapping(map) = value else {
1202            panic!("expected a mapping");
1203        };
1204        assert_eq!(
1205            map["k"],
1206            YamlValue::Scalar(ScalarValue::parse("1").with_resolved_tag("!t".to_string()))
1207        );
1208        assert_eq!(map["k"].tag(), Some("!t"));
1209    }
1210
1211    #[test]
1212    fn test_omap_items_tagged_map() {
1213        use std::str::FromStr;
1214        let doc = crate::Document::from_str("o: !!omap [!!map {a: 1}, {b: 2}]\n").unwrap();
1215        let expected = YamlValue::OrderedMapping(vec![
1216            ("a".to_string(), YamlValue::from(1i64)),
1217            ("b".to_string(), YamlValue::from(2i64)),
1218        ]);
1219        let o = doc.as_mapping().unwrap().get("o").unwrap();
1220        assert_eq!(
1221            YamlValue::cast(o.syntax().clone()),
1222            Ok(Some(expected.clone()))
1223        );
1224        assert_eq!(
1225            YamlValue::from_document(&doc),
1226            Ok(YamlValue::from_mapping(BTreeMap::from([(
1227                "o".to_string(),
1228                expected
1229            )])))
1230        );
1231
1232        let doc = crate::Document::from_str("o: !!omap [!!map [a]]\n").unwrap();
1233        assert_eq!(
1234            YamlValue::from_document(&doc),
1235            Err(crate::anchor_resolution::ResolveError::InvalidPairsItem {
1236                tag: "!!omap".to_string(),
1237                item: "!!map [a]".to_string(),
1238            })
1239        );
1240    }
1241}