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
107impl ScalarValue {
108 pub fn string(value: impl Into<String>) -> Self {
137 let value = value.into();
138 let style = Self::detect_style(&value);
139 let scalar_type = ScalarType::String;
141 Self {
142 value,
143 style,
144 scalar_type,
145 tag: None,
146 }
147 }
148
149 pub fn with_style(value: impl Into<String>, style: ScalarStyle) -> Self {
151 Self {
152 value: value.into(),
153 style,
154 scalar_type: ScalarType::String,
155 tag: None,
156 }
157 }
158
159 pub fn plain(value: impl Into<String>) -> Self {
161 Self::with_style(value, ScalarStyle::Plain)
162 }
163
164 pub fn single_quoted(value: impl Into<String>) -> Self {
166 Self::with_style(value, ScalarStyle::SingleQuoted)
167 }
168
169 pub fn double_quoted(value: impl Into<String>) -> Self {
171 Self::with_style(value, ScalarStyle::DoubleQuoted)
172 }
173
174 pub fn literal(value: impl Into<String>) -> Self {
176 Self::with_style(value, ScalarStyle::Literal)
177 }
178
179 pub fn folded(value: impl Into<String>) -> Self {
181 Self::with_style(value, ScalarStyle::Folded)
182 }
183
184 pub fn null() -> Self {
186 Self {
187 value: "null".to_string(),
188 style: ScalarStyle::Plain,
189 scalar_type: ScalarType::Null,
190 tag: None,
191 }
192 }
193
194 pub fn timestamp(value: impl Into<String>) -> Self {
196 Self {
197 value: value.into(),
198 style: ScalarStyle::Plain,
199 scalar_type: ScalarType::Timestamp,
200 tag: None,
201 }
202 }
203
204 pub fn regex(pattern: impl Into<String>) -> Self {
206 Self {
207 value: pattern.into(),
208 style: ScalarStyle::Plain,
209 scalar_type: ScalarType::Regex,
210 tag: None,
211 }
212 }
213
214 pub fn with_tag(mut self, tag: impl Into<String>) -> Self {
227 self.tag = Some(tag.into());
228 self
229 }
230
231 pub fn tag(&self) -> Option<&str> {
233 self.tag.as_deref()
234 }
235
236 pub fn value(&self) -> &str {
238 &self.value
239 }
240
241 pub fn style(&self) -> ScalarStyle {
243 self.style
244 }
245
246 pub fn scalar_type(&self) -> ScalarType {
248 self.scalar_type
249 }
250
251 pub fn to_i64(&self) -> Option<i64> {
258 if self.scalar_type == ScalarType::Integer {
259 Self::parse_integer(self.value.trim())
260 } else {
261 None
262 }
263 }
264
265 #[deprecated(since = "0.3.3", note = "to_i64 follows YAML 1.2; use it instead")]
267 pub fn to_i64_yaml_1_2(&self) -> Option<i64> {
268 self.to_i64()
269 }
270
271 pub fn to_f64(&self) -> Option<f64> {
275 if self.scalar_type == ScalarType::Float {
276 Self::parse_float(self.value.trim())
277 } else {
278 None
279 }
280 }
281
282 pub fn to_bool(&self) -> Option<bool> {
288 if self.scalar_type == ScalarType::Boolean {
289 Self::parse_bool(self.value.trim())
290 } else {
291 None
292 }
293 }
294
295 pub fn is_timestamp(&self) -> bool {
297 self.scalar_type == ScalarType::Timestamp
298 }
299
300 pub fn is_regex(&self) -> bool {
302 self.scalar_type == ScalarType::Regex
303 }
304
305 #[cfg(feature = "regex")]
322 pub fn as_regex(&self) -> Option<regex::Regex> {
323 if self.scalar_type == ScalarType::Regex {
324 regex::Regex::new(&self.value).ok()
325 } else {
326 None
327 }
328 }
329
330 #[cfg(feature = "regex")]
348 pub fn try_as_regex(&self) -> Result<regex::Regex, regex::Error> {
349 regex::Regex::new(&self.value)
350 }
351
352 pub fn coerce_to_type(&self, target_type: ScalarType) -> Option<ScalarValue> {
354 if self.scalar_type == target_type {
355 return Some(self.clone());
356 }
357
358 match target_type {
359 ScalarType::String => Some(ScalarValue {
360 value: self.value.clone(),
361 style: ScalarStyle::Plain,
362 scalar_type: ScalarType::String,
363 tag: None,
364 }),
365 ScalarType::Integer => Self::parse_integer(self.value.trim()).map(ScalarValue::from),
366 ScalarType::Float => Self::parse_float(self.value.trim()).map(ScalarValue::from),
367 ScalarType::Boolean => match self.value.to_lowercase().as_str() {
368 "true" | "yes" | "on" | "1" => Some(ScalarValue::from(true)),
369 "false" | "no" | "off" | "0" => Some(ScalarValue::from(false)),
370 _ => None,
371 },
372 ScalarType::Null => match self.value.to_lowercase().as_str() {
373 "null" | "~" | "" => Some(ScalarValue::null()),
374 _ => None,
375 },
376 #[cfg(feature = "base64")]
377 ScalarType::Binary => {
378 if base64_decode(&self.value).is_ok() {
380 Some(ScalarValue {
381 value: self.value.clone(),
382 style: ScalarStyle::Plain,
383 scalar_type: ScalarType::Binary,
384 tag: None,
385 })
386 } else {
387 None
388 }
389 }
390 ScalarType::Timestamp => {
391 if Self::is_valid_timestamp(&self.value) {
393 Some(ScalarValue::timestamp(&self.value))
394 } else {
395 None
396 }
397 }
398 ScalarType::Regex => {
399 Some(ScalarValue::regex(&self.value))
401 }
402 }
403 }
404
405 pub fn parse(value: impl Into<String>) -> Self {
435 let value = value.into();
436 let scalar_type = Self::auto_detect_type(&value);
437 let style = match scalar_type {
440 ScalarType::String => Self::detect_style(&value),
441 _ => ScalarStyle::Plain,
443 };
444
445 Self {
446 value,
447 style,
448 scalar_type,
449 tag: None,
450 }
451 }
452
453 pub fn from_scalar(scalar: &crate::yaml::Scalar) -> Self {
459 let value = scalar.as_string();
460 let raw_text = scalar.value();
461
462 let style = if raw_text.starts_with('"') && raw_text.ends_with('"') {
463 ScalarStyle::DoubleQuoted
464 } else if raw_text.starts_with('\'') && raw_text.ends_with('\'') {
465 ScalarStyle::SingleQuoted
466 } else if scalar.is_block() && raw_text.starts_with('|') {
467 ScalarStyle::Literal
468 } else if scalar.is_block() {
469 ScalarStyle::Folded
470 } else {
471 ScalarStyle::Plain
472 };
473
474 let scalar_type = if style != ScalarStyle::Plain {
483 ScalarType::String
484 } else {
485 Self::classify_plain(raw_text.trim_end()).into()
486 };
487
488 Self {
489 value,
490 style,
491 scalar_type,
492 tag: None,
493 }
494 }
495}
496
497impl fmt::Display for ScalarValue {
498 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
499 write!(f, "{}", self.to_yaml_string())
500 }
501}
502
503impl From<String> for ScalarValue {
504 fn from(value: String) -> Self {
505 Self::string(value)
506 }
507}
508
509impl From<&str> for ScalarValue {
510 fn from(value: &str) -> Self {
511 Self::string(value)
512 }
513}
514
515macro_rules! impl_from_number {
518 ($($t:ty => $ty:expr),* $(,)?) => {
519 $(
520 impl From<$t> for ScalarValue {
521 fn from(value: $t) -> Self {
522 Self {
523 value: value.to_string(),
524 style: ScalarStyle::Plain,
525 scalar_type: $ty,
526 tag: None,
527 }
528 }
529 }
530 )*
531 };
532}
533
534impl_from_number! {
535 i32 => ScalarType::Integer,
536 i64 => ScalarType::Integer,
537}
538
539macro_rules! impl_from_float {
540 ($($t:ty),* $(,)?) => {
541 $(
542 impl From<$t> for ScalarValue {
543 fn from(value: $t) -> Self {
544 Self {
545 value: Self::format_float(value.to_string()),
546 style: ScalarStyle::Plain,
547 scalar_type: ScalarType::Float,
548 tag: None,
549 }
550 }
551 }
552 )*
553 };
554}
555
556impl_from_float!(f32, f64);
557
558impl From<bool> for ScalarValue {
559 fn from(value: bool) -> Self {
560 Self {
561 value: if value { "true" } else { "false" }.to_string(),
562 style: ScalarStyle::Plain,
563 scalar_type: ScalarType::Boolean,
564 tag: None,
565 }
566 }
567}
568
569impl From<crate::yaml::Scalar> for ScalarValue {
571 fn from(scalar: crate::yaml::Scalar) -> Self {
572 Self::from_scalar(&scalar)
573 }
574}
575
576impl crate::AsYaml for ScalarValue {
577 fn as_node(&self) -> Option<&crate::yaml::SyntaxNode> {
578 None
579 }
580
581 fn kind(&self) -> crate::as_yaml::YamlKind {
582 match self.rendered_tag() {
583 Some(tag) => crate::as_yaml::YamlKind::Tagged(std::borrow::Cow::Owned(tag.to_string())),
584 None => crate::as_yaml::YamlKind::Scalar,
585 }
586 }
587
588 fn build_content(
589 &self,
590 builder: &mut rowan::GreenNodeBuilder,
591 indent: usize,
592 flow_context: bool,
593 ) -> bool {
594 self.build(
595 builder,
596 indent,
597 flow_context.then_some(FlowQuoting::Minimal),
598 )
599 }
600
601 fn build_flow_content(
602 &self,
603 builder: &mut rowan::GreenNodeBuilder,
604 indent: usize,
605 quoting: FlowQuoting,
606 ) -> bool {
607 self.build(builder, indent, Some(quoting))
608 }
609
610 fn is_inline(&self) -> bool {
611 true
612 }
613}
614
615#[cfg(test)]
616mod tests {
617 use super::*;
618
619 #[test]
620 fn test_plain_scalars() {
621 let scalar = ScalarValue::string("simple");
622 assert_eq!(scalar.to_yaml_string(), "simple");
623
624 let scalar = ScalarValue::string("hello world");
625 assert_eq!(scalar.to_yaml_string(), "hello world");
626 }
627
628 #[test]
629 fn test_values_needing_quotes() {
630 let scalar = ScalarValue::string("true");
632 assert_eq!(scalar.to_yaml_string(), "'true'");
633
634 let scalar = ScalarValue::string("false");
635 assert_eq!(scalar.to_yaml_string(), "'false'");
636
637 let scalar = ScalarValue::string("yes");
638 assert_eq!(scalar.to_yaml_string(), "'yes'");
639
640 let scalar = ScalarValue::string("no");
641 assert_eq!(scalar.to_yaml_string(), "'no'");
642
643 let scalar = ScalarValue::string("null");
645 assert_eq!(scalar.to_yaml_string(), "'null'");
646
647 let scalar = ScalarValue::string("~");
648 assert_eq!(scalar.to_yaml_string(), "'~'");
649
650 let scalar = ScalarValue::string("123");
652 assert_eq!(scalar.to_yaml_string(), "'123'");
653
654 let scalar = ScalarValue::string("3.14");
655 assert_eq!(scalar.to_yaml_string(), "'3.14'");
656
657 let scalar = ScalarValue::string("value: something");
659 assert_eq!(scalar.to_yaml_string(), "'value: something'");
660
661 let scalar = ScalarValue::string("# comment");
662 assert_eq!(scalar.to_yaml_string(), "'# comment'");
663
664 let scalar = ScalarValue::string(" spaces ");
666 assert_eq!(scalar.to_yaml_string(), "' spaces '");
667 }
668
669 #[test]
670 fn test_single_quoted() {
671 let scalar = ScalarValue::single_quoted("value with 'quotes'");
672 assert_eq!(scalar.to_yaml_string(), "'value with ''quotes'''");
673 }
674
675 #[test]
676 fn test_double_quoted() {
677 let scalar = ScalarValue::double_quoted("value with \"quotes\" and \\backslash");
678 assert_eq!(
679 scalar.to_yaml_string(),
680 "\"value with \\\"quotes\\\" and \\\\backslash\""
681 );
682
683 let scalar = ScalarValue::double_quoted("line1\nline2\ttab");
684 assert_eq!(scalar.to_yaml_string(), "\"line1\\nline2\\ttab\"");
685 }
686
687 #[test]
688 fn test_carriage_return_is_escaped() {
689 assert_eq!(ScalarValue::plain("a\rb").to_yaml_string(), "\"a\\rb\"");
690 assert_eq!(
691 ScalarValue::single_quoted("a\rb").to_yaml_string(),
692 "\"a\\rb\""
693 );
694 }
695
696 #[test]
697 fn test_multiline() {
698 let scalar = ScalarValue::string("line1\nline2\nline3");
699 assert_eq!(scalar.style(), ScalarStyle::Literal);
701 }
702
703 #[test]
704 fn test_from_types() {
705 let scalar = ScalarValue::from(42);
706 assert_eq!(scalar.to_yaml_string(), "42");
707
708 let scalar = ScalarValue::from(1.234);
709 assert_eq!(scalar.to_yaml_string(), "1.234");
710
711 let scalar = ScalarValue::from(true);
712 assert_eq!(scalar.to_yaml_string(), "true");
713
714 let scalar = ScalarValue::from(false);
715 assert_eq!(scalar.to_yaml_string(), "false");
716 }
717
718 #[test]
719 fn test_empty_string() {
720 let scalar = ScalarValue::string("");
721 assert_eq!(scalar.to_yaml_string(), "''");
722 }
723
724 #[test]
725 fn test_special_start_chars() {
726 let scalar = ScalarValue::string("-item");
727 assert_eq!(scalar.to_yaml_string(), "'-item'");
728
729 let scalar = ScalarValue::string("?key");
730 assert_eq!(scalar.to_yaml_string(), "'?key'");
731
732 let scalar = ScalarValue::string("[array]");
733 assert_eq!(scalar.to_yaml_string(), "'[array]'");
734 }
735
736 #[test]
737 fn test_null_scalar() {
738 let scalar = ScalarValue::null();
739 assert_eq!(scalar.to_yaml_string(), "null");
740 assert_eq!(scalar.scalar_type, ScalarType::Null);
741 }
742
743 #[test]
744 fn test_indentation_preservation() {
745 let content_with_indent = " Line 1\n Line 2 more indented\n Line 3";
747 let scalar = ScalarValue::literal(content_with_indent);
748
749 let yaml_output = scalar.to_literal_with_indent(0, 2);
752 assert_eq!(
753 yaml_output,
754 "|2-\n Line 1\n Line 2 more indented\n Line 3"
755 );
756 }
757
758 #[test]
759 fn test_literal_with_custom_indent() {
760 let content = "Line 1\nLine 2\nLine 3";
762 let scalar = ScalarValue::literal(content);
763
764 let yaml_4_spaces = scalar.to_literal_with_indent(0, 4);
765 assert_eq!(yaml_4_spaces, "|-\n Line 1\n Line 2\n Line 3");
766
767 let yaml_1_space = scalar.to_literal_with_indent(0, 1);
768 assert_eq!(yaml_1_space, "|-\n Line 1\n Line 2\n Line 3");
769 }
770
771 #[test]
772 fn test_block_indent_counts_from_parent_column() {
773 let scalar = ScalarValue::literal(" x\n y");
774 assert_eq!(scalar.to_yaml_string(), "|2-\n x\n y");
775 assert_eq!(
776 scalar.to_yaml_string_with_indent(2, 2),
777 "|2-\n x\n y"
778 );
779 assert_eq!(
780 scalar.to_literal_with_indent(4, 3),
781 "|3-\n x\n y"
782 );
783 assert_eq!(
784 ScalarValue::folded(" x\n").to_folded_with_indent(2, 2),
785 ">2\n x"
786 );
787 assert_eq!(
788 ScalarValue::literal("x\ny\n").to_yaml_string_with_indent(2, 2),
789 "|\n x\n y"
790 );
791 }
792
793 #[test]
794 fn test_block_indicator_when_first_line_is_indented() {
795 assert_eq!(
798 ScalarValue::literal(" a\nb").to_yaml_string(),
799 "|2-\n a\n b"
800 );
801 assert_eq!(
802 ScalarValue::literal(" L1\n L2\n L3").to_yaml_string_with_indent(2, 2),
803 "|2-\n L1\n L2\n L3"
804 );
805 assert_eq!(
806 ScalarValue::literal(" \nx").to_yaml_string(),
807 "|2-\n \n x"
808 );
809 assert_eq!(
810 ScalarValue::literal("a\n b").to_yaml_string(),
811 "|-\n a\n b"
812 );
813 }
814
815 #[test]
816 fn test_block_keeps_whitespace_only_lines() {
817 assert_eq!(
818 ScalarValue::literal("a\n \nb").to_yaml_string(),
819 "|-\n a\n \n b"
820 );
821 }
822
823 #[test]
824 #[should_panic(expected = "does not fit in one digit")]
825 fn test_block_indent_indicator_too_large() {
826 ScalarValue::literal(" x").to_literal_with_indent(0, 10);
827 }
828
829 #[test]
830 #[should_panic(expected = "must be indented")]
831 fn test_block_indent_zero() {
832 ScalarValue::literal("x").to_literal_with_indent(2, 0);
833 }
834
835 #[test]
836 fn test_folded_with_custom_indent() {
837 let content = "Line 1\nLine 2\nLine 3";
839 let scalar = ScalarValue::folded(content);
840
841 let yaml_3_spaces = scalar.to_folded_with_indent(0, 3);
842 assert_eq!(yaml_3_spaces, ">-\n Line 1\n\n Line 2\n\n Line 3");
843 }
844
845 #[test]
846 fn test_block_scalar_first_line_more_indented() {
847 assert_eq!(
850 ScalarValue::string(" a\nb").to_yaml_string(),
851 "|2-\n a\n b"
852 );
853 assert_eq!(
854 ScalarValue::string(" x\n y").to_yaml_string_with_indent(2, 2),
855 "|2-\n x\n y"
856 );
857 assert_eq!(
858 ScalarValue::string("\n x").to_yaml_string_with_indent(2, 2),
859 "|2-\n\n x"
860 );
861 }
862
863 #[test]
864 fn test_block_scalar_whitespace_only_lines() {
865 assert_eq!(
866 ScalarValue::literal("a\n ").to_yaml_string(),
867 "|-\n a\n "
868 );
869 assert_eq!(
870 ScalarValue::literal("a\n \nb").to_yaml_string(),
871 "|-\n a\n \n b"
872 );
873 assert_eq!(ScalarValue::literal(" ").to_yaml_string(), "|2-\n ");
874 assert_eq!(ScalarValue::literal(" \n").to_yaml_string(), "|2\n ");
875 assert_eq!(ScalarValue::string("a\n ").to_yaml_string(), "\"a\\n \"");
877 assert_eq!(ScalarValue::string(" \n").to_yaml_string(), "\" \\n\"");
878 }
879
880 #[test]
881 fn test_folded_preserves_line_breaks() {
882 assert_eq!(
883 ScalarValue::folded("a\nb").to_yaml_string(),
884 ">-\n a\n\n b"
885 );
886 assert_eq!(
889 ScalarValue::folded("a\n\nb\n c\nd\n").to_yaml_string(),
890 ">\n a\n\n\n b\n c\n d"
891 );
892 }
893
894 #[test]
895 fn test_single_quoted_line_breaks() {
896 assert_eq!(
898 ScalarValue::single_quoted("a\nb").to_yaml_string(),
899 "'a\n\n b'"
900 );
901 assert_eq!(
902 ScalarValue::single_quoted("a\n\nb").to_yaml_string(),
903 "'a\n\n\n b'"
904 );
905 assert_eq!(
906 ScalarValue::single_quoted("\nb\n").to_yaml_string(),
907 "'\n\n b\n\n'"
908 );
909 assert_eq!(
910 ScalarValue::single_quoted("a\nb").to_yaml_string_with_indent(2, 2),
911 "'a\n\n b'"
912 );
913 assert_eq!(
916 ScalarValue::single_quoted("a \nb").to_yaml_string(),
917 "\"a \\nb\""
918 );
919 assert_eq!(
920 ScalarValue::single_quoted("a\n b").to_yaml_string(),
921 "\"a\\n b\""
922 );
923 }
924
925 #[test]
926 fn test_mixed_empty_lines_preservation() {
927 let content_with_empty_lines = "Line 1\n\nLine 3\n\n\nLine 6";
929 let scalar = ScalarValue::literal(content_with_empty_lines);
930
931 let yaml_output = scalar.to_literal_with_indent(0, 2);
932 assert_eq!(yaml_output, "|-\n Line 1\n\n Line 3\n\n\n Line 6");
933
934 let lines: Vec<&str> = yaml_output.lines().collect();
937 let empty_line_count = lines.iter().filter(|line| line.is_empty()).count();
938 assert_eq!(empty_line_count, 3);
939 }
940
941 #[test]
942 fn test_double_quoted_with_escapes() {
943 let original = "Hello\nWorld\t😃";
945 let scalar = ScalarValue::double_quoted(original);
946 let yaml_string = scalar.to_yaml_string();
947
948 assert_eq!(yaml_string, "\"Hello\\nWorld\\t\\U0001F603\"");
950
951 let parsed = ScalarValue::parse_escape_sequences(&yaml_string[1..yaml_string.len() - 1]);
953 assert_eq!(parsed, original);
954 }
955
956 #[test]
957 fn test_unicode_output_formatting() {
958 let scalar = ScalarValue::double_quoted("Hello 世界 🌍");
960 let yaml_string = scalar.to_yaml_string();
961
962 assert_eq!(yaml_string, "\"Hello \\u4E16\\u754C \\U0001F30D\"");
964
965 assert_eq!(scalar.value(), "Hello 世界 🌍");
967 }
968
969 #[test]
970 fn test_timestamp_creation_and_validation() {
971 let valid_timestamps = [
973 "2023-12-25",
974 "2023-12-25T10:30:45",
975 "2023-12-25 10:30:45",
976 "2023-12-25T10:30:45Z",
977 "2001-12-14 21:59:43.10 -5", "2001-12-15T02:59:43.1Z", "2001-12-14t21:59:43.10-05:00", ];
981
982 for ts in valid_timestamps {
983 let scalar = ScalarValue::timestamp(ts);
984 assert!(scalar.is_timestamp());
985 assert_eq!(scalar.scalar_type(), ScalarType::Timestamp);
986 assert_eq!(scalar.value(), ts);
987
988 let yaml_output = scalar.to_yaml_string();
989 assert_eq!(yaml_output, format!("!!timestamp {}", ts));
990
991 let auto_scalar = ScalarValue::parse(ts);
993 assert_eq!(
994 auto_scalar.scalar_type(),
995 ScalarType::Timestamp,
996 "Failed to auto-detect '{}' as timestamp",
997 ts
998 );
999 }
1000
1001 let invalid_timestamps = [
1003 "not-a-date",
1004 "2023-13-01", "2023-12-32", "12:34:56", "2023/12/25", ];
1009
1010 for ts in invalid_timestamps {
1011 let auto_scalar = ScalarValue::parse(ts);
1012 assert_ne!(
1013 auto_scalar.scalar_type(),
1014 ScalarType::Timestamp,
1015 "'{}' should not be detected as timestamp",
1016 ts
1017 );
1018 }
1019 }
1020
1021 #[test]
1022 fn test_regex_creation() {
1023 let pattern = r"^\d{3}-\d{2}-\d{4}$";
1024 let scalar = ScalarValue::regex(pattern);
1025
1026 assert!(scalar.is_regex());
1027 assert_eq!(scalar.scalar_type(), ScalarType::Regex);
1028 assert_eq!(scalar.value(), pattern);
1029
1030 let yaml_output = scalar.to_yaml_string();
1031 assert_eq!(yaml_output, format!("!!regex {}", pattern));
1032 }
1033
1034 #[test]
1035 fn test_regex_edge_cases() {
1036 let empty_regex = ScalarValue::regex("");
1038 assert!(empty_regex.is_regex());
1039 assert_eq!(empty_regex.value(), "");
1040 assert_eq!(empty_regex.to_yaml_string(), "!!regex");
1041
1042 let special_chars = ScalarValue::regex(r"[.*+?^${}()|[\]\\]");
1044 assert!(special_chars.is_regex());
1045 assert_eq!(special_chars.value(), r"[.*+?^${}()|[\]\\]");
1046
1047 let unicode_regex = ScalarValue::regex(r"\p{L}+");
1049 assert!(unicode_regex.is_regex());
1050 assert_eq!(unicode_regex.value(), r"\p{L}+");
1051
1052 let long_pattern = "a".repeat(1000);
1054 let long_regex = ScalarValue::regex(&long_pattern);
1055 assert!(long_regex.is_regex());
1056 assert_eq!(long_regex.value(), long_pattern);
1057
1058 let quoted_regex = ScalarValue::regex(r#"'quoted' and "double quoted" with \\ backslash"#);
1060 assert!(quoted_regex.is_regex());
1061 assert_eq!(
1062 quoted_regex.value(),
1063 r#"'quoted' and "double quoted" with \\ backslash"#
1064 );
1065 }
1066
1067 #[test]
1068 fn test_regex_type_coercion() {
1069 let regex_scalar = ScalarValue::regex(r"\d+");
1070
1071 let string_scalar = regex_scalar.coerce_to_type(ScalarType::String).unwrap();
1073 assert_eq!(string_scalar.scalar_type(), ScalarType::String);
1074 assert_eq!(string_scalar.value(), r"\d+");
1075 assert!(!string_scalar.is_regex());
1076
1077 let str_scalar = ScalarValue::string("test.*");
1079 let regex_from_string = str_scalar.coerce_to_type(ScalarType::Regex).unwrap();
1080 assert_eq!(regex_from_string.scalar_type(), ScalarType::Regex);
1081 assert_eq!(regex_from_string.value(), "test.*");
1082 assert!(regex_from_string.is_regex());
1083
1084 assert!(regex_scalar.coerce_to_type(ScalarType::Integer).is_none());
1086 assert!(regex_scalar.coerce_to_type(ScalarType::Float).is_none());
1087 assert!(regex_scalar.coerce_to_type(ScalarType::Boolean).is_none());
1088 }
1089
1090 #[test]
1091 #[cfg(feature = "regex")]
1092 fn test_regex_compilation() {
1093 let regex_scalar = ScalarValue::regex(r"\d{3}-\d{4}");
1095 let compiled = regex_scalar.as_regex().unwrap();
1096 assert!(compiled.is_match("555-1234"));
1097 assert!(!compiled.is_match("not-a-phone"));
1098
1099 let string_scalar = ScalarValue::string("not a regex");
1101 assert!(string_scalar.as_regex().is_none());
1102
1103 let pattern_scalar = ScalarValue::string(r"^\w+@\w+\.\w+$");
1105 let email_regex = pattern_scalar.try_as_regex().unwrap();
1106 assert!(email_regex.is_match("test@example.com"));
1107 assert!(!email_regex.is_match("not-an-email"));
1108
1109 let invalid_scalar = ScalarValue::regex(r"[invalid(");
1111 assert!(invalid_scalar.as_regex().is_none());
1112
1113 let invalid_pattern = ScalarValue::string(r"[invalid(");
1115 assert!(invalid_pattern.try_as_regex().is_err());
1116 }
1117
1118 #[test]
1119 #[cfg(feature = "regex")]
1120 fn test_regex_extraction_use_cases() {
1121 let validation_rules = [
1123 ScalarValue::regex(r"^\d{5}$"), ScalarValue::regex(r"^[A-Z]{2}$"), ScalarValue::regex(r"^\(\d{3}\) \d{3}-\d{4}$"), ];
1127
1128 let test_values = ["12345", "CA", "(555) 123-4567"];
1129
1130 for (rule, value) in validation_rules.iter().zip(test_values.iter()) {
1131 let regex = rule.as_regex().unwrap();
1132 assert!(regex.is_match(value), "Pattern should match {}", value);
1133 }
1134
1135 let email_regex = ScalarValue::regex(r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$");
1137 let compiled = email_regex.as_regex().unwrap();
1138 assert!(compiled.is_match("user@example.com"));
1139 assert!(compiled.is_match("test.user+tag@sub.domain.org"));
1140 assert!(!compiled.is_match("invalid.email"));
1141
1142 let version_regex = ScalarValue::regex(r"^v(\d+)\.(\d+)\.(\d+)$");
1144 let compiled = version_regex.as_regex().unwrap();
1145 if let Some(captures) = compiled.captures("v1.2.3") {
1146 assert_eq!(captures.get(1).unwrap().as_str(), "1");
1147 assert_eq!(captures.get(2).unwrap().as_str(), "2");
1148 assert_eq!(captures.get(3).unwrap().as_str(), "3");
1149 } else {
1150 panic!("Should have matched version string");
1151 }
1152 }
1153
1154 #[test]
1155 fn test_type_coercion() {
1156 let str_scalar = ScalarValue::string("42");
1158 let int_scalar = str_scalar.coerce_to_type(ScalarType::Integer).unwrap();
1159 assert_eq!(int_scalar.scalar_type(), ScalarType::Integer);
1160 assert_eq!(int_scalar.value(), "42");
1161
1162 let bool_scalar = ScalarValue::string("true")
1164 .coerce_to_type(ScalarType::Boolean)
1165 .unwrap();
1166 assert_eq!(bool_scalar.scalar_type(), ScalarType::Boolean);
1167 assert_eq!(bool_scalar.value(), "true");
1168
1169 let yes_scalar = ScalarValue::string("yes")
1171 .coerce_to_type(ScalarType::Boolean)
1172 .unwrap();
1173 assert_eq!(yes_scalar.value(), "true");
1174
1175 let no_scalar = ScalarValue::string("no")
1176 .coerce_to_type(ScalarType::Boolean)
1177 .unwrap();
1178 assert_eq!(no_scalar.value(), "false");
1179
1180 let str_scalar = ScalarValue::string("not_a_number");
1182 assert!(str_scalar.coerce_to_type(ScalarType::Integer).is_none());
1183 }
1184
1185 #[test]
1186 fn test_auto_type_detection() {
1187 assert_eq!(ScalarValue::auto_detect_type("42"), ScalarType::Integer);
1189 assert_eq!(ScalarValue::auto_detect_type("3.14"), ScalarType::Float);
1190 assert_eq!(ScalarValue::auto_detect_type("true"), ScalarType::Boolean);
1191 assert_eq!(ScalarValue::auto_detect_type("false"), ScalarType::Boolean);
1192 assert_eq!(ScalarValue::auto_detect_type("yes"), ScalarType::String);
1193 assert_eq!(ScalarValue::auto_detect_type("NuLl"), ScalarType::String);
1194 assert_eq!(ScalarValue::auto_detect_type("null"), ScalarType::Null);
1195 assert_eq!(ScalarValue::auto_detect_type("~"), ScalarType::Null);
1196 assert_eq!(ScalarValue::auto_detect_type(""), ScalarType::Null);
1197 assert_eq!(
1198 ScalarValue::auto_detect_type("2023-12-25"),
1199 ScalarType::Timestamp
1200 );
1201 assert_eq!(
1202 ScalarValue::auto_detect_type("2023-12-25T10:30:45"),
1203 ScalarType::Timestamp
1204 );
1205 assert_eq!(
1207 ScalarValue::auto_detect_type("SGVsbG8gV29ybGQ="),
1208 ScalarType::String
1209 );
1210 assert_eq!(
1211 ScalarValue::auto_detect_type("hello world"),
1212 ScalarType::String
1213 );
1214 }
1215
1216 #[test]
1217 fn test_from_yaml_scalar_creation() {
1218 let int_scalar = ScalarValue::parse("123");
1219 assert_eq!(int_scalar.scalar_type(), ScalarType::Integer);
1220
1221 let bool_scalar = ScalarValue::parse("true");
1222 assert_eq!(bool_scalar.scalar_type(), ScalarType::Boolean);
1223
1224 let timestamp_scalar = ScalarValue::parse("2023-12-25");
1225 assert_eq!(timestamp_scalar.scalar_type(), ScalarType::Timestamp);
1226
1227 let string_scalar = ScalarValue::parse("hello world");
1228 assert_eq!(string_scalar.scalar_type(), ScalarType::String);
1229 }
1230
1231 #[test]
1232 fn test_yaml_1_1_timestamp_examples() {
1233 for text in [
1236 "2001-12-15T02:59:43.1Z",
1237 "2001-12-14t21:59:43.10-05:00",
1238 "2001-12-14 21:59:43.10 -5",
1239 "2001-12-15 2:59:43.10",
1240 "2002-12-14",
1241 "2001-1-5T1:02:03Z",
1242 "2001-12-14 21:59:43",
1243 "2001-12-14\t21:59:43",
1244 ] {
1245 assert_eq!(
1246 ScalarValue::parse(text).scalar_type(),
1247 ScalarType::Timestamp,
1248 "{text:?}"
1249 );
1250 }
1251 for text in [
1252 "2001-1-5",
1253 "2001-12-5",
1254 "2001-1-15",
1255 "2001-12-14T",
1256 "2001-12-14 123:59:43",
1257 "2001-12-14T21:5:43",
1258 "2001-12-14T21:59:4",
1259 "2001-12-14x21:59:43",
1260 "2001-12-14T24:00:00",
1261 "2001-12-32",
1262 ] {
1263 assert_eq!(
1264 ScalarValue::parse(text).scalar_type(),
1265 ScalarType::String,
1266 "{text:?}"
1267 );
1268 }
1269 }
1270
1271 #[test]
1272 #[cfg(feature = "base64")]
1273 fn test_binary_yaml_output_with_tags() {
1274 let data = b"Binary data here";
1275 let scalar = ScalarValue::binary(data);
1276 let yaml_output = scalar.to_yaml_string();
1277
1278 assert!(yaml_output.starts_with("!!binary "));
1279
1280 let base64_part = &yaml_output[9..]; let decoded = base64_decode(base64_part).expect("Should decode");
1283 assert_eq!(decoded, data);
1284 }
1285
1286 #[test]
1287 #[cfg(feature = "base64")]
1288 fn test_special_data_types_with_different_styles() {
1289 let data = b"test";
1291 let binary_scalar = ScalarValue::binary(data);
1292
1293 let mut styled_binary = binary_scalar;
1295 styled_binary.style = ScalarStyle::DoubleQuoted;
1296
1297 assert_eq!(styled_binary.to_yaml_string(), "!!binary \"dGVzdA==\"");
1299 }
1300
1301 #[test]
1302 fn test_type_checking_methods() {
1303 #[cfg(feature = "base64")]
1304 let binary_scalar = ScalarValue::binary(b"test");
1305 let timestamp_scalar = ScalarValue::timestamp("2023-12-25");
1306 let regex_scalar = ScalarValue::regex(r"\d+");
1307 let string_scalar = ScalarValue::string("hello");
1308
1309 #[cfg(feature = "base64")]
1311 assert!(binary_scalar.is_binary());
1312 #[cfg(feature = "base64")]
1313 assert!(!binary_scalar.is_timestamp());
1314 #[cfg(feature = "base64")]
1315 assert!(!binary_scalar.is_regex());
1316
1317 #[cfg(feature = "base64")]
1318 assert!(!timestamp_scalar.is_binary());
1319 assert!(timestamp_scalar.is_timestamp());
1320 assert!(!timestamp_scalar.is_regex());
1321
1322 #[cfg(feature = "base64")]
1323 assert!(!regex_scalar.is_binary());
1324 assert!(!regex_scalar.is_timestamp());
1325 assert!(regex_scalar.is_regex());
1326
1327 #[cfg(feature = "base64")]
1328 assert!(!string_scalar.is_binary());
1329 assert!(!string_scalar.is_timestamp());
1330 assert!(!string_scalar.is_regex());
1331 }
1332
1333 #[test]
1334 fn test_binary_is_not_an_integer() {
1335 for text in ["0b1010", "0B101", "-0b1010", "+0b101"] {
1337 assert_eq!(ScalarValue::parse_integer(text), None, "{text}");
1338 assert_eq!(ScalarValue::auto_detect_type(text), ScalarType::String);
1339 }
1340 }
1341
1342 #[test]
1343 fn test_modern_octal_number_parsing() {
1344 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);
1348
1349 for text in ["0O777", "-0o755", "+0o644"] {
1351 assert_eq!(ScalarValue::parse_integer(text), None, "{text}");
1352 assert_eq!(ScalarValue::auto_detect_type(text), ScalarType::String);
1353 }
1354
1355 assert_eq!(ScalarValue::parse_integer("0o789"), None); assert_eq!(ScalarValue::parse_integer("0o"), None); }
1359
1360 #[test]
1361 fn test_leading_zero_is_decimal() {
1362 assert_eq!(ScalarValue::parse_integer("0755"), Some(755));
1364 assert_eq!(ScalarValue::parse_integer("0644"), Some(644));
1365 assert_eq!(ScalarValue::parse_integer("-0777"), Some(-777));
1366
1367 assert_eq!(ScalarValue::auto_detect_type("0755"), ScalarType::Integer);
1369 assert_eq!(ScalarValue::auto_detect_type("0644"), ScalarType::Integer);
1370
1371 assert_eq!(ScalarValue::parse_integer("0"), Some(0)); assert_eq!(ScalarValue::parse_integer("00"), Some(0)); assert_eq!(ScalarValue::parse_integer("0789"), Some(789));
1376 assert_eq!(ScalarValue::parse_integer("08"), Some(8));
1377 }
1378
1379 #[test]
1380 fn test_hexadecimal_number_parsing() {
1381 assert_eq!(ScalarValue::parse_integer("0xFF"), Some(255));
1383 assert_eq!(ScalarValue::parse_integer("0x1A"), Some(26));
1384 assert_eq!(ScalarValue::parse_integer("0xff"), Some(255));
1385 assert_eq!(ScalarValue::auto_detect_type("0xFF"), ScalarType::Integer);
1386
1387 for text in ["0XFF", "-0xFF", "+0x1A", "+0x1F", "-0o7", "0X1F", "0O7"] {
1389 assert_eq!(ScalarValue::parse_integer(text), None, "{text}");
1390 assert_eq!(ScalarValue::auto_detect_type(text), ScalarType::String);
1391 assert_eq!(
1392 ScalarValue::classify_plain(text),
1393 CoreScalarType::String,
1394 "{text}"
1395 );
1396 }
1397 }
1398
1399 #[test]
1400 fn test_decimal_number_parsing() {
1401 assert_eq!(ScalarValue::parse_integer("42"), Some(42));
1403 assert_eq!(ScalarValue::parse_integer("123"), Some(123));
1404 assert_eq!(ScalarValue::parse_integer("-42"), Some(-42));
1405 assert_eq!(ScalarValue::parse_integer("+123"), Some(123));
1406
1407 assert_eq!(ScalarValue::auto_detect_type("42"), ScalarType::Integer);
1409 assert_eq!(ScalarValue::auto_detect_type("-123"), ScalarType::Integer);
1410 }
1411
1412 #[test]
1413 fn test_number_format_yaml_output() {
1414 let binary_scalar = ScalarValue::parse("0b1010");
1416 assert_eq!(binary_scalar.scalar_type(), ScalarType::String);
1417 assert_eq!(binary_scalar.to_yaml_string(), "'0b1010'");
1418
1419 let octal_scalar = ScalarValue::parse("0o755");
1420 assert_eq!(octal_scalar.scalar_type(), ScalarType::Integer);
1421 assert_eq!(octal_scalar.value(), "0o755");
1422
1423 let hex_scalar = ScalarValue::parse("0xFF");
1424 assert_eq!(hex_scalar.scalar_type(), ScalarType::Integer);
1425 assert_eq!(hex_scalar.value(), "0xFF");
1426
1427 let legacy_octal_scalar = ScalarValue::parse("0755");
1428 assert_eq!(legacy_octal_scalar.scalar_type(), ScalarType::Integer);
1429 assert_eq!(legacy_octal_scalar.value(), "0755");
1430 assert_eq!(legacy_octal_scalar.to_i64(), Some(755));
1431 }
1432
1433 #[test]
1434 fn test_coerce_to_null() {
1435 let s = ScalarValue::string("null");
1437 assert_eq!(
1438 s.coerce_to_type(ScalarType::Null).map(|v| v.scalar_type()),
1439 Some(ScalarType::Null)
1440 );
1441 assert!(ScalarValue::string("~")
1442 .coerce_to_type(ScalarType::Null)
1443 .is_some());
1444 assert!(ScalarValue::string("")
1445 .coerce_to_type(ScalarType::Null)
1446 .is_some());
1447 assert!(ScalarValue::string("notnull")
1448 .coerce_to_type(ScalarType::Null)
1449 .is_none());
1450 }
1451
1452 #[test]
1453 fn test_parse_preserves_string_style() {
1454 let plain = ScalarValue::parse("hello");
1456 assert_eq!(plain.scalar_type(), ScalarType::String);
1457 assert_eq!(plain.style, ScalarStyle::Plain);
1458
1459 let multiline = ScalarValue::parse("multi line\nvalue");
1462 assert_eq!(multiline.scalar_type(), ScalarType::String);
1463 assert_eq!(multiline.style, ScalarStyle::Literal);
1464 }
1465
1466 #[test]
1467 fn test_from_scalar_type_and_style() {
1468 use crate::yaml::Document;
1469 use rowan::ast::AstNode;
1470 use std::str::FromStr;
1471
1472 let doc = Document::from_str("k: null\n").unwrap();
1474 let mapping = doc.as_mapping().unwrap();
1475 let null_node = mapping.get("k").unwrap();
1476 let null_scalar = crate::yaml::Scalar::cast(null_node.syntax().clone()).unwrap();
1477 let sv = ScalarValue::from_scalar(&null_scalar);
1478 assert_eq!(sv.scalar_type(), ScalarType::Null);
1479 assert_eq!(sv.style, ScalarStyle::Plain);
1480
1481 let doc = Document::from_str("k: \"hi\"\n").unwrap();
1483 let node = doc.as_mapping().unwrap().get("k").unwrap();
1484 let scalar = crate::yaml::Scalar::cast(node.syntax().clone()).unwrap();
1485 let sv = ScalarValue::from_scalar(&scalar);
1486 assert_eq!(sv.scalar_type(), ScalarType::String);
1487 assert_eq!(sv.style, ScalarStyle::DoubleQuoted);
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.style, ScalarStyle::SingleQuoted);
1495 }
1496
1497 #[test]
1498 fn test_from_scalar_ampersand_in_plain_is_string() {
1499 use crate::yaml::Document;
1503 use rowan::ast::AstNode;
1504 use std::str::FromStr;
1505
1506 let doc = Document::from_str("k: 3.1&1\n").unwrap();
1507 let node = doc.as_mapping().unwrap().get("k").unwrap();
1508 let scalar = crate::yaml::Scalar::cast(node.syntax().clone()).unwrap();
1509 let sv = ScalarValue::from_scalar(&scalar);
1510 assert_eq!(sv.scalar_type(), ScalarType::String);
1511 assert_eq!(sv.style, ScalarStyle::Plain);
1512 }
1513
1514 #[test]
1515 fn test_from_scalar_signed_yaml_infinity_is_float() {
1516 use crate::yaml::Document;
1519 use rowan::ast::AstNode;
1520 use std::str::FromStr;
1521
1522 let doc = Document::from_str("k: +.INF\n").unwrap();
1523 let node = doc.as_mapping().unwrap().get("k").unwrap();
1524 let scalar = crate::yaml::Scalar::cast(node.syntax().clone()).unwrap();
1525 let sv = ScalarValue::from_scalar(&scalar);
1526 assert_eq!(sv.scalar_type(), ScalarType::Float);
1527 }
1528
1529 #[test]
1530 fn test_classify_plain_bare_inf_nan_is_string() {
1531 for input in [
1535 "inf", "Inf", "InF", "INF", "iNf", "nan", "NaN", "NAN", "nAn", "infinity", "Infinity",
1536 "INFINITY", "+inf", "-Inf", "+nan",
1537 ] {
1538 assert_eq!(
1539 ScalarValue::classify_plain(input),
1540 CoreScalarType::String,
1541 "bare {input:?} should classify as String"
1542 );
1543 }
1544 for input in [".inf", ".Inf", ".INF", "+.inf", "-.INF", ".nan"] {
1546 assert_eq!(
1547 ScalarValue::classify_plain(input),
1548 CoreScalarType::Float,
1549 "{input:?} should classify as Float"
1550 );
1551 }
1552 }
1553
1554 #[test]
1555 fn test_canonical_integer() {
1556 for (text, expected) in [
1557 ("0", "0"),
1558 ("-0", "0"),
1559 ("+000", "0"),
1560 ("0x0", "0"),
1561 ("0o0", "0"),
1562 ("42", "42"),
1563 ("+0042", "42"),
1564 ("-42", "-42"),
1565 ("0x2A", "42"),
1566 ("0o52", "42"),
1567 ("-9223372036854775808", "-9223372036854775808"),
1568 ("9223372036854775808", "9223372036854775808"),
1569 ("+9223372036854775808", "9223372036854775808"),
1570 ("0x10000000000000000", "18446744073709551616"),
1571 ("0o2000000000000000000000", "18446744073709551616"),
1572 ("0x1000000000", "68719476736"),
1573 (
1574 "0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF",
1575 "340282366920938463463374607431768211455",
1576 ),
1577 (
1578 "0x100000000000000000000000000000000",
1579 "340282366920938463463374607431768211456",
1580 ),
1581 (
1582 "-000100000000000000000000000000",
1583 "-100000000000000000000000000",
1584 ),
1585 ] {
1586 assert_eq!(
1587 ScalarValue::canonical_integer(text).as_deref(),
1588 Some(expected),
1589 "{text}"
1590 );
1591 }
1592 for text in [
1593 "", "+", "-", "0x", "0o8", "-0x1", "+0o1", "1_000", "1.0", " 1",
1594 ] {
1595 assert_eq!(ScalarValue::canonical_integer(text), None, "{text:?}");
1596 }
1597 }
1598
1599 #[test]
1600 fn test_from_scalar_block_style_is_string() {
1601 use crate::yaml::Document;
1604 use rowan::ast::AstNode;
1605 use std::str::FromStr;
1606
1607 let doc = Document::from_str("k: |\n 1\n").unwrap();
1608 let node = doc.as_mapping().unwrap().get("k").unwrap();
1609 let scalar = crate::yaml::Scalar::cast(node.syntax().clone()).unwrap();
1610 let sv = ScalarValue::from_scalar(&scalar);
1611 assert_eq!(sv.style, ScalarStyle::Literal);
1612 assert_eq!(sv.scalar_type(), ScalarType::String);
1613
1614 let doc = Document::from_str("k: >\n x\n").unwrap();
1615 let node = doc.as_mapping().unwrap().get("k").unwrap();
1616 let scalar = crate::yaml::Scalar::cast(node.syntax().clone()).unwrap();
1617 let sv = ScalarValue::from_scalar(&scalar);
1618 assert_eq!(sv.style, ScalarStyle::Folded);
1619 assert_eq!(sv.scalar_type(), ScalarType::String);
1620 }
1621
1622 #[test]
1623 fn test_needs_quoting_special_chars() {
1624 let cases = [
1629 ("&anchor", "'&anchor'"),
1630 ("*alias", "'*alias'"),
1631 ("!tag", "'!tag'"),
1632 ("a|b", "'a|b'"),
1633 ("it's", "\"it's\""),
1634 ("a\"b", "'a\"b'"),
1635 ("50%", "'50%'"),
1636 ("on", "'on'"),
1637 ("off", "'off'"),
1638 ("On", "'On'"),
1639 ("OFF", "'OFF'"),
1640 ("yes", "'yes'"),
1641 ("no", "'no'"),
1642 ("true", "'true'"),
1643 ("false", "'false'"),
1644 ("null", "'null'"),
1645 ];
1646 for (input, expected) in cases {
1647 assert_eq!(ScalarValue::string(input).to_yaml_string(), expected);
1648 }
1649 assert_eq!(ScalarValue::string("plain").to_yaml_string(), "plain");
1651 }
1652
1653 #[test]
1654 fn test_plain_string_with_control_character_is_double_quoted() {
1655 assert_eq!(ScalarValue::plain("\u{7}").to_yaml_string(), "\"\\a\"");
1656 assert_eq!(ScalarValue::plain("a\rb").to_yaml_string(), "\"a\\rb\"");
1657 assert_eq!(
1658 ScalarValue::plain("a\rb").with_tag("!x").to_yaml_string(),
1659 "!x \"a\\rb\""
1660 );
1661 }
1662
1663 #[test]
1664 fn test_literal_and_folded_nonempty() {
1665 let s = ScalarValue::with_style("line one\nline two", ScalarStyle::Literal);
1669 assert_eq!(s.to_yaml_string(), "|-\n line one\n line two");
1670
1671 let s = ScalarValue::with_style("line one\nline two", ScalarStyle::Folded);
1672 assert_eq!(s.to_yaml_string(), ">-\n line one\n\n line two");
1673 }
1674
1675 #[test]
1676 fn test_display_matches_yaml_string() {
1677 let s = ScalarValue::string("true");
1678 assert_eq!(format!("{}", s), s.to_yaml_string());
1679 assert_eq!(format!("{}", s), "'true'");
1680 }
1681
1682 #[test]
1683 fn test_as_yaml_token_kind_per_type() {
1684 use crate::lex::SyntaxKind;
1685 use rowan::ast::AstNode;
1686
1687 let cases = [
1688 (ScalarValue::parse("42"), SyntaxKind::INT),
1689 (ScalarValue::parse("3.14"), SyntaxKind::FLOAT),
1690 (ScalarValue::parse("true"), SyntaxKind::BOOL),
1691 (ScalarValue::parse("null"), SyntaxKind::NULL),
1692 (ScalarValue::string("hello"), SyntaxKind::STRING),
1693 ];
1694 for (value, expected) in cases {
1695 let mut builder = rowan::GreenNodeBuilder::new();
1696 crate::AsYaml::build_content(&value, &mut builder, 0, false);
1697 let green = builder.finish();
1698 let node = rowan::SyntaxNode::<crate::yaml::Lang>::new_root(green);
1699 let scalar = crate::yaml::Scalar::cast(node).unwrap();
1700 let kind = scalar
1701 .syntax()
1702 .children_with_tokens()
1703 .filter_map(|c| c.into_token())
1704 .next()
1705 .unwrap()
1706 .kind();
1707 assert_eq!(kind, expected, "value {:?}", value.value());
1708 }
1709 }
1710
1711 #[test]
1712 #[cfg(feature = "regex")]
1713 fn test_as_regex_type_gated() {
1714 let re = ScalarValue::regex(r"\d+");
1716 assert!(re.as_regex().is_some());
1717
1718 let not_re = ScalarValue::string(r"\d+");
1719 assert!(not_re.as_regex().is_none());
1720
1721 assert!(not_re.try_as_regex().is_ok());
1723 assert!(ScalarValue::string("(").try_as_regex().is_err());
1724 }
1725
1726 #[test]
1727 fn test_from_f64_renders_yaml_float() {
1728 let cases: [(f64, &str); 6] = [
1729 (f64::NAN, ".nan"),
1730 (f64::INFINITY, ".inf"),
1731 (f64::NEG_INFINITY, "-.inf"),
1732 (1.0, "1.0"),
1733 (-0.0, "-0.0"),
1734 (2.5, "2.5"),
1735 ];
1736 for (value, expected) in cases {
1737 let sv = ScalarValue::from(value);
1738 assert_eq!(sv.to_yaml_string(), expected);
1739 assert_eq!(ScalarValue::classify_plain(expected), CoreScalarType::Float);
1740 }
1741 assert_eq!(ScalarValue::from(1.0f32).to_yaml_string(), "1.0");
1742 assert_eq!(ScalarValue::from(f32::NAN).to_yaml_string(), ".nan");
1743 }
1744
1745 #[test]
1746 fn test_to_f64_special_values() {
1747 let float = |s: &str| ScalarValue {
1748 value: s.to_string(),
1749 style: ScalarStyle::Plain,
1750 scalar_type: ScalarType::Float,
1751 tag: None,
1752 };
1753 assert_eq!(float(".inf").to_f64(), Some(f64::INFINITY));
1754 assert_eq!(float("+.INF").to_f64(), Some(f64::INFINITY));
1755 assert_eq!(float("-.Inf").to_f64(), Some(f64::NEG_INFINITY));
1756 assert!(float(".NaN").to_f64().unwrap().is_nan());
1757 assert_eq!(float("inf").to_f64(), None);
1758 assert_eq!(float("1.5").to_f64(), Some(1.5));
1759 }
1760
1761 #[test]
1762 fn test_parse_does_not_infer_binary() {
1763 for text in ["test", "abcd", "noon", "SGVsbG8="] {
1764 let scalar = ScalarValue::parse(text);
1765 assert_eq!(scalar.scalar_type(), ScalarType::String, "{text}");
1766 assert_eq!(scalar.to_yaml_string(), text);
1767 }
1768 }
1769
1770 #[test]
1771 fn test_sign_after_radix_prefix_is_not_integer() {
1772 for text in ["0x-5", "0x+5", "0o-7", "0o+7", "0b-1", "+-5", "-+5"] {
1773 assert_eq!(ScalarValue::parse_integer(text), None, "{text}");
1774 assert_eq!(
1775 ScalarValue::classify_plain(text),
1776 CoreScalarType::String,
1777 "{text}"
1778 );
1779 assert_eq!(
1780 ScalarValue::auto_detect_type(text),
1781 ScalarType::String,
1782 "{text}"
1783 );
1784 let doc: crate::yaml::Document = format!("k: {text}\n").parse().unwrap();
1785 let node = doc.as_mapping().unwrap().get("k").unwrap();
1786 assert_eq!(node.as_scalar().unwrap().as_i64(), None, "{text}");
1787 }
1788 }
1789
1790 #[test]
1791 fn test_integers_outside_i64() {
1792 assert_eq!(
1793 ScalarValue::parse_integer("-9223372036854775808"),
1794 Some(i64::MIN)
1795 );
1796 assert_eq!(ScalarValue::parse_integer("9223372036854775808"), None);
1797 for text in [
1798 "9223372036854775808",
1799 "-9223372036854775808",
1800 "0xFFFFFFFFFFFFFFFF",
1801 "123456789012345678901234567890",
1802 ] {
1803 assert_eq!(
1804 ScalarValue::classify_plain(text),
1805 CoreScalarType::Integer,
1806 "{text}"
1807 );
1808 assert_eq!(
1809 ScalarValue::auto_detect_type(text),
1810 ScalarType::Integer,
1811 "{text}"
1812 );
1813 }
1814 assert_eq!(
1815 ScalarValue::parse("-9223372036854775808").to_i64(),
1816 Some(i64::MIN)
1817 );
1818 assert_eq!(ScalarValue::parse("9223372036854775808").to_i64(), None);
1819 }
1820
1821 #[test]
1822 fn test_coerce_to_special_types() {
1823 let coerce = |value: &str, target: ScalarType| {
1824 ScalarValue::string(value)
1825 .coerce_to_type(target)
1826 .map(|s| (s.scalar_type(), s.value().to_string()))
1827 };
1828 #[cfg(feature = "base64")]
1829 {
1830 assert_eq!(
1831 coerce("aGk=", ScalarType::Binary),
1832 Some((ScalarType::Binary, "aGk=".to_string()))
1833 );
1834 assert_eq!(coerce("!!", ScalarType::Binary), None);
1835 assert_eq!(ScalarValue::string("aGk=").as_binary(), None);
1836 assert_eq!(
1837 ScalarValue::binary(b"hi").as_binary(),
1838 Some(Ok(b"hi".to_vec()))
1839 );
1840 }
1841 assert_eq!(
1842 coerce("2001-12-14", ScalarType::Timestamp),
1843 Some((ScalarType::Timestamp, "2001-12-14".to_string()))
1844 );
1845 assert_eq!(coerce("x", ScalarType::Timestamp), None);
1846 assert_eq!(
1847 coerce("a+", ScalarType::Regex),
1848 Some((ScalarType::Regex, "a+".to_string()))
1849 );
1850 assert_eq!(
1851 coerce("~", ScalarType::Null),
1852 Some((ScalarType::Null, "null".to_string()))
1853 );
1854 assert_eq!(coerce("x", ScalarType::Null), None);
1855 }
1856
1857 #[test]
1858 #[should_panic(expected = "does not fit in one digit")]
1859 fn test_block_indent_indicator_from_first_line_only() {
1860 ScalarValue::literal("\n x\ny").to_literal_with_indent(0, 10);
1861 }
1862}