1#[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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum ScalarStyle {
21 Plain,
23 SingleQuoted,
25 DoubleQuoted,
27 Literal,
29 Folded,
31}
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
41pub enum CoreScalarType {
42 Null,
44 Boolean,
46 Integer,
48 Float,
50 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
66pub(crate) const INDICATORS: &[char] = &[
69 '-', '?', ':', ',', '[', ']', '{', '}', '#', '&', '*', '!', '|', '>', '\'', '"', '%', '@', '`',
70];
71
72#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
74pub enum ScalarType {
75 String,
77 Integer,
79 Float,
81 Boolean,
83 Null,
85 #[cfg(feature = "base64")]
87 Binary,
88 Timestamp,
90 Regex,
92}
93
94#[derive(Debug, Clone, PartialEq, Eq)]
96pub struct ScalarValue {
97 value: String,
99 style: ScalarStyle,
101 scalar_type: ScalarType,
103 tag: Option<String>,
105}
106
107pub(crate) fn trim_white(s: &str) -> &str {
110 s.trim_matches([' ', '\t'])
111}
112
113impl ScalarValue {
114 pub fn string(value: impl Into<String>) -> Self {
143 let value = value.into();
144 let style = Self::detect_style(&value);
145 let scalar_type = ScalarType::String;
147 Self {
148 value,
149 style,
150 scalar_type,
151 tag: None,
152 }
153 }
154
155 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 pub fn plain(value: impl Into<String>) -> Self {
167 Self::with_style(value, ScalarStyle::Plain)
168 }
169
170 pub fn single_quoted(value: impl Into<String>) -> Self {
172 Self::with_style(value, ScalarStyle::SingleQuoted)
173 }
174
175 pub fn double_quoted(value: impl Into<String>) -> Self {
177 Self::with_style(value, ScalarStyle::DoubleQuoted)
178 }
179
180 pub fn literal(value: impl Into<String>) -> Self {
182 Self::with_style(value, ScalarStyle::Literal)
183 }
184
185 pub fn folded(value: impl Into<String>) -> Self {
187 Self::with_style(value, ScalarStyle::Folded)
188 }
189
190 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 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 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 pub fn with_tag(mut self, tag: impl Into<String>) -> Self {
233 self.tag = Some(tag.into());
234 self
235 }
236
237 pub fn tag(&self) -> Option<&str> {
239 self.tag.as_deref()
240 }
241
242 pub fn value(&self) -> &str {
244 &self.value
245 }
246
247 pub fn style(&self) -> ScalarStyle {
249 self.style
250 }
251
252 pub fn scalar_type(&self) -> ScalarType {
254 self.scalar_type
255 }
256
257 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 #[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 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 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 pub fn is_timestamp(&self) -> bool {
303 self.scalar_type == ScalarType::Timestamp
304 }
305
306 pub fn is_regex(&self) -> bool {
308 self.scalar_type == ScalarType::Regex
309 }
310
311 #[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 #[cfg(feature = "regex")]
354 pub fn try_as_regex(&self) -> Result<regex::Regex, regex::Error> {
355 regex::Regex::new(&self.value)
356 }
357
358 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 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 if Self::is_valid_timestamp(&self.value) {
401 Some(ScalarValue::timestamp(&self.value))
402 } else {
403 None
404 }
405 }
406 ScalarType::Regex => {
407 Some(ScalarValue::regex(&self.value))
409 }
410 }
411 }
412
413 pub fn parse(value: impl Into<String>) -> Self {
443 let value = value.into();
444 let scalar_type = Self::auto_detect_type(&value);
445 let style = match scalar_type {
448 ScalarType::String => Self::detect_style(&value),
449 _ => ScalarStyle::Plain,
451 };
452
453 Self {
454 value,
455 style,
456 scalar_type,
457 tag: None,
458 }
459 }
460
461 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 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
523macro_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
577impl 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 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 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 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 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 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 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 let content_with_indent = " Line 1\n Line 2 more indented\n Line 3";
755 let scalar = ScalarValue::literal(content_with_indent);
756
757 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 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 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 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 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 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 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 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 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 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 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 let original = "Hello\nWorld\t😃";
953 let scalar = ScalarValue::double_quoted(original);
954 let yaml_string = scalar.to_yaml_string();
955
956 assert_eq!(yaml_string, "\"Hello\\nWorld\\t\\U0001F603\"");
958
959 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 let scalar = ScalarValue::double_quoted("Hello 世界 🌍");
968 let yaml_string = scalar.to_yaml_string();
969
970 assert_eq!(yaml_string, "\"Hello \\u4E16\\u754C \\U0001F30D\"");
972
973 assert_eq!(scalar.value(), "Hello 世界 🌍");
975 }
976
977 #[test]
978 fn test_timestamp_creation_and_validation() {
979 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", "2001-12-15T02:59:43.1Z", "2001-12-14t21:59:43.10-05:00", ];
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 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 let invalid_timestamps = [
1011 "not-a-date",
1012 "2023-13-01", "2023-12-32", "12:34:56", "2023/12/25", ];
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 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 let special_chars = ScalarValue::regex(r"[.*+?^${}()|[\]\\]");
1052 assert!(special_chars.is_regex());
1053 assert_eq!(special_chars.value(), r"[.*+?^${}()|[\]\\]");
1054
1055 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 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 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 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 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 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 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 let string_scalar = ScalarValue::string("not a regex");
1109 assert!(string_scalar.as_regex().is_none());
1110
1111 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 let invalid_scalar = ScalarValue::regex(r"[invalid(");
1119 assert!(invalid_scalar.as_regex().is_none());
1120
1121 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 let validation_rules = [
1131 ScalarValue::regex(r"^\d{5}$"), ScalarValue::regex(r"^[A-Z]{2}$"), ScalarValue::regex(r"^\(\d{3}\) \d{3}-\d{4}$"), ];
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 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 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 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 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 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 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 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 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 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 let base64_part = &yaml_output[9..]; 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 let data = b"test";
1299 let binary_scalar = ScalarValue::binary(data);
1300
1301 let mut styled_binary = binary_scalar;
1303 styled_binary.style = ScalarStyle::DoubleQuoted;
1304
1305 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 #[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 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 assert_eq!(ScalarValue::parse_integer("0o755"), Some(493)); assert_eq!(ScalarValue::parse_integer("0o644"), Some(420)); assert_eq!(ScalarValue::auto_detect_type("0o755"), ScalarType::Integer);
1356
1357 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 assert_eq!(ScalarValue::parse_integer("0o789"), None); assert_eq!(ScalarValue::parse_integer("0o"), None); }
1367
1368 #[test]
1369 fn test_leading_zero_is_decimal() {
1370 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 assert_eq!(ScalarValue::auto_detect_type("0755"), ScalarType::Integer);
1377 assert_eq!(ScalarValue::auto_detect_type("0644"), ScalarType::Integer);
1378
1379 assert_eq!(ScalarValue::parse_integer("0"), Some(0)); assert_eq!(ScalarValue::parse_integer("00"), Some(0)); 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 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 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 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 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 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 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 let plain = ScalarValue::parse("hello");
1464 assert_eq!(plain.scalar_type(), ScalarType::String);
1465 assert_eq!(plain.style, ScalarStyle::Plain);
1466
1467 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 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 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 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 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 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 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 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 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 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 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 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 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 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}