Skip to main content

yaml_edit/scalar/
mod.rs

1//! Scalar value wrapper with proper escaping and style support.
2
3#[cfg(feature = "base64")]
4mod binary;
5mod decimal;
6mod quoting;
7mod render;
8mod resolve;
9
10pub(crate) use decimal::{digits_to_decimal, places_to_decimal, signed_digits_to_decimal};
11
12use crate::as_yaml::FlowQuoting;
13use std::fmt;
14
15#[cfg(feature = "base64")]
16use binary::base64_decode;
17
18/// Style of scalar representation in YAML
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum ScalarStyle {
21    /// Plain scalar (no quotes)
22    Plain,
23    /// Single-quoted scalar
24    SingleQuoted,
25    /// Double-quoted scalar
26    DoubleQuoted,
27    /// Literal scalar (|)
28    Literal,
29    /// Folded scalar (>)
30    Folded,
31}
32
33/// The five scalar types recognised by the YAML 1.2 core schema.
34///
35/// Returned by [`ScalarValue::classify_plain`], which is the single
36/// source of truth for plain-scalar tag resolution. This is a strict
37/// subset of [`ScalarType`], which additionally covers yaml-edit's
38/// out-of-band types (`Timestamp`, `Binary`, `Regex`) used when
39/// constructing new values.
40#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
41pub enum CoreScalarType {
42    /// `!!null`
43    Null,
44    /// `!!bool`
45    Boolean,
46    /// `!!int`
47    Integer,
48    /// `!!float`
49    Float,
50    /// `!!str` (the fallback when no other tag matches)
51    String,
52}
53
54impl From<CoreScalarType> for ScalarType {
55    fn from(t: CoreScalarType) -> Self {
56        match t {
57            CoreScalarType::Null => ScalarType::Null,
58            CoreScalarType::Boolean => ScalarType::Boolean,
59            CoreScalarType::Integer => ScalarType::Integer,
60            CoreScalarType::Float => ScalarType::Float,
61            CoreScalarType::String => ScalarType::String,
62        }
63    }
64}
65
66/// The c-indicator characters (YAML 1.2 5.3), which cannot start a plain
67/// scalar, except for `-`, `?` and `:` followed by a safe character.
68pub(crate) const INDICATORS: &[char] = &[
69    '-', '?', ':', ',', '[', ']', '{', '}', '#', '&', '*', '!', '|', '>', '\'', '"', '%', '@', '`',
70];
71
72/// Type of a scalar value
73#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
74pub enum ScalarType {
75    /// String value
76    String,
77    /// Integer value
78    Integer,
79    /// Float value
80    Float,
81    /// Boolean value
82    Boolean,
83    /// Null value
84    Null,
85    /// Binary data (base64 encoded)
86    #[cfg(feature = "base64")]
87    Binary,
88    /// Timestamp value
89    Timestamp,
90    /// Regular expression
91    Regex,
92}
93
94/// A scalar value with metadata about its style and content
95#[derive(Debug, Clone, PartialEq, Eq)]
96pub struct ScalarValue {
97    /// The actual value
98    value: String,
99    /// The style to use when rendering
100    style: ScalarStyle,
101    /// The type of the scalar
102    scalar_type: ScalarType,
103    /// The explicit tag written on the node, e.g. `!!float` or `!local`
104    tag: Option<String>,
105}
106
107/// `s` without leading and trailing YAML white space, which is only spaces
108/// and tabs: other Unicode white space, such as a no-break space, is content.
109pub(crate) fn trim_white(s: &str) -> &str {
110    s.trim_matches([' ', '\t'])
111}
112
113impl ScalarValue {
114    /// Create a scalar value explicitly treating it as a string (no type auto-detection)
115    ///
116    /// This method always creates a `String` type scalar. The value will be properly
117    /// quoted if needed when rendering to YAML, but no type detection is performed.
118    ///
119    /// # Examples
120    ///
121    /// ```
122    /// use yaml_edit::ScalarValue;
123    ///
124    /// let scalar = ScalarValue::string("123");
125    /// assert_eq!(scalar.scalar_type(), yaml_edit::ScalarType::String);
126    /// // Renders with quotes to distinguish from integer
127    /// assert_eq!(scalar.to_yaml_string(), "'123'");
128    ///
129    /// let scalar = ScalarValue::string("true");
130    /// assert_eq!(scalar.scalar_type(), yaml_edit::ScalarType::String);
131    /// // Renders with quotes to distinguish from boolean
132    /// assert_eq!(scalar.to_yaml_string(), "'true'");
133    ///
134    /// let scalar = ScalarValue::string("hello");
135    /// assert_eq!(scalar.scalar_type(), yaml_edit::ScalarType::String);
136    /// // Plain strings don't need quotes
137    /// assert_eq!(scalar.to_yaml_string(), "hello");
138    /// ```
139    ///
140    /// For YAML-style type detection (parsing "123" as Integer, "true" as Boolean),
141    /// use [`ScalarValue::parse()`] instead.
142    pub fn string(value: impl Into<String>) -> Self {
143        let value = value.into();
144        let style = Self::detect_style(&value);
145        // Detect the type - default to String for user-provided values
146        let scalar_type = ScalarType::String;
147        Self {
148            value,
149            style,
150            scalar_type,
151            tag: None,
152        }
153    }
154
155    /// Create a new scalar with a specific style
156    pub fn with_style(value: impl Into<String>, style: ScalarStyle) -> Self {
157        Self {
158            value: value.into(),
159            style,
160            scalar_type: ScalarType::String,
161            tag: None,
162        }
163    }
164
165    /// Create a plain scalar
166    pub fn plain(value: impl Into<String>) -> Self {
167        Self::with_style(value, ScalarStyle::Plain)
168    }
169
170    /// Create a single-quoted scalar
171    pub fn single_quoted(value: impl Into<String>) -> Self {
172        Self::with_style(value, ScalarStyle::SingleQuoted)
173    }
174
175    /// Create a double-quoted scalar
176    pub fn double_quoted(value: impl Into<String>) -> Self {
177        Self::with_style(value, ScalarStyle::DoubleQuoted)
178    }
179
180    /// Create a literal scalar
181    pub fn literal(value: impl Into<String>) -> Self {
182        Self::with_style(value, ScalarStyle::Literal)
183    }
184
185    /// Create a folded scalar
186    pub fn folded(value: impl Into<String>) -> Self {
187        Self::with_style(value, ScalarStyle::Folded)
188    }
189
190    /// Create a null scalar
191    pub fn null() -> Self {
192        Self {
193            value: "null".to_string(),
194            style: ScalarStyle::Plain,
195            scalar_type: ScalarType::Null,
196            tag: None,
197        }
198    }
199
200    /// Create a timestamp scalar
201    pub fn timestamp(value: impl Into<String>) -> Self {
202        Self {
203            value: value.into(),
204            style: ScalarStyle::Plain,
205            scalar_type: ScalarType::Timestamp,
206            tag: None,
207        }
208    }
209
210    /// Create a regex scalar
211    pub fn regex(pattern: impl Into<String>) -> Self {
212        Self {
213            value: pattern.into(),
214            style: ScalarStyle::Plain,
215            scalar_type: ScalarType::Regex,
216            tag: None,
217        }
218    }
219
220    /// Return this scalar with an explicit tag, such as `!!str` or `!local`.
221    ///
222    /// The tag is rendered in front of the content. It does not change the
223    /// scalar's [`ScalarType`].
224    ///
225    /// ```
226    /// use yaml_edit::ScalarValue;
227    ///
228    /// let scalar = ScalarValue::from(1.0).with_tag("!!float");
229    /// assert_eq!(scalar.tag(), Some("!!float"));
230    /// assert_eq!(scalar.to_yaml_string(), "!!float 1.0");
231    /// ```
232    pub fn with_tag(mut self, tag: impl Into<String>) -> Self {
233        self.tag = Some(tag.into());
234        self
235    }
236
237    /// The explicit tag on this scalar, if any.
238    pub fn tag(&self) -> Option<&str> {
239        self.tag.as_deref()
240    }
241
242    /// Get the raw value
243    pub fn value(&self) -> &str {
244        &self.value
245    }
246
247    /// Get the style
248    pub fn style(&self) -> ScalarStyle {
249        self.style
250    }
251
252    /// Get the scalar type
253    pub fn scalar_type(&self) -> ScalarType {
254        self.scalar_type
255    }
256
257    /// Try to parse this scalar as an `i64`.
258    ///
259    /// Returns `None` if the scalar type is not `Integer` or the value does
260    /// not fit in an `i64`. Integers are read as the YAML 1.2 core schema
261    /// defines them, so `0755` is seven hundred and fifty five; only
262    /// `0o755` is octal.
263    pub fn to_i64(&self) -> Option<i64> {
264        if self.scalar_type == ScalarType::Integer {
265            Self::parse_integer(trim_white(&self.value))
266        } else {
267            None
268        }
269    }
270
271    /// Same as [`to_i64`](Self::to_i64), which now follows YAML 1.2.
272    #[deprecated(since = "0.3.3", note = "to_i64 follows YAML 1.2; use it instead")]
273    pub fn to_i64_yaml_1_2(&self) -> Option<i64> {
274        self.to_i64()
275    }
276
277    /// Try to parse this scalar as an `f64`.
278    ///
279    /// Returns `None` if the scalar type is not `Float`.
280    pub fn to_f64(&self) -> Option<f64> {
281        if self.scalar_type == ScalarType::Float {
282            Self::parse_float(trim_white(&self.value))
283        } else {
284            None
285        }
286    }
287
288    /// Try to parse this scalar as a `bool`.
289    ///
290    /// Returns `None` if the scalar type is not `Boolean`.
291    /// Recognizes the YAML 1.2 core schema spellings: `true`, `True`,
292    /// `TRUE`, `false`, `False` and `FALSE`.
293    pub fn to_bool(&self) -> Option<bool> {
294        if self.scalar_type == ScalarType::Boolean {
295            Self::parse_bool(trim_white(&self.value))
296        } else {
297            None
298        }
299    }
300
301    /// Check if this is a timestamp scalar
302    pub fn is_timestamp(&self) -> bool {
303        self.scalar_type == ScalarType::Timestamp
304    }
305
306    /// Check if this is a regex scalar
307    pub fn is_regex(&self) -> bool {
308        self.scalar_type == ScalarType::Regex
309    }
310
311    /// Compile and return a Regex object if this is a regex scalar
312    ///
313    /// This method is only available when the `regex` feature is enabled.
314    /// Returns None if this is not a regex scalar or if the pattern is invalid.
315    ///
316    /// # Example
317    /// ```
318    /// # #[cfg(feature = "regex")]
319    /// # {
320    /// use yaml_edit::ScalarValue;
321    ///
322    /// let scalar = ScalarValue::regex(r"\d{3}-\d{4}");
323    /// let regex = scalar.as_regex().unwrap();
324    /// assert!(regex.is_match("555-1234"));
325    /// # }
326    /// ```
327    #[cfg(feature = "regex")]
328    pub fn as_regex(&self) -> Option<regex::Regex> {
329        if self.scalar_type == ScalarType::Regex {
330            regex::Regex::new(&self.value).ok()
331        } else {
332            None
333        }
334    }
335
336    /// Try to compile this scalar as a regex, regardless of its type
337    ///
338    /// This method is only available when the `regex` feature is enabled.
339    /// This will attempt to compile the scalar value as a regex pattern,
340    /// even if it's not marked with the !!regex tag.
341    ///
342    /// # Example
343    /// ```
344    /// # #[cfg(feature = "regex")]
345    /// # {
346    /// use yaml_edit::ScalarValue;
347    ///
348    /// let scalar = ScalarValue::string(r"\d+");  // Plain string scalar
349    /// let regex = scalar.try_as_regex().unwrap();
350    /// assert!(regex.is_match("123"));
351    /// # }
352    /// ```
353    #[cfg(feature = "regex")]
354    pub fn try_as_regex(&self) -> Result<regex::Regex, regex::Error> {
355        regex::Regex::new(&self.value)
356    }
357
358    /// Try to coerce this scalar to the specified type
359    pub fn coerce_to_type(&self, target_type: ScalarType) -> Option<ScalarValue> {
360        if self.scalar_type == target_type {
361            return Some(self.clone());
362        }
363
364        match target_type {
365            ScalarType::String => Some(ScalarValue {
366                value: self.value.clone(),
367                style: ScalarStyle::Plain,
368                scalar_type: ScalarType::String,
369                tag: None,
370            }),
371            ScalarType::Integer => {
372                Self::parse_integer(trim_white(&self.value)).map(ScalarValue::from)
373            }
374            ScalarType::Float => Self::parse_float(trim_white(&self.value)).map(ScalarValue::from),
375            ScalarType::Boolean => match self.value.to_lowercase().as_str() {
376                "true" | "yes" | "on" | "1" => Some(ScalarValue::from(true)),
377                "false" | "no" | "off" | "0" => Some(ScalarValue::from(false)),
378                _ => None,
379            },
380            ScalarType::Null => match self.value.to_lowercase().as_str() {
381                "null" | "~" | "" => Some(ScalarValue::null()),
382                _ => None,
383            },
384            #[cfg(feature = "base64")]
385            ScalarType::Binary => {
386                // Try to decode as base64 to verify it's valid binary data
387                if base64_decode(&self.value).is_ok() {
388                    Some(ScalarValue {
389                        value: self.value.clone(),
390                        style: ScalarStyle::Plain,
391                        scalar_type: ScalarType::Binary,
392                        tag: None,
393                    })
394                } else {
395                    None
396                }
397            }
398            ScalarType::Timestamp => {
399                // Basic timestamp format validation
400                if Self::is_valid_timestamp(&self.value) {
401                    Some(ScalarValue::timestamp(&self.value))
402                } else {
403                    None
404                }
405            }
406            ScalarType::Regex => {
407                // For regex, just convert the value
408                Some(ScalarValue::regex(&self.value))
409            }
410        }
411    }
412
413    /// Parse a YAML scalar value with automatic type detection
414    ///
415    /// This method automatically detects the YAML type based on the content:
416    /// - "123" → Integer
417    /// - "3.14" → Float
418    /// - "true" / "false" → Boolean
419    /// - "null" / "~" → Null
420    /// - etc.
421    ///
422    /// # Examples
423    ///
424    /// ```
425    /// use yaml_edit::{ScalarValue, ScalarType};
426    ///
427    /// let scalar = ScalarValue::parse("123");
428    /// assert_eq!(scalar.scalar_type(), ScalarType::Integer);
429    /// assert_eq!(scalar.to_yaml_string(), "123");
430    ///
431    /// let scalar = ScalarValue::parse("true");
432    /// assert_eq!(scalar.scalar_type(), ScalarType::Boolean);
433    /// assert_eq!(scalar.to_yaml_string(), "true");
434    ///
435    /// let scalar = ScalarValue::parse("hello");
436    /// assert_eq!(scalar.scalar_type(), ScalarType::String);
437    /// assert_eq!(scalar.to_yaml_string(), "hello");
438    /// ```
439    ///
440    /// To create a String-type scalar without auto-detection (e.g., to represent
441    /// the string "123" rather than the integer 123), use [`ScalarValue::string()`] instead.
442    pub fn parse(value: impl Into<String>) -> Self {
443        let value = value.into();
444        let scalar_type = Self::auto_detect_type(&value);
445        // For non-string types, use Plain style (no quotes)
446        // For string types, detect appropriate style
447        let style = match scalar_type {
448            ScalarType::String => Self::detect_style(&value),
449            // All other types use plain style
450            _ => ScalarStyle::Plain,
451        };
452
453        Self {
454            value,
455            style,
456            scalar_type,
457            tag: None,
458        }
459    }
460
461    /// Create a ScalarValue from a Scalar syntax node, preserving the type from the lexer
462    ///
463    /// This extracts type information directly from the token kind (INT, BOOL, FLOAT, etc.)
464    /// rather than guessing based on heuristics. This is the correct way to convert
465    /// parsed YAML into ScalarValue.
466    pub fn from_scalar(scalar: &crate::yaml::Scalar) -> Self {
467        let value = scalar.as_string();
468        let raw_text = scalar.value();
469
470        let style = if raw_text.starts_with('"') && raw_text.ends_with('"') {
471            ScalarStyle::DoubleQuoted
472        } else if raw_text.starts_with('\'') && raw_text.ends_with('\'') {
473            ScalarStyle::SingleQuoted
474        } else if scalar.is_block() && raw_text.starts_with('|') {
475            ScalarStyle::Literal
476        } else if scalar.is_block() {
477            ScalarStyle::Folded
478        } else {
479            ScalarStyle::Plain
480        };
481
482        // Quoted and block scalars are always !!str per YAML 1.2 tag
483        // resolution. Plain scalars go through the core-schema
484        // classifier, which stays correct even when the plain-scalar
485        // text spans multiple lexer tokens (e.g. multi-line plain
486        // scalars, which yield STRING + NEWLINE + INDENT + STRING).
487        // Trailing white space is trimmed because the lexer may absorb it
488        // into the scalar text; leading white space can't appear because
489        // it would have been emitted as INDENT.
490        let scalar_type = if style != ScalarStyle::Plain {
491            ScalarType::String
492        } else {
493            Self::classify_plain(raw_text.trim_end_matches([' ', '\t'])).into()
494        };
495
496        Self {
497            value,
498            style,
499            scalar_type,
500            tag: None,
501        }
502    }
503}
504
505impl fmt::Display for ScalarValue {
506    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
507        write!(f, "{}", self.to_yaml_string())
508    }
509}
510
511impl From<String> for ScalarValue {
512    fn from(value: String) -> Self {
513        Self::string(value)
514    }
515}
516
517impl From<&str> for ScalarValue {
518    fn from(value: &str) -> Self {
519        Self::string(value)
520    }
521}
522
523/// `From<$t>` for the numeric primitives, all of which render through
524/// `to_string()` and differ only in the YAML type they carry.
525macro_rules! impl_from_number {
526    ($($t:ty => $ty:expr),* $(,)?) => {
527        $(
528            impl From<$t> for ScalarValue {
529                fn from(value: $t) -> Self {
530                    Self {
531                        value: value.to_string(),
532                        style: ScalarStyle::Plain,
533                        scalar_type: $ty,
534                        tag: None,
535                    }
536                }
537            }
538        )*
539    };
540}
541
542impl_from_number! {
543    i32 => ScalarType::Integer,
544    i64 => ScalarType::Integer,
545}
546
547macro_rules! impl_from_float {
548    ($($t:ty),* $(,)?) => {
549        $(
550            impl From<$t> for ScalarValue {
551                fn from(value: $t) -> Self {
552                    Self {
553                        value: Self::format_float(value.to_string()),
554                        style: ScalarStyle::Plain,
555                        scalar_type: ScalarType::Float,
556                        tag: None,
557                    }
558                }
559            }
560        )*
561    };
562}
563
564impl_from_float!(f32, f64);
565
566impl From<bool> for ScalarValue {
567    fn from(value: bool) -> Self {
568        Self {
569            value: if value { "true" } else { "false" }.to_string(),
570            style: ScalarStyle::Plain,
571            scalar_type: ScalarType::Boolean,
572            tag: None,
573        }
574    }
575}
576
577/// Same as [`ScalarValue::from_scalar`].
578impl From<crate::yaml::Scalar> for ScalarValue {
579    fn from(scalar: crate::yaml::Scalar) -> Self {
580        Self::from_scalar(&scalar)
581    }
582}
583
584impl crate::AsYaml for ScalarValue {
585    fn as_node(&self) -> Option<&crate::yaml::SyntaxNode> {
586        None
587    }
588
589    fn kind(&self) -> crate::as_yaml::YamlKind {
590        match self.rendered_tag() {
591            Some(tag) => crate::as_yaml::YamlKind::Tagged(std::borrow::Cow::Owned(tag.to_string())),
592            None => crate::as_yaml::YamlKind::Scalar,
593        }
594    }
595
596    fn build_content(
597        &self,
598        builder: &mut rowan::GreenNodeBuilder,
599        indent: usize,
600        flow_context: bool,
601    ) -> bool {
602        self.build(
603            builder,
604            indent,
605            flow_context.then_some(FlowQuoting::Minimal),
606        )
607    }
608
609    fn build_flow_content(
610        &self,
611        builder: &mut rowan::GreenNodeBuilder,
612        indent: usize,
613        quoting: FlowQuoting,
614    ) -> bool {
615        self.build(builder, indent, Some(quoting))
616    }
617
618    fn is_inline(&self) -> bool {
619        true
620    }
621}
622
623#[cfg(test)]
624mod tests {
625    use super::*;
626
627    #[test]
628    fn test_plain_scalars() {
629        let scalar = ScalarValue::string("simple");
630        assert_eq!(scalar.to_yaml_string(), "simple");
631
632        let scalar = ScalarValue::string("hello world");
633        assert_eq!(scalar.to_yaml_string(), "hello world");
634    }
635
636    #[test]
637    fn test_values_needing_quotes() {
638        // Boolean-like values
639        let scalar = ScalarValue::string("true");
640        assert_eq!(scalar.to_yaml_string(), "'true'");
641
642        let scalar = ScalarValue::string("false");
643        assert_eq!(scalar.to_yaml_string(), "'false'");
644
645        let scalar = ScalarValue::string("yes");
646        assert_eq!(scalar.to_yaml_string(), "'yes'");
647
648        let scalar = ScalarValue::string("no");
649        assert_eq!(scalar.to_yaml_string(), "'no'");
650
651        // Null-like values
652        let scalar = ScalarValue::string("null");
653        assert_eq!(scalar.to_yaml_string(), "'null'");
654
655        let scalar = ScalarValue::string("~");
656        assert_eq!(scalar.to_yaml_string(), "'~'");
657
658        // Numbers
659        let scalar = ScalarValue::string("123");
660        assert_eq!(scalar.to_yaml_string(), "'123'");
661
662        let scalar = ScalarValue::string("3.14");
663        assert_eq!(scalar.to_yaml_string(), "'3.14'");
664
665        // Special characters
666        let scalar = ScalarValue::string("value: something");
667        assert_eq!(scalar.to_yaml_string(), "'value: something'");
668
669        let scalar = ScalarValue::string("# comment");
670        assert_eq!(scalar.to_yaml_string(), "'# comment'");
671
672        // Leading/trailing whitespace
673        let scalar = ScalarValue::string("  spaces  ");
674        assert_eq!(scalar.to_yaml_string(), "'  spaces  '");
675    }
676
677    #[test]
678    fn test_single_quoted() {
679        let scalar = ScalarValue::single_quoted("value with 'quotes'");
680        assert_eq!(scalar.to_yaml_string(), "'value with ''quotes'''");
681    }
682
683    #[test]
684    fn test_double_quoted() {
685        let scalar = ScalarValue::double_quoted("value with \"quotes\" and \\backslash");
686        assert_eq!(
687            scalar.to_yaml_string(),
688            "\"value with \\\"quotes\\\" and \\\\backslash\""
689        );
690
691        let scalar = ScalarValue::double_quoted("line1\nline2\ttab");
692        assert_eq!(scalar.to_yaml_string(), "\"line1\\nline2\\ttab\"");
693    }
694
695    #[test]
696    fn test_carriage_return_is_escaped() {
697        assert_eq!(ScalarValue::plain("a\rb").to_yaml_string(), "\"a\\rb\"");
698        assert_eq!(
699            ScalarValue::single_quoted("a\rb").to_yaml_string(),
700            "\"a\\rb\""
701        );
702    }
703
704    #[test]
705    fn test_multiline() {
706        let scalar = ScalarValue::string("line1\nline2\nline3");
707        // Should auto-detect literal style for multiline
708        assert_eq!(scalar.style(), ScalarStyle::Literal);
709    }
710
711    #[test]
712    fn test_from_types() {
713        let scalar = ScalarValue::from(42);
714        assert_eq!(scalar.to_yaml_string(), "42");
715
716        let scalar = ScalarValue::from(1.234);
717        assert_eq!(scalar.to_yaml_string(), "1.234");
718
719        let scalar = ScalarValue::from(true);
720        assert_eq!(scalar.to_yaml_string(), "true");
721
722        let scalar = ScalarValue::from(false);
723        assert_eq!(scalar.to_yaml_string(), "false");
724    }
725
726    #[test]
727    fn test_empty_string() {
728        let scalar = ScalarValue::string("");
729        assert_eq!(scalar.to_yaml_string(), "''");
730    }
731
732    #[test]
733    fn test_special_start_chars() {
734        let scalar = ScalarValue::string("-item");
735        assert_eq!(scalar.to_yaml_string(), "'-item'");
736
737        let scalar = ScalarValue::string("?key");
738        assert_eq!(scalar.to_yaml_string(), "'?key'");
739
740        let scalar = ScalarValue::string("[array]");
741        assert_eq!(scalar.to_yaml_string(), "'[array]'");
742    }
743
744    #[test]
745    fn test_null_scalar() {
746        let scalar = ScalarValue::null();
747        assert_eq!(scalar.to_yaml_string(), "null");
748        assert_eq!(scalar.scalar_type, ScalarType::Null);
749    }
750
751    #[test]
752    fn test_indentation_preservation() {
753        // Test preserving exact indentation in block scalars
754        let content_with_indent = "  Line 1\n    Line 2 more indented\n  Line 3";
755        let scalar = ScalarValue::literal(content_with_indent);
756
757        // The indicator keeps the leading spaces from being read as the
758        // block's own indentation.
759        let yaml_output = scalar.to_literal_with_indent(0, 2);
760        assert_eq!(
761            yaml_output,
762            "|2-\n    Line 1\n      Line 2 more indented\n    Line 3"
763        );
764    }
765
766    #[test]
767    fn test_literal_with_custom_indent() {
768        // Test applying custom indentation to unindented content
769        let content = "Line 1\nLine 2\nLine 3";
770        let scalar = ScalarValue::literal(content);
771
772        let yaml_4_spaces = scalar.to_literal_with_indent(0, 4);
773        assert_eq!(yaml_4_spaces, "|-\n    Line 1\n    Line 2\n    Line 3");
774
775        let yaml_1_space = scalar.to_literal_with_indent(0, 1);
776        assert_eq!(yaml_1_space, "|-\n Line 1\n Line 2\n Line 3");
777    }
778
779    #[test]
780    fn test_block_indent_counts_from_parent_column() {
781        let scalar = ScalarValue::literal("  x\n  y");
782        assert_eq!(scalar.to_yaml_string(), "|2-\n    x\n    y");
783        assert_eq!(
784            scalar.to_yaml_string_with_indent(2, 2),
785            "|2-\n      x\n      y"
786        );
787        assert_eq!(
788            scalar.to_literal_with_indent(4, 3),
789            "|3-\n         x\n         y"
790        );
791        assert_eq!(
792            ScalarValue::folded("  x\n").to_folded_with_indent(2, 2),
793            ">2\n      x"
794        );
795        assert_eq!(
796            ScalarValue::literal("x\ny\n").to_yaml_string_with_indent(2, 2),
797            "|\n    x\n    y"
798        );
799    }
800
801    #[test]
802    fn test_block_indicator_when_first_line_is_indented() {
803        // Only the first line decides the indentation a reader detects, so
804        // it alone decides whether an indicator is needed.
805        assert_eq!(
806            ScalarValue::literal(" a\nb").to_yaml_string(),
807            "|2-\n   a\n  b"
808        );
809        assert_eq!(
810            ScalarValue::literal("  L1\n    L2\n  L3").to_yaml_string_with_indent(2, 2),
811            "|2-\n      L1\n        L2\n      L3"
812        );
813        assert_eq!(
814            ScalarValue::literal("  \nx").to_yaml_string(),
815            "|2-\n    \n  x"
816        );
817        assert_eq!(
818            ScalarValue::literal("a\n  b").to_yaml_string(),
819            "|-\n  a\n    b"
820        );
821    }
822
823    #[test]
824    fn test_block_keeps_whitespace_only_lines() {
825        assert_eq!(
826            ScalarValue::literal("a\n  \nb").to_yaml_string(),
827            "|-\n  a\n    \n  b"
828        );
829    }
830
831    #[test]
832    #[should_panic(expected = "does not fit in one digit")]
833    fn test_block_indent_indicator_too_large() {
834        ScalarValue::literal("  x").to_literal_with_indent(0, 10);
835    }
836
837    #[test]
838    #[should_panic(expected = "must be indented")]
839    fn test_block_indent_zero() {
840        ScalarValue::literal("x").to_literal_with_indent(2, 0);
841    }
842
843    #[test]
844    fn test_folded_with_custom_indent() {
845        // Test applying custom indentation to folded scalars
846        let content = "Line 1\nLine 2\nLine 3";
847        let scalar = ScalarValue::folded(content);
848
849        let yaml_3_spaces = scalar.to_folded_with_indent(0, 3);
850        assert_eq!(yaml_3_spaces, ">-\n   Line 1\n\n   Line 2\n\n   Line 3");
851    }
852
853    #[test]
854    fn test_block_scalar_first_line_more_indented() {
855        // The first line's extra spaces would be taken as the block's own
856        // indentation, leaving the later line outside it.
857        assert_eq!(
858            ScalarValue::string("  a\nb").to_yaml_string(),
859            "|2-\n    a\n  b"
860        );
861        assert_eq!(
862            ScalarValue::string("  x\n y").to_yaml_string_with_indent(2, 2),
863            "|2-\n      x\n     y"
864        );
865        assert_eq!(
866            ScalarValue::string("\n x").to_yaml_string_with_indent(2, 2),
867            "|2-\n\n     x"
868        );
869    }
870
871    #[test]
872    fn test_block_scalar_whitespace_only_lines() {
873        assert_eq!(
874            ScalarValue::literal("a\n ").to_yaml_string(),
875            "|-\n  a\n   "
876        );
877        assert_eq!(
878            ScalarValue::literal("a\n   \nb").to_yaml_string(),
879            "|-\n  a\n     \n  b"
880        );
881        assert_eq!(ScalarValue::literal(" ").to_yaml_string(), "|2-\n   ");
882        assert_eq!(ScalarValue::literal(" \n").to_yaml_string(), "|2\n   ");
883        // Left to choose, such a value is double-quoted.
884        assert_eq!(ScalarValue::string("a\n ").to_yaml_string(), "\"a\\n \"");
885        assert_eq!(ScalarValue::string(" \n").to_yaml_string(), "\" \\n\"");
886    }
887
888    #[test]
889    fn test_folded_preserves_line_breaks() {
890        assert_eq!(
891            ScalarValue::folded("a\nb").to_yaml_string(),
892            ">-\n  a\n\n  b"
893        );
894        // Breaks next to a more-indented line are not folded, so they need
895        // no extra empty line.
896        assert_eq!(
897            ScalarValue::folded("a\n\nb\n  c\nd\n").to_yaml_string(),
898            ">\n  a\n\n\n  b\n    c\n  d"
899        );
900    }
901
902    #[test]
903    fn test_single_quoted_line_breaks() {
904        // Continuation lines are indented to stay inside the parent block.
905        assert_eq!(
906            ScalarValue::single_quoted("a\nb").to_yaml_string(),
907            "'a\n\n  b'"
908        );
909        assert_eq!(
910            ScalarValue::single_quoted("a\n\nb").to_yaml_string(),
911            "'a\n\n\n  b'"
912        );
913        assert_eq!(
914            ScalarValue::single_quoted("\nb\n").to_yaml_string(),
915            "'\n\n  b\n\n'"
916        );
917        assert_eq!(
918            ScalarValue::single_quoted("a\nb").to_yaml_string_with_indent(2, 2),
919            "'a\n\n    b'"
920        );
921        // Whitespace around a line break is trimmed when folding, so only
922        // double quotes can carry it.
923        assert_eq!(
924            ScalarValue::single_quoted("a \nb").to_yaml_string(),
925            "\"a \\nb\""
926        );
927        assert_eq!(
928            ScalarValue::single_quoted("a\n b").to_yaml_string(),
929            "\"a\\n b\""
930        );
931    }
932
933    #[test]
934    fn test_mixed_empty_lines_preservation() {
935        // Test handling of empty lines in block scalars
936        let content_with_empty_lines = "Line 1\n\nLine 3\n\n\nLine 6";
937        let scalar = ScalarValue::literal(content_with_empty_lines);
938
939        let yaml_output = scalar.to_literal_with_indent(0, 2);
940        assert_eq!(yaml_output, "|-\n  Line 1\n\n  Line 3\n\n\n  Line 6");
941
942        // Empty lines should remain empty (no indentation added)
943        // Input has 3 empty lines; they should appear unchanged in the output
944        let lines: Vec<&str> = yaml_output.lines().collect();
945        let empty_line_count = lines.iter().filter(|line| line.is_empty()).count();
946        assert_eq!(empty_line_count, 3);
947    }
948
949    #[test]
950    fn test_double_quoted_with_escapes() {
951        // Test that double-quoted scalars properly escape and unescape
952        let original = "Hello\nWorld\t😃";
953        let scalar = ScalarValue::double_quoted(original);
954        let yaml_string = scalar.to_yaml_string();
955
956        // Should contain escaped sequences
957        assert_eq!(yaml_string, "\"Hello\\nWorld\\t\\U0001F603\"");
958
959        // Parse it back
960        let parsed = ScalarValue::parse_escape_sequences(&yaml_string[1..yaml_string.len() - 1]);
961        assert_eq!(parsed, original);
962    }
963
964    #[test]
965    fn test_unicode_output_formatting() {
966        // Test that Unicode characters are properly formatted in output
967        let scalar = ScalarValue::double_quoted("Hello 世界 🌍");
968        let yaml_string = scalar.to_yaml_string();
969
970        // Should escape non-ASCII characters
971        assert_eq!(yaml_string, "\"Hello \\u4E16\\u754C \\U0001F30D\"");
972
973        // But the internal value should remain unchanged
974        assert_eq!(scalar.value(), "Hello 世界 🌍");
975    }
976
977    #[test]
978    fn test_timestamp_creation_and_validation() {
979        // Test various timestamp formats
980        let valid_timestamps = [
981            "2023-12-25",
982            "2023-12-25T10:30:45",
983            "2023-12-25 10:30:45",
984            "2023-12-25T10:30:45Z",
985            "2001-12-14 21:59:43.10 -5", // Space-separated with timezone
986            "2001-12-15T02:59:43.1Z",    // ISO 8601
987            "2001-12-14t21:59:43.10-05:00", // Lowercase t
988        ];
989
990        for ts in valid_timestamps {
991            let scalar = ScalarValue::timestamp(ts);
992            assert!(scalar.is_timestamp());
993            assert_eq!(scalar.scalar_type(), ScalarType::Timestamp);
994            assert_eq!(scalar.value(), ts);
995
996            let yaml_output = scalar.to_yaml_string();
997            assert_eq!(yaml_output, format!("!!timestamp {}", ts));
998
999            // Test auto-detection recognizes it as timestamp
1000            let auto_scalar = ScalarValue::parse(ts);
1001            assert_eq!(
1002                auto_scalar.scalar_type(),
1003                ScalarType::Timestamp,
1004                "Failed to auto-detect '{}' as timestamp",
1005                ts
1006            );
1007        }
1008
1009        // Test invalid timestamps are not recognized
1010        let invalid_timestamps = [
1011            "not-a-date",
1012            "2023-13-01", // Invalid month
1013            "2023-12-32", // Invalid day
1014            "12:34:56",   // Time only (should be String)
1015            "2023/12/25", // Wrong separator
1016        ];
1017
1018        for ts in invalid_timestamps {
1019            let auto_scalar = ScalarValue::parse(ts);
1020            assert_ne!(
1021                auto_scalar.scalar_type(),
1022                ScalarType::Timestamp,
1023                "'{}' should not be detected as timestamp",
1024                ts
1025            );
1026        }
1027    }
1028
1029    #[test]
1030    fn test_regex_creation() {
1031        let pattern = r"^\d{3}-\d{2}-\d{4}$";
1032        let scalar = ScalarValue::regex(pattern);
1033
1034        assert!(scalar.is_regex());
1035        assert_eq!(scalar.scalar_type(), ScalarType::Regex);
1036        assert_eq!(scalar.value(), pattern);
1037
1038        let yaml_output = scalar.to_yaml_string();
1039        assert_eq!(yaml_output, format!("!!regex {}", pattern));
1040    }
1041
1042    #[test]
1043    fn test_regex_edge_cases() {
1044        // Test empty pattern
1045        let empty_regex = ScalarValue::regex("");
1046        assert!(empty_regex.is_regex());
1047        assert_eq!(empty_regex.value(), "");
1048        assert_eq!(empty_regex.to_yaml_string(), "!!regex");
1049
1050        // Test pattern with special characters
1051        let special_chars = ScalarValue::regex(r"[.*+?^${}()|[\]\\]");
1052        assert!(special_chars.is_regex());
1053        assert_eq!(special_chars.value(), r"[.*+?^${}()|[\]\\]");
1054
1055        // Test unicode patterns
1056        let unicode_regex = ScalarValue::regex(r"\p{L}+");
1057        assert!(unicode_regex.is_regex());
1058        assert_eq!(unicode_regex.value(), r"\p{L}+");
1059
1060        // Test very long pattern
1061        let long_pattern = "a".repeat(1000);
1062        let long_regex = ScalarValue::regex(&long_pattern);
1063        assert!(long_regex.is_regex());
1064        assert_eq!(long_regex.value(), long_pattern);
1065
1066        // Test pattern with quotes and escapes
1067        let quoted_regex = ScalarValue::regex(r#"'quoted' and "double quoted" with \\ backslash"#);
1068        assert!(quoted_regex.is_regex());
1069        assert_eq!(
1070            quoted_regex.value(),
1071            r#"'quoted' and "double quoted" with \\ backslash"#
1072        );
1073    }
1074
1075    #[test]
1076    fn test_regex_type_coercion() {
1077        let regex_scalar = ScalarValue::regex(r"\d+");
1078
1079        // Test coercing regex to string
1080        let string_scalar = regex_scalar.coerce_to_type(ScalarType::String).unwrap();
1081        assert_eq!(string_scalar.scalar_type(), ScalarType::String);
1082        assert_eq!(string_scalar.value(), r"\d+");
1083        assert!(!string_scalar.is_regex());
1084
1085        // Test coercing string to regex
1086        let str_scalar = ScalarValue::string("test.*");
1087        let regex_from_string = str_scalar.coerce_to_type(ScalarType::Regex).unwrap();
1088        assert_eq!(regex_from_string.scalar_type(), ScalarType::Regex);
1089        assert_eq!(regex_from_string.value(), "test.*");
1090        assert!(regex_from_string.is_regex());
1091
1092        // Test that regex cannot be coerced to number types
1093        assert!(regex_scalar.coerce_to_type(ScalarType::Integer).is_none());
1094        assert!(regex_scalar.coerce_to_type(ScalarType::Float).is_none());
1095        assert!(regex_scalar.coerce_to_type(ScalarType::Boolean).is_none());
1096    }
1097
1098    #[test]
1099    #[cfg(feature = "regex")]
1100    fn test_regex_compilation() {
1101        // Test as_regex() with a regex scalar
1102        let regex_scalar = ScalarValue::regex(r"\d{3}-\d{4}");
1103        let compiled = regex_scalar.as_regex().unwrap();
1104        assert!(compiled.is_match("555-1234"));
1105        assert!(!compiled.is_match("not-a-phone"));
1106
1107        // Test as_regex() with a non-regex scalar returns None
1108        let string_scalar = ScalarValue::string("not a regex");
1109        assert!(string_scalar.as_regex().is_none());
1110
1111        // Test try_as_regex() with any scalar type
1112        let pattern_scalar = ScalarValue::string(r"^\w+@\w+\.\w+$");
1113        let email_regex = pattern_scalar.try_as_regex().unwrap();
1114        assert!(email_regex.is_match("test@example.com"));
1115        assert!(!email_regex.is_match("not-an-email"));
1116
1117        // Test with invalid regex pattern
1118        let invalid_scalar = ScalarValue::regex(r"[invalid(");
1119        assert!(invalid_scalar.as_regex().is_none());
1120
1121        // Test try_as_regex() with invalid pattern returns error
1122        let invalid_pattern = ScalarValue::string(r"[invalid(");
1123        assert!(invalid_pattern.try_as_regex().is_err());
1124    }
1125
1126    #[test]
1127    #[cfg(feature = "regex")]
1128    fn test_regex_extraction_use_cases() {
1129        // Test extracting and using regex for validation
1130        let validation_rules = [
1131            ScalarValue::regex(r"^\d{5}$"),                 // ZIP code
1132            ScalarValue::regex(r"^[A-Z]{2}$"),              // State code
1133            ScalarValue::regex(r"^\(\d{3}\) \d{3}-\d{4}$"), // Phone number
1134        ];
1135
1136        let test_values = ["12345", "CA", "(555) 123-4567"];
1137
1138        for (rule, value) in validation_rules.iter().zip(test_values.iter()) {
1139            let regex = rule.as_regex().unwrap();
1140            assert!(regex.is_match(value), "Pattern should match {}", value);
1141        }
1142
1143        // Test with complex regex patterns
1144        let email_regex = ScalarValue::regex(r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$");
1145        let compiled = email_regex.as_regex().unwrap();
1146        assert!(compiled.is_match("user@example.com"));
1147        assert!(compiled.is_match("test.user+tag@sub.domain.org"));
1148        assert!(!compiled.is_match("invalid.email"));
1149
1150        // Test extracting capture groups
1151        let version_regex = ScalarValue::regex(r"^v(\d+)\.(\d+)\.(\d+)$");
1152        let compiled = version_regex.as_regex().unwrap();
1153        if let Some(captures) = compiled.captures("v1.2.3") {
1154            assert_eq!(captures.get(1).unwrap().as_str(), "1");
1155            assert_eq!(captures.get(2).unwrap().as_str(), "2");
1156            assert_eq!(captures.get(3).unwrap().as_str(), "3");
1157        } else {
1158            panic!("Should have matched version string");
1159        }
1160    }
1161
1162    #[test]
1163    fn test_type_coercion() {
1164        // Test coercing string to integer
1165        let str_scalar = ScalarValue::string("42");
1166        let int_scalar = str_scalar.coerce_to_type(ScalarType::Integer).unwrap();
1167        assert_eq!(int_scalar.scalar_type(), ScalarType::Integer);
1168        assert_eq!(int_scalar.value(), "42");
1169
1170        // Test coercing string to boolean
1171        let bool_scalar = ScalarValue::string("true")
1172            .coerce_to_type(ScalarType::Boolean)
1173            .unwrap();
1174        assert_eq!(bool_scalar.scalar_type(), ScalarType::Boolean);
1175        assert_eq!(bool_scalar.value(), "true");
1176
1177        // Test coercing boolean string variations
1178        let yes_scalar = ScalarValue::string("yes")
1179            .coerce_to_type(ScalarType::Boolean)
1180            .unwrap();
1181        assert_eq!(yes_scalar.value(), "true");
1182
1183        let no_scalar = ScalarValue::string("no")
1184            .coerce_to_type(ScalarType::Boolean)
1185            .unwrap();
1186        assert_eq!(no_scalar.value(), "false");
1187
1188        // Test failed coercion
1189        let str_scalar = ScalarValue::string("not_a_number");
1190        assert!(str_scalar.coerce_to_type(ScalarType::Integer).is_none());
1191    }
1192
1193    #[test]
1194    fn test_auto_type_detection() {
1195        // Test various automatic type detections
1196        assert_eq!(ScalarValue::auto_detect_type("42"), ScalarType::Integer);
1197        assert_eq!(ScalarValue::auto_detect_type("3.14"), ScalarType::Float);
1198        assert_eq!(ScalarValue::auto_detect_type("true"), ScalarType::Boolean);
1199        assert_eq!(ScalarValue::auto_detect_type("false"), ScalarType::Boolean);
1200        assert_eq!(ScalarValue::auto_detect_type("yes"), ScalarType::String);
1201        assert_eq!(ScalarValue::auto_detect_type("NuLl"), ScalarType::String);
1202        assert_eq!(ScalarValue::auto_detect_type("null"), ScalarType::Null);
1203        assert_eq!(ScalarValue::auto_detect_type("~"), ScalarType::Null);
1204        assert_eq!(ScalarValue::auto_detect_type(""), ScalarType::Null);
1205        assert_eq!(
1206            ScalarValue::auto_detect_type("2023-12-25"),
1207            ScalarType::Timestamp
1208        );
1209        assert_eq!(
1210            ScalarValue::auto_detect_type("2023-12-25T10:30:45"),
1211            ScalarType::Timestamp
1212        );
1213        // Base64-looking text is still a string unless tagged !!binary.
1214        assert_eq!(
1215            ScalarValue::auto_detect_type("SGVsbG8gV29ybGQ="),
1216            ScalarType::String
1217        );
1218        assert_eq!(
1219            ScalarValue::auto_detect_type("hello world"),
1220            ScalarType::String
1221        );
1222    }
1223
1224    #[test]
1225    fn test_from_yaml_scalar_creation() {
1226        let int_scalar = ScalarValue::parse("123");
1227        assert_eq!(int_scalar.scalar_type(), ScalarType::Integer);
1228
1229        let bool_scalar = ScalarValue::parse("true");
1230        assert_eq!(bool_scalar.scalar_type(), ScalarType::Boolean);
1231
1232        let timestamp_scalar = ScalarValue::parse("2023-12-25");
1233        assert_eq!(timestamp_scalar.scalar_type(), ScalarType::Timestamp);
1234
1235        let string_scalar = ScalarValue::parse("hello world");
1236        assert_eq!(string_scalar.scalar_type(), ScalarType::String);
1237    }
1238
1239    #[test]
1240    fn test_yaml_1_1_timestamp_examples() {
1241        // The examples of https://yaml.org/type/timestamp.html, and the
1242        // one-digit and white space forms its regular expression allows.
1243        for text in [
1244            "2001-12-15T02:59:43.1Z",
1245            "2001-12-14t21:59:43.10-05:00",
1246            "2001-12-14 21:59:43.10 -5",
1247            "2001-12-15 2:59:43.10",
1248            "2002-12-14",
1249            "2001-1-5T1:02:03Z",
1250            "2001-12-14  21:59:43",
1251            "2001-12-14\t21:59:43",
1252        ] {
1253            assert_eq!(
1254                ScalarValue::parse(text).scalar_type(),
1255                ScalarType::Timestamp,
1256                "{text:?}"
1257            );
1258        }
1259        for text in [
1260            "2001-1-5",
1261            "2001-12-5",
1262            "2001-1-15",
1263            "2001-12-14T",
1264            "2001-12-14 123:59:43",
1265            "2001-12-14T21:5:43",
1266            "2001-12-14T21:59:4",
1267            "2001-12-14x21:59:43",
1268            "2001-12-14T24:00:00",
1269            "2001-12-32",
1270        ] {
1271            assert_eq!(
1272                ScalarValue::parse(text).scalar_type(),
1273                ScalarType::String,
1274                "{text:?}"
1275            );
1276        }
1277    }
1278
1279    #[test]
1280    #[cfg(feature = "base64")]
1281    fn test_binary_yaml_output_with_tags() {
1282        let data = b"Binary data here";
1283        let scalar = ScalarValue::binary(data);
1284        let yaml_output = scalar.to_yaml_string();
1285
1286        assert!(yaml_output.starts_with("!!binary "));
1287
1288        // Extract just the base64 part
1289        let base64_part = &yaml_output[9..]; // Remove "!!binary "
1290        let decoded = base64_decode(base64_part).expect("Should decode");
1291        assert_eq!(decoded, data);
1292    }
1293
1294    #[test]
1295    #[cfg(feature = "base64")]
1296    fn test_special_data_types_with_different_styles() {
1297        // Binary with different styles should still include tag
1298        let data = b"test";
1299        let binary_scalar = ScalarValue::binary(data);
1300
1301        // Even if we change style, binary type should maintain tag
1302        let mut styled_binary = binary_scalar;
1303        styled_binary.style = ScalarStyle::DoubleQuoted;
1304
1305        // The to_yaml_string should still respect the scalar type for tagging
1306        assert_eq!(styled_binary.to_yaml_string(), "!!binary \"dGVzdA==\"");
1307    }
1308
1309    #[test]
1310    fn test_type_checking_methods() {
1311        #[cfg(feature = "base64")]
1312        let binary_scalar = ScalarValue::binary(b"test");
1313        let timestamp_scalar = ScalarValue::timestamp("2023-12-25");
1314        let regex_scalar = ScalarValue::regex(r"\d+");
1315        let string_scalar = ScalarValue::string("hello");
1316
1317        // Test type checking methods
1318        #[cfg(feature = "base64")]
1319        assert!(binary_scalar.is_binary());
1320        #[cfg(feature = "base64")]
1321        assert!(!binary_scalar.is_timestamp());
1322        #[cfg(feature = "base64")]
1323        assert!(!binary_scalar.is_regex());
1324
1325        #[cfg(feature = "base64")]
1326        assert!(!timestamp_scalar.is_binary());
1327        assert!(timestamp_scalar.is_timestamp());
1328        assert!(!timestamp_scalar.is_regex());
1329
1330        #[cfg(feature = "base64")]
1331        assert!(!regex_scalar.is_binary());
1332        assert!(!regex_scalar.is_timestamp());
1333        assert!(regex_scalar.is_regex());
1334
1335        #[cfg(feature = "base64")]
1336        assert!(!string_scalar.is_binary());
1337        assert!(!string_scalar.is_timestamp());
1338        assert!(!string_scalar.is_regex());
1339    }
1340
1341    #[test]
1342    fn test_binary_is_not_an_integer() {
1343        // YAML 1.2 has no binary integer form.
1344        for text in ["0b1010", "0B101", "-0b1010", "+0b101"] {
1345            assert_eq!(ScalarValue::parse_integer(text), None, "{text}");
1346            assert_eq!(ScalarValue::auto_detect_type(text), ScalarType::String);
1347        }
1348    }
1349
1350    #[test]
1351    fn test_modern_octal_number_parsing() {
1352        // Test modern octal number parsing (0o prefix)
1353        assert_eq!(ScalarValue::parse_integer("0o755"), Some(493)); // 7*64 + 5*8 + 5
1354        assert_eq!(ScalarValue::parse_integer("0o644"), Some(420)); // 6*64 + 4*8 + 4
1355        assert_eq!(ScalarValue::auto_detect_type("0o755"), ScalarType::Integer);
1356
1357        // The core schema allows neither a sign nor an uppercase prefix.
1358        for text in ["0O777", "-0o755", "+0o644"] {
1359            assert_eq!(ScalarValue::parse_integer(text), None, "{text}");
1360            assert_eq!(ScalarValue::auto_detect_type(text), ScalarType::String);
1361        }
1362
1363        // Test invalid octal
1364        assert_eq!(ScalarValue::parse_integer("0o789"), None); // Contains invalid digit
1365        assert_eq!(ScalarValue::parse_integer("0o"), None); // Empty after prefix
1366    }
1367
1368    #[test]
1369    fn test_leading_zero_is_decimal() {
1370        // YAML 1.2 dropped 1.1's bare-octal form.
1371        assert_eq!(ScalarValue::parse_integer("0755"), Some(755));
1372        assert_eq!(ScalarValue::parse_integer("0644"), Some(644));
1373        assert_eq!(ScalarValue::parse_integer("-0777"), Some(-777));
1374
1375        // Test auto-detection
1376        assert_eq!(ScalarValue::auto_detect_type("0755"), ScalarType::Integer);
1377        assert_eq!(ScalarValue::auto_detect_type("0644"), ScalarType::Integer);
1378
1379        // Test edge cases
1380        assert_eq!(ScalarValue::parse_integer("0"), Some(0)); // Single zero
1381        assert_eq!(ScalarValue::parse_integer("00"), Some(0)); // Double zero
1382
1383        assert_eq!(ScalarValue::parse_integer("0789"), Some(789));
1384        assert_eq!(ScalarValue::parse_integer("08"), Some(8));
1385    }
1386
1387    #[test]
1388    fn test_hexadecimal_number_parsing() {
1389        // Test hexadecimal number parsing (0x prefix) - should still work
1390        assert_eq!(ScalarValue::parse_integer("0xFF"), Some(255));
1391        assert_eq!(ScalarValue::parse_integer("0x1A"), Some(26));
1392        assert_eq!(ScalarValue::parse_integer("0xff"), Some(255));
1393        assert_eq!(ScalarValue::auto_detect_type("0xFF"), ScalarType::Integer);
1394
1395        // The core schema allows neither a sign nor an uppercase prefix.
1396        for text in ["0XFF", "-0xFF", "+0x1A", "+0x1F", "-0o7", "0X1F", "0O7"] {
1397            assert_eq!(ScalarValue::parse_integer(text), None, "{text}");
1398            assert_eq!(ScalarValue::auto_detect_type(text), ScalarType::String);
1399            assert_eq!(
1400                ScalarValue::classify_plain(text),
1401                CoreScalarType::String,
1402                "{text}"
1403            );
1404        }
1405    }
1406
1407    #[test]
1408    fn test_decimal_number_parsing() {
1409        // Test decimal number parsing (no prefix) - should still work
1410        assert_eq!(ScalarValue::parse_integer("42"), Some(42));
1411        assert_eq!(ScalarValue::parse_integer("123"), Some(123));
1412        assert_eq!(ScalarValue::parse_integer("-42"), Some(-42));
1413        assert_eq!(ScalarValue::parse_integer("+123"), Some(123));
1414
1415        // Test auto-detection
1416        assert_eq!(ScalarValue::auto_detect_type("42"), ScalarType::Integer);
1417        assert_eq!(ScalarValue::auto_detect_type("-123"), ScalarType::Integer);
1418    }
1419
1420    #[test]
1421    fn test_number_format_yaml_output() {
1422        // Test that different number formats are properly detected and output
1423        let binary_scalar = ScalarValue::parse("0b1010");
1424        assert_eq!(binary_scalar.scalar_type(), ScalarType::String);
1425        assert_eq!(binary_scalar.to_yaml_string(), "'0b1010'");
1426
1427        let octal_scalar = ScalarValue::parse("0o755");
1428        assert_eq!(octal_scalar.scalar_type(), ScalarType::Integer);
1429        assert_eq!(octal_scalar.value(), "0o755");
1430
1431        let hex_scalar = ScalarValue::parse("0xFF");
1432        assert_eq!(hex_scalar.scalar_type(), ScalarType::Integer);
1433        assert_eq!(hex_scalar.value(), "0xFF");
1434
1435        let legacy_octal_scalar = ScalarValue::parse("0755");
1436        assert_eq!(legacy_octal_scalar.scalar_type(), ScalarType::Integer);
1437        assert_eq!(legacy_octal_scalar.value(), "0755");
1438        assert_eq!(legacy_octal_scalar.to_i64(), Some(755));
1439    }
1440
1441    #[test]
1442    fn test_coerce_to_null() {
1443        // Only the recognized null spellings coerce to Null.
1444        let s = ScalarValue::string("null");
1445        assert_eq!(
1446            s.coerce_to_type(ScalarType::Null).map(|v| v.scalar_type()),
1447            Some(ScalarType::Null)
1448        );
1449        assert!(ScalarValue::string("~")
1450            .coerce_to_type(ScalarType::Null)
1451            .is_some());
1452        assert!(ScalarValue::string("")
1453            .coerce_to_type(ScalarType::Null)
1454            .is_some());
1455        assert!(ScalarValue::string("notnull")
1456            .coerce_to_type(ScalarType::Null)
1457            .is_none());
1458    }
1459
1460    #[test]
1461    fn test_parse_preserves_string_style() {
1462        // The String arm of parse() runs detect_style; a plain word stays Plain.
1463        let plain = ScalarValue::parse("hello");
1464        assert_eq!(plain.scalar_type(), ScalarType::String);
1465        assert_eq!(plain.style, ScalarStyle::Plain);
1466
1467        // A multi-line string is given Literal style by detect_style, which only
1468        // runs in the String arm of parse().
1469        let multiline = ScalarValue::parse("multi line\nvalue");
1470        assert_eq!(multiline.scalar_type(), ScalarType::String);
1471        assert_eq!(multiline.style, ScalarStyle::Literal);
1472    }
1473
1474    #[test]
1475    fn test_from_scalar_type_and_style() {
1476        use crate::yaml::Document;
1477        use rowan::ast::AstNode;
1478        use std::str::FromStr;
1479
1480        // NULL token maps to Null type.
1481        let doc = Document::from_str("k: null\n").unwrap();
1482        let mapping = doc.as_mapping().unwrap();
1483        let null_node = mapping.get("k").unwrap();
1484        let null_scalar = crate::yaml::Scalar::cast(null_node.syntax().clone()).unwrap();
1485        let sv = ScalarValue::from_scalar(&null_scalar);
1486        assert_eq!(sv.scalar_type(), ScalarType::Null);
1487        assert_eq!(sv.style, ScalarStyle::Plain);
1488
1489        // STRING token in double quotes maps to String type, DoubleQuoted style.
1490        let doc = Document::from_str("k: \"hi\"\n").unwrap();
1491        let node = doc.as_mapping().unwrap().get("k").unwrap();
1492        let scalar = crate::yaml::Scalar::cast(node.syntax().clone()).unwrap();
1493        let sv = ScalarValue::from_scalar(&scalar);
1494        assert_eq!(sv.scalar_type(), ScalarType::String);
1495        assert_eq!(sv.style, ScalarStyle::DoubleQuoted);
1496
1497        // Single quoted string keeps SingleQuoted style.
1498        let doc = Document::from_str("k: 'hi'\n").unwrap();
1499        let node = doc.as_mapping().unwrap().get("k").unwrap();
1500        let scalar = crate::yaml::Scalar::cast(node.syntax().clone()).unwrap();
1501        let sv = ScalarValue::from_scalar(&scalar);
1502        assert_eq!(sv.style, ScalarStyle::SingleQuoted);
1503    }
1504
1505    #[test]
1506    fn test_from_scalar_ampersand_in_plain_is_string() {
1507        // `3.1&1` is a single plain scalar with literal content `3.1&1`.
1508        // Per YAML 1.2 core-schema tag resolution it must resolve to
1509        // !!str, not !!float.
1510        use crate::yaml::Document;
1511        use rowan::ast::AstNode;
1512        use std::str::FromStr;
1513
1514        let doc = Document::from_str("k: 3.1&1\n").unwrap();
1515        let node = doc.as_mapping().unwrap().get("k").unwrap();
1516        let scalar = crate::yaml::Scalar::cast(node.syntax().clone()).unwrap();
1517        let sv = ScalarValue::from_scalar(&scalar);
1518        assert_eq!(sv.scalar_type(), ScalarType::String);
1519        assert_eq!(sv.style, ScalarStyle::Plain);
1520    }
1521
1522    #[test]
1523    fn test_from_scalar_signed_yaml_infinity_is_float() {
1524        // `+.INF` is one !!float per YAML 1.2 even though Rust's
1525        // `f64::from_str` wouldn't accept the bare token on its own.
1526        use crate::yaml::Document;
1527        use rowan::ast::AstNode;
1528        use std::str::FromStr;
1529
1530        let doc = Document::from_str("k: +.INF\n").unwrap();
1531        let node = doc.as_mapping().unwrap().get("k").unwrap();
1532        let scalar = crate::yaml::Scalar::cast(node.syntax().clone()).unwrap();
1533        let sv = ScalarValue::from_scalar(&scalar);
1534        assert_eq!(sv.scalar_type(), ScalarType::Float);
1535    }
1536
1537    #[test]
1538    fn test_classify_plain_bare_inf_nan_is_string() {
1539        // Per YAML 1.2 only the dotted spellings are !!float; bare
1540        // `inf`, `nan`, `infinity` in any casing are strings even
1541        // though Rust's `f64::from_str` accepts them.
1542        for input in [
1543            "inf", "Inf", "InF", "INF", "iNf", "nan", "NaN", "NAN", "nAn", "infinity", "Infinity",
1544            "INFINITY", "+inf", "-Inf", "+nan",
1545        ] {
1546            assert_eq!(
1547                ScalarValue::classify_plain(input),
1548                CoreScalarType::String,
1549                "bare {input:?} should classify as String"
1550            );
1551        }
1552        // But the dotted spellings still resolve to Float.
1553        for input in [".inf", ".Inf", ".INF", "+.inf", "-.INF", ".nan"] {
1554            assert_eq!(
1555                ScalarValue::classify_plain(input),
1556                CoreScalarType::Float,
1557                "{input:?} should classify as Float"
1558            );
1559        }
1560    }
1561
1562    #[test]
1563    fn test_canonical_integer() {
1564        for (text, expected) in [
1565            ("0", "0"),
1566            ("-0", "0"),
1567            ("+000", "0"),
1568            ("0x0", "0"),
1569            ("0o0", "0"),
1570            ("42", "42"),
1571            ("+0042", "42"),
1572            ("-42", "-42"),
1573            ("0x2A", "42"),
1574            ("0o52", "42"),
1575            ("-9223372036854775808", "-9223372036854775808"),
1576            ("9223372036854775808", "9223372036854775808"),
1577            ("+9223372036854775808", "9223372036854775808"),
1578            ("0x10000000000000000", "18446744073709551616"),
1579            ("0o2000000000000000000000", "18446744073709551616"),
1580            ("0x1000000000", "68719476736"),
1581            (
1582                "0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF",
1583                "340282366920938463463374607431768211455",
1584            ),
1585            (
1586                "0x100000000000000000000000000000000",
1587                "340282366920938463463374607431768211456",
1588            ),
1589            (
1590                "-000100000000000000000000000000",
1591                "-100000000000000000000000000",
1592            ),
1593        ] {
1594            assert_eq!(
1595                ScalarValue::canonical_integer(text).as_deref(),
1596                Some(expected),
1597                "{text}"
1598            );
1599        }
1600        for text in [
1601            "", "+", "-", "0x", "0o8", "-0x1", "+0o1", "1_000", "1.0", " 1",
1602        ] {
1603            assert_eq!(ScalarValue::canonical_integer(text), None, "{text:?}");
1604        }
1605    }
1606
1607    #[test]
1608    fn test_from_scalar_block_style_is_string() {
1609        // Block scalars (| and >) always resolve to !!str regardless of
1610        // content, and their style must be reported as Literal or Folded.
1611        use crate::yaml::Document;
1612        use rowan::ast::AstNode;
1613        use std::str::FromStr;
1614
1615        let doc = Document::from_str("k: |\n  1\n").unwrap();
1616        let node = doc.as_mapping().unwrap().get("k").unwrap();
1617        let scalar = crate::yaml::Scalar::cast(node.syntax().clone()).unwrap();
1618        let sv = ScalarValue::from_scalar(&scalar);
1619        assert_eq!(sv.style, ScalarStyle::Literal);
1620        assert_eq!(sv.scalar_type(), ScalarType::String);
1621
1622        let doc = Document::from_str("k: >\n  x\n").unwrap();
1623        let node = doc.as_mapping().unwrap().get("k").unwrap();
1624        let scalar = crate::yaml::Scalar::cast(node.syntax().clone()).unwrap();
1625        let sv = ScalarValue::from_scalar(&scalar);
1626        assert_eq!(sv.style, ScalarStyle::Folded);
1627        assert_eq!(sv.scalar_type(), ScalarType::String);
1628    }
1629
1630    #[test]
1631    fn test_needs_quoting_special_chars() {
1632        // Leading-character based quoting and special embedded characters, plus
1633        // each boolean/null keyword spelling (so dropping any one keyword arm is
1634        // detected). Single quotes are preferred unless the value already
1635        // contains one, in which case double quotes are used.
1636        let cases = [
1637            ("&anchor", "'&anchor'"),
1638            ("*alias", "'*alias'"),
1639            ("!tag", "'!tag'"),
1640            ("a|b", "'a|b'"),
1641            ("it's", "\"it's\""),
1642            ("a\"b", "'a\"b'"),
1643            ("50%", "'50%'"),
1644            ("on", "'on'"),
1645            ("off", "'off'"),
1646            ("On", "'On'"),
1647            ("OFF", "'OFF'"),
1648            ("yes", "'yes'"),
1649            ("no", "'no'"),
1650            ("true", "'true'"),
1651            ("false", "'false'"),
1652            ("null", "'null'"),
1653        ];
1654        for (input, expected) in cases {
1655            assert_eq!(ScalarValue::string(input).to_yaml_string(), expected);
1656        }
1657        // A plain word that needs no quoting round-trips unchanged.
1658        assert_eq!(ScalarValue::string("plain").to_yaml_string(), "plain");
1659    }
1660
1661    #[test]
1662    fn test_plain_string_with_control_character_is_double_quoted() {
1663        assert_eq!(ScalarValue::plain("\u{7}").to_yaml_string(), "\"\\a\"");
1664        assert_eq!(ScalarValue::plain("a\rb").to_yaml_string(), "\"a\\rb\"");
1665        assert_eq!(
1666            ScalarValue::plain("a\rb").with_tag("!x").to_yaml_string(),
1667            "!x \"a\\rb\""
1668        );
1669    }
1670
1671    #[test]
1672    fn test_literal_and_folded_nonempty() {
1673        // Literal and folded styles render the block indicator on its own line
1674        // followed by the content indented by two spaces. A folded block
1675        // needs an empty line to keep a line break.
1676        let s = ScalarValue::with_style("line one\nline two", ScalarStyle::Literal);
1677        assert_eq!(s.to_yaml_string(), "|-\n  line one\n  line two");
1678
1679        let s = ScalarValue::with_style("line one\nline two", ScalarStyle::Folded);
1680        assert_eq!(s.to_yaml_string(), ">-\n  line one\n\n  line two");
1681    }
1682
1683    #[test]
1684    fn test_display_matches_yaml_string() {
1685        let s = ScalarValue::string("true");
1686        assert_eq!(format!("{}", s), s.to_yaml_string());
1687        assert_eq!(format!("{}", s), "'true'");
1688    }
1689
1690    #[test]
1691    fn test_as_yaml_token_kind_per_type() {
1692        use crate::lex::SyntaxKind;
1693        use rowan::ast::AstNode;
1694
1695        let cases = [
1696            (ScalarValue::parse("42"), SyntaxKind::INT),
1697            (ScalarValue::parse("3.14"), SyntaxKind::FLOAT),
1698            (ScalarValue::parse("true"), SyntaxKind::BOOL),
1699            (ScalarValue::parse("null"), SyntaxKind::NULL),
1700            (ScalarValue::string("hello"), SyntaxKind::STRING),
1701        ];
1702        for (value, expected) in cases {
1703            let mut builder = rowan::GreenNodeBuilder::new();
1704            crate::AsYaml::build_content(&value, &mut builder, 0, false);
1705            let green = builder.finish();
1706            let node = rowan::SyntaxNode::<crate::yaml::Lang>::new_root(green);
1707            let scalar = crate::yaml::Scalar::cast(node).unwrap();
1708            let kind = scalar
1709                .syntax()
1710                .children_with_tokens()
1711                .filter_map(|c| c.into_token())
1712                .next()
1713                .unwrap()
1714                .kind();
1715            assert_eq!(kind, expected, "value {:?}", value.value());
1716        }
1717    }
1718
1719    #[test]
1720    #[cfg(feature = "regex")]
1721    fn test_as_regex_type_gated() {
1722        // as_regex only compiles when the scalar is tagged as a regex.
1723        let re = ScalarValue::regex(r"\d+");
1724        assert!(re.as_regex().is_some());
1725
1726        let not_re = ScalarValue::string(r"\d+");
1727        assert!(not_re.as_regex().is_none());
1728
1729        // try_as_regex ignores the type and compiles the value directly.
1730        assert!(not_re.try_as_regex().is_ok());
1731        assert!(ScalarValue::string("(").try_as_regex().is_err());
1732    }
1733
1734    #[test]
1735    fn test_from_f64_renders_yaml_float() {
1736        let cases: [(f64, &str); 6] = [
1737            (f64::NAN, ".nan"),
1738            (f64::INFINITY, ".inf"),
1739            (f64::NEG_INFINITY, "-.inf"),
1740            (1.0, "1.0"),
1741            (-0.0, "-0.0"),
1742            (2.5, "2.5"),
1743        ];
1744        for (value, expected) in cases {
1745            let sv = ScalarValue::from(value);
1746            assert_eq!(sv.to_yaml_string(), expected);
1747            assert_eq!(ScalarValue::classify_plain(expected), CoreScalarType::Float);
1748        }
1749        assert_eq!(ScalarValue::from(1.0f32).to_yaml_string(), "1.0");
1750        assert_eq!(ScalarValue::from(f32::NAN).to_yaml_string(), ".nan");
1751    }
1752
1753    #[test]
1754    fn test_to_f64_special_values() {
1755        let float = |s: &str| ScalarValue {
1756            value: s.to_string(),
1757            style: ScalarStyle::Plain,
1758            scalar_type: ScalarType::Float,
1759            tag: None,
1760        };
1761        assert_eq!(float(".inf").to_f64(), Some(f64::INFINITY));
1762        assert_eq!(float("+.INF").to_f64(), Some(f64::INFINITY));
1763        assert_eq!(float("-.Inf").to_f64(), Some(f64::NEG_INFINITY));
1764        assert!(float(".NaN").to_f64().unwrap().is_nan());
1765        assert_eq!(float("inf").to_f64(), None);
1766        assert_eq!(float("1.5").to_f64(), Some(1.5));
1767    }
1768
1769    #[test]
1770    fn test_parse_does_not_infer_binary() {
1771        for text in ["test", "abcd", "noon", "SGVsbG8="] {
1772            let scalar = ScalarValue::parse(text);
1773            assert_eq!(scalar.scalar_type(), ScalarType::String, "{text}");
1774            assert_eq!(scalar.to_yaml_string(), text);
1775        }
1776    }
1777
1778    #[test]
1779    fn test_sign_after_radix_prefix_is_not_integer() {
1780        for text in ["0x-5", "0x+5", "0o-7", "0o+7", "0b-1", "+-5", "-+5"] {
1781            assert_eq!(ScalarValue::parse_integer(text), None, "{text}");
1782            assert_eq!(
1783                ScalarValue::classify_plain(text),
1784                CoreScalarType::String,
1785                "{text}"
1786            );
1787            assert_eq!(
1788                ScalarValue::auto_detect_type(text),
1789                ScalarType::String,
1790                "{text}"
1791            );
1792            let doc: crate::yaml::Document = format!("k: {text}\n").parse().unwrap();
1793            let node = doc.as_mapping().unwrap().get("k").unwrap();
1794            assert_eq!(node.as_scalar().unwrap().as_i64(), None, "{text}");
1795        }
1796    }
1797
1798    #[test]
1799    fn test_integers_outside_i64() {
1800        assert_eq!(
1801            ScalarValue::parse_integer("-9223372036854775808"),
1802            Some(i64::MIN)
1803        );
1804        assert_eq!(ScalarValue::parse_integer("9223372036854775808"), None);
1805        for text in [
1806            "9223372036854775808",
1807            "-9223372036854775808",
1808            "0xFFFFFFFFFFFFFFFF",
1809            "123456789012345678901234567890",
1810        ] {
1811            assert_eq!(
1812                ScalarValue::classify_plain(text),
1813                CoreScalarType::Integer,
1814                "{text}"
1815            );
1816            assert_eq!(
1817                ScalarValue::auto_detect_type(text),
1818                ScalarType::Integer,
1819                "{text}"
1820            );
1821        }
1822        assert_eq!(
1823            ScalarValue::parse("-9223372036854775808").to_i64(),
1824            Some(i64::MIN)
1825        );
1826        assert_eq!(ScalarValue::parse("9223372036854775808").to_i64(), None);
1827    }
1828
1829    #[test]
1830    fn test_coerce_to_special_types() {
1831        let coerce = |value: &str, target: ScalarType| {
1832            ScalarValue::string(value)
1833                .coerce_to_type(target)
1834                .map(|s| (s.scalar_type(), s.value().to_string()))
1835        };
1836        #[cfg(feature = "base64")]
1837        {
1838            assert_eq!(
1839                coerce("aGk=", ScalarType::Binary),
1840                Some((ScalarType::Binary, "aGk=".to_string()))
1841            );
1842            assert_eq!(coerce("!!", ScalarType::Binary), None);
1843            assert_eq!(ScalarValue::string("aGk=").as_binary(), None);
1844            assert_eq!(
1845                ScalarValue::binary(b"hi").as_binary(),
1846                Some(Ok(b"hi".to_vec()))
1847            );
1848        }
1849        assert_eq!(
1850            coerce("2001-12-14", ScalarType::Timestamp),
1851            Some((ScalarType::Timestamp, "2001-12-14".to_string()))
1852        );
1853        assert_eq!(coerce("x", ScalarType::Timestamp), None);
1854        assert_eq!(
1855            coerce("a+", ScalarType::Regex),
1856            Some((ScalarType::Regex, "a+".to_string()))
1857        );
1858        assert_eq!(
1859            coerce("~", ScalarType::Null),
1860            Some((ScalarType::Null, "null".to_string()))
1861        );
1862        assert_eq!(coerce("x", ScalarType::Null), None);
1863    }
1864
1865    #[test]
1866    #[should_panic(expected = "does not fit in one digit")]
1867    fn test_block_indent_indicator_from_first_line_only() {
1868        ScalarValue::literal("\n  x\ny").to_literal_with_indent(0, 10);
1869    }
1870}