1use std::fmt;
4
5#[cfg(feature = "base64")]
6use base64::{engine::general_purpose, Engine as _};
7
8#[cfg(feature = "base64")]
10fn base64_encode(input: &[u8]) -> String {
11 general_purpose::STANDARD.encode(input)
12}
13
14#[cfg(feature = "base64")]
16fn base64_decode(input: &str) -> Result<Vec<u8>, String> {
17 general_purpose::STANDARD
18 .decode(input.trim())
19 .map_err(|e| format!("Base64 decode error: {e}"))
20}
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub enum ScalarStyle {
25 Plain,
27 SingleQuoted,
29 DoubleQuoted,
31 Literal,
33 Folded,
35}
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
45pub enum CoreScalarType {
46 Null,
48 Boolean,
50 Integer,
52 Float,
54 String,
56}
57
58impl From<CoreScalarType> for ScalarType {
59 fn from(t: CoreScalarType) -> Self {
60 match t {
61 CoreScalarType::Null => ScalarType::Null,
62 CoreScalarType::Boolean => ScalarType::Boolean,
63 CoreScalarType::Integer => ScalarType::Integer,
64 CoreScalarType::Float => ScalarType::Float,
65 CoreScalarType::String => ScalarType::String,
66 }
67 }
68}
69
70#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
72pub enum ScalarType {
73 String,
75 Integer,
77 Float,
79 Boolean,
81 Null,
83 #[cfg(feature = "base64")]
85 Binary,
86 Timestamp,
88 Regex,
90}
91
92#[derive(Debug, Clone, PartialEq, Eq)]
94pub struct ScalarValue {
95 value: String,
97 style: ScalarStyle,
99 scalar_type: ScalarType,
101}
102
103fn push_unicode_escape(
109 out: &mut String,
110 chars: &mut std::iter::Peekable<std::str::Chars<'_>>,
111 prefix: char,
112 width: usize,
113) {
114 let hex_digits: String = chars.by_ref().take(width).collect();
115 let decoded = if hex_digits.len() == width {
116 u32::from_str_radix(&hex_digits, 16)
117 .ok()
118 .and_then(char::from_u32)
119 } else {
120 None
121 };
122 match decoded {
123 Some(c) => out.push(c),
124 None => {
125 out.push('\\');
126 out.push(prefix);
127 out.push_str(&hex_digits);
128 }
129 }
130}
131
132impl ScalarValue {
133 pub fn string(value: impl Into<String>) -> Self {
162 let value = value.into();
163 let style = Self::detect_style(&value);
164 let scalar_type = ScalarType::String;
166 Self {
167 value,
168 style,
169 scalar_type,
170 }
171 }
172
173 pub fn parse_escape_sequences(text: &str) -> String {
175 let mut result = String::with_capacity(text.len()); let mut chars = text.chars().peekable();
177
178 while let Some(ch) = chars.next() {
179 if ch == '\\' {
180 if let Some(&escaped) = chars.peek() {
181 chars.next(); match escaped {
183 'n' => result.push('\n'),
185 't' => result.push('\t'),
186 'r' => result.push('\r'),
187 'b' => result.push('\x08'),
188 'f' => result.push('\x0C'),
189 'a' => result.push('\x07'), 'e' => result.push('\x1B'), 'v' => result.push('\x0B'), '0' => result.push('\0'), '\\' => result.push('\\'),
194 '"' => result.push('"'),
195 '\'' => result.push('\''),
196 '/' => result.push('/'),
197 ' ' => {
199 if let Some(&'\n') = chars.peek() {
201 chars.next(); continue;
204 }
205 result.push(' ');
206 }
207 '\n' => {
208 continue;
210 }
211 'x' => {
213 let mut hex_chars = [0u8; 2];
215 let mut count = 0;
216 for (i, ch) in chars.by_ref().take(2).enumerate() {
217 if let Some(digit) = ch.to_digit(16) {
218 hex_chars[i] = digit as u8;
219 count += 1;
220 } else {
221 result.push('\\');
223 result.push('x');
224 for &hex_char in hex_chars.iter().take(count) {
225 result.push(char::from_digit(hex_char as u32, 16).unwrap());
226 }
227 result.push(ch);
228 break;
229 }
230 }
231 if count == 2 {
232 let code = hex_chars[0] * 16 + hex_chars[1];
233 result.push(code as char);
234 } else if count > 0 {
235 result.push('\\');
237 result.push('x');
238 for &hex_char in hex_chars.iter().take(count) {
239 result.push(char::from_digit(hex_char as u32, 16).unwrap());
240 }
241 }
242 }
243 'u' => push_unicode_escape(&mut result, &mut chars, 'u', 4),
244 'U' => push_unicode_escape(&mut result, &mut chars, 'U', 8),
245 _ => {
247 result.push('\\');
248 result.push(escaped);
249 }
250 }
251 } else {
252 result.push('\\');
254 }
255 } else {
256 result.push(ch);
257 }
258 }
259
260 result
261 }
262
263 pub fn with_style(value: impl Into<String>, style: ScalarStyle) -> Self {
265 Self {
266 value: value.into(),
267 style,
268 scalar_type: ScalarType::String,
269 }
270 }
271
272 pub fn plain(value: impl Into<String>) -> Self {
274 Self::with_style(value, ScalarStyle::Plain)
275 }
276
277 pub fn single_quoted(value: impl Into<String>) -> Self {
279 Self::with_style(value, ScalarStyle::SingleQuoted)
280 }
281
282 pub fn double_quoted(value: impl Into<String>) -> Self {
284 Self::with_style(value, ScalarStyle::DoubleQuoted)
285 }
286
287 pub fn literal(value: impl Into<String>) -> Self {
289 Self::with_style(value, ScalarStyle::Literal)
290 }
291
292 pub fn folded(value: impl Into<String>) -> Self {
294 Self::with_style(value, ScalarStyle::Folded)
295 }
296
297 pub fn null() -> Self {
299 Self {
300 value: "null".to_string(),
301 style: ScalarStyle::Plain,
302 scalar_type: ScalarType::Null,
303 }
304 }
305
306 #[cfg(feature = "base64")]
308 pub fn binary(data: &[u8]) -> Self {
309 let encoded = base64_encode(data);
310 Self {
311 value: encoded,
312 style: ScalarStyle::Plain,
313 scalar_type: ScalarType::Binary,
314 }
315 }
316
317 pub fn timestamp(value: impl Into<String>) -> Self {
319 Self {
320 value: value.into(),
321 style: ScalarStyle::Plain,
322 scalar_type: ScalarType::Timestamp,
323 }
324 }
325
326 pub fn regex(pattern: impl Into<String>) -> Self {
328 Self {
329 value: pattern.into(),
330 style: ScalarStyle::Plain,
331 scalar_type: ScalarType::Regex,
332 }
333 }
334
335 pub fn value(&self) -> &str {
337 &self.value
338 }
339
340 pub fn style(&self) -> ScalarStyle {
342 self.style
343 }
344
345 pub fn scalar_type(&self) -> ScalarType {
347 self.scalar_type
348 }
349
350 pub fn to_i64(&self) -> Option<i64> {
358 if self.scalar_type == ScalarType::Integer {
359 Self::parse_integer(&self.value)
360 } else {
361 None
362 }
363 }
364
365 pub fn to_i64_yaml_1_2(&self) -> Option<i64> {
371 if self.scalar_type != ScalarType::Integer {
372 return None;
373 }
374 if Self::is_legacy_octal(&self.value) {
375 let body = self.value.strip_prefix(['+', '-']).unwrap_or(&self.value);
376 let magnitude = body.parse::<i64>().ok()?;
377 return Some(if self.value.starts_with('-') {
378 -magnitude
379 } else {
380 magnitude
381 });
382 }
383 Self::parse_integer(&self.value)
384 }
385
386 pub fn to_f64(&self) -> Option<f64> {
390 if self.scalar_type == ScalarType::Float {
391 self.value.trim().parse::<f64>().ok()
392 } else {
393 None
394 }
395 }
396
397 pub fn to_bool(&self) -> Option<bool> {
402 if self.scalar_type == ScalarType::Boolean {
403 match self.value.to_lowercase().as_str() {
404 "true" | "yes" | "on" => Some(true),
405 "false" | "no" | "off" => Some(false),
406 _ => None,
407 }
408 } else {
409 None
410 }
411 }
412
413 #[cfg(feature = "base64")]
415 pub fn as_binary(&self) -> Option<Result<Vec<u8>, String>> {
416 match self.scalar_type {
417 ScalarType::Binary => Some(base64_decode(&self.value)),
418 _ => None,
419 }
420 }
421
422 #[cfg(feature = "base64")]
424 pub fn is_binary(&self) -> bool {
425 self.scalar_type == ScalarType::Binary
426 }
427
428 pub fn is_timestamp(&self) -> bool {
430 self.scalar_type == ScalarType::Timestamp
431 }
432
433 pub fn is_regex(&self) -> bool {
435 self.scalar_type == ScalarType::Regex
436 }
437
438 #[cfg(feature = "regex")]
455 pub fn as_regex(&self) -> Option<regex::Regex> {
456 if self.scalar_type == ScalarType::Regex {
457 regex::Regex::new(&self.value).ok()
458 } else {
459 None
460 }
461 }
462
463 #[cfg(feature = "regex")]
481 pub fn try_as_regex(&self) -> Result<regex::Regex, regex::Error> {
482 regex::Regex::new(&self.value)
483 }
484
485 pub fn coerce_to_type(&self, target_type: ScalarType) -> Option<ScalarValue> {
487 if self.scalar_type == target_type {
488 return Some(self.clone());
489 }
490
491 match target_type {
492 ScalarType::String => Some(ScalarValue {
493 value: self.value.clone(),
494 style: ScalarStyle::Plain,
495 scalar_type: ScalarType::String,
496 }),
497 ScalarType::Integer => Self::parse_integer(&self.value).map(ScalarValue::from),
498 ScalarType::Float => self.value.parse::<f64>().ok().map(ScalarValue::from),
499 ScalarType::Boolean => match self.value.to_lowercase().as_str() {
500 "true" | "yes" | "on" | "1" => Some(ScalarValue::from(true)),
501 "false" | "no" | "off" | "0" => Some(ScalarValue::from(false)),
502 _ => None,
503 },
504 ScalarType::Null => match self.value.to_lowercase().as_str() {
505 "null" | "~" | "" => Some(ScalarValue::null()),
506 _ => None,
507 },
508 #[cfg(feature = "base64")]
509 ScalarType::Binary => {
510 if base64_decode(&self.value).is_ok() {
512 Some(ScalarValue {
513 value: self.value.clone(),
514 style: ScalarStyle::Plain,
515 scalar_type: ScalarType::Binary,
516 })
517 } else {
518 None
519 }
520 }
521 ScalarType::Timestamp => {
522 if self.is_valid_timestamp(&self.value) {
524 Some(ScalarValue::timestamp(&self.value))
525 } else {
526 None
527 }
528 }
529 ScalarType::Regex => {
530 Some(ScalarValue::regex(&self.value))
532 }
533 }
534 }
535
536 pub(crate) fn parse_integer(value: &str) -> Option<i64> {
539 let value = value.trim();
540
541 let (is_negative, value) = if let Some(stripped) = value.strip_prefix('-') {
543 (true, stripped)
544 } else if let Some(stripped) = value.strip_prefix('+') {
545 (false, stripped)
546 } else {
547 (false, value)
548 };
549
550 let parsed = if let Some(hex_part) = value
551 .strip_prefix("0x")
552 .or_else(|| value.strip_prefix("0X"))
553 {
554 i64::from_str_radix(hex_part, 16).ok()
556 } else if let Some(bin_part) = value
557 .strip_prefix("0b")
558 .or_else(|| value.strip_prefix("0B"))
559 {
560 i64::from_str_radix(bin_part, 2).ok()
562 } else if let Some(oct_part) = value
563 .strip_prefix("0o")
564 .or_else(|| value.strip_prefix("0O"))
565 {
566 i64::from_str_radix(oct_part, 8).ok()
568 } else if value.starts_with('0')
569 && value.len() > 1
570 && value.chars().all(|c| c.is_ascii_digit())
571 {
572 i64::from_str_radix(value, 8).ok()
574 } else {
575 value.parse::<i64>().ok()
577 };
578
579 parsed.map(|n| if is_negative { -n } else { n })
580 }
581
582 pub(crate) fn is_legacy_octal(value: &str) -> bool {
588 let body = value.strip_prefix(['+', '-']).unwrap_or(value);
589 body.len() > 1 && body.starts_with('0') && body.bytes().all(|b| b.is_ascii_digit())
590 }
591
592 pub fn classify_plain(value: &str) -> CoreScalarType {
606 match value {
607 "true" | "false" | "True" | "False" | "TRUE" | "FALSE" => {
608 return CoreScalarType::Boolean
609 }
610 "null" | "Null" | "NULL" | "~" => return CoreScalarType::Null,
611 _ => {}
612 }
613
614 if Self::parse_integer(value).is_some() || Self::is_legacy_octal(value) {
622 return CoreScalarType::Integer;
623 }
624
625 match value {
631 ".inf" | ".Inf" | ".INF" | "+.inf" | "+.Inf" | "+.INF" | "-.inf" | "-.Inf"
632 | "-.INF" | ".nan" | ".NaN" | ".NAN" => return CoreScalarType::Float,
633 _ => {}
634 }
635 let body = value.strip_prefix(['+', '-']).unwrap_or(value);
636 if body.eq_ignore_ascii_case("inf")
637 || body.eq_ignore_ascii_case("infinity")
638 || body.eq_ignore_ascii_case("nan")
639 {
640 return CoreScalarType::String;
641 }
642
643 if value.parse::<f64>().is_ok() {
644 return CoreScalarType::Float;
645 }
646
647 CoreScalarType::String
648 }
649
650 pub fn auto_detect_type(value: &str) -> ScalarType {
652 match value.to_lowercase().as_str() {
654 "null" | "~" | "" => return ScalarType::Null,
655 _ => {}
656 }
657
658 match value.to_lowercase().as_str() {
660 "true" | "false" | "yes" | "no" | "on" | "off" => return ScalarType::Boolean,
661 _ => {}
662 }
663
664 if Self::parse_integer(value).is_some() {
666 return ScalarType::Integer;
667 }
668 if value.parse::<f64>().is_ok() {
669 return ScalarType::Float;
670 }
671
672 if Self::is_valid_timestamp_static(value) {
674 return ScalarType::Timestamp;
675 }
676
677 #[cfg(feature = "base64")]
679 if Self::looks_like_base64(value) && base64_decode(value).is_ok() {
680 return ScalarType::Binary;
681 }
682
683 ScalarType::String
685 }
686
687 pub fn parse(value: impl Into<String>) -> Self {
717 let value = value.into();
718 let scalar_type = Self::auto_detect_type(&value);
719 let style = match scalar_type {
722 ScalarType::String => Self::detect_style(&value),
723 _ => ScalarStyle::Plain,
725 };
726
727 Self {
728 value,
729 style,
730 scalar_type,
731 }
732 }
733
734 pub fn from_scalar(scalar: &crate::yaml::Scalar) -> Self {
740 let value = scalar.as_string();
741 let raw_text = scalar.value();
742
743 let style = if raw_text.starts_with('"') && raw_text.ends_with('"') {
744 ScalarStyle::DoubleQuoted
745 } else if raw_text.starts_with('\'') && raw_text.ends_with('\'') {
746 ScalarStyle::SingleQuoted
747 } else if raw_text.starts_with('|') {
748 ScalarStyle::Literal
749 } else if raw_text.starts_with('>') {
750 ScalarStyle::Folded
751 } else {
752 ScalarStyle::Plain
753 };
754
755 let scalar_type = if style != ScalarStyle::Plain {
764 ScalarType::String
765 } else {
766 Self::classify_plain(raw_text.trim_end()).into()
767 };
768
769 Self {
770 value,
771 style,
772 scalar_type,
773 }
774 }
775
776 #[cfg(feature = "base64")]
778 fn looks_like_base64(value: &str) -> bool {
779 if value.is_empty() {
780 return false;
781 }
782
783 if value.len() < 4 || value.len() % 4 != 0 {
786 return false;
787 }
788
789 let padding_count = value.chars().filter(|&c| c == '=').count();
790 if padding_count > 2 {
791 return false;
792 }
793
794 if !value
796 .chars()
797 .all(|c| matches!(c, 'A'..='Z' | 'a'..='z' | '0'..='9' | '+' | '/' | '='))
798 {
799 return false;
800 }
801
802 if padding_count > 0 {
804 let padding_start = value.len() - padding_count;
805 if !value[padding_start..].chars().all(|c| c == '=') {
806 return false;
807 }
808 if value[..padding_start].contains('=') {
810 return false;
811 }
812 }
813
814 base64_decode(value).is_ok()
817 }
818
819 fn is_valid_timestamp(&self, value: &str) -> bool {
821 Self::is_valid_timestamp_static(value)
822 }
823
824 fn is_valid_timestamp_static(value: &str) -> bool {
826 if Self::matches_iso8601_pattern(value) {
829 return true;
830 }
831
832 if let Ok(timestamp) = value.parse::<u64>() {
834 return timestamp > 0 && timestamp < 4_102_444_800; }
837
838 false
839 }
840
841 fn matches_iso8601_pattern(value: &str) -> bool {
843 let chars: Vec<char> = value.chars().collect();
844
845 if chars.len() < 10 {
847 return false;
848 }
849
850 if !(chars[0..4].iter().all(|c| c.is_ascii_digit())
852 && chars[4] == '-'
853 && chars[5..7].iter().all(|c| c.is_ascii_digit())
854 && chars[7] == '-'
855 && chars[8..10].iter().all(|c| c.is_ascii_digit()))
856 {
857 return false;
858 }
859
860 let month_str: String = chars[5..7].iter().collect();
862 let day_str: String = chars[8..10].iter().collect();
863
864 if let (Ok(month), Ok(day)) = (month_str.parse::<u8>(), day_str.parse::<u8>()) {
865 if !(1..=12).contains(&month) || !(1..=31).contains(&day) {
866 return false;
867 }
868 } else {
869 return false;
870 }
871
872 if chars.len() == 10 {
874 return true;
875 }
876
877 if chars.len() >= 19 {
879 let sep = chars[10];
880 if (sep == 'T' || sep == 't' || sep == ' ')
881 && chars[11..13].iter().all(|c| c.is_ascii_digit())
882 && chars[13] == ':'
883 && chars[14..16].iter().all(|c| c.is_ascii_digit())
884 && chars[16] == ':'
885 && chars[17..19].iter().all(|c| c.is_ascii_digit())
886 {
887 let hour_str: String = chars[11..13].iter().collect();
889 let minute_str: String = chars[14..16].iter().collect();
890 let second_str: String = chars[17..19].iter().collect();
891
892 if let (Ok(hour), Ok(minute), Ok(second)) = (
893 hour_str.parse::<u8>(),
894 minute_str.parse::<u8>(),
895 second_str.parse::<u8>(),
896 ) {
897 if hour > 23 || minute > 59 || second > 59 {
898 return false;
899 }
900 } else {
901 return false;
902 }
903
904 return true;
905 }
906 }
907
908 false
909 }
910
911 fn detect_style(value: &str) -> ScalarStyle {
913 if value.contains('\n') {
919 return ScalarStyle::Literal;
920 }
921 if Self::needs_quoting(value) {
923 if !value.contains('\'') {
925 ScalarStyle::SingleQuoted
926 } else {
927 ScalarStyle::DoubleQuoted
928 }
929 } else {
930 ScalarStyle::Plain
931 }
932 }
933
934 fn needs_quoting(value: &str) -> bool {
936 if value.is_empty() {
938 return true;
939 }
940
941 if value.eq_ignore_ascii_case("true")
944 || value.eq_ignore_ascii_case("false")
945 || value.eq_ignore_ascii_case("yes")
946 || value.eq_ignore_ascii_case("no")
947 || value.eq_ignore_ascii_case("on")
948 || value.eq_ignore_ascii_case("off")
949 || value.eq_ignore_ascii_case("null")
950 || value == "~"
951 {
952 return true;
953 }
954
955 if value.parse::<f64>().is_ok() || Self::parse_integer(value).is_some() {
957 return true;
958 }
959
960 if value.starts_with(|ch: char| {
965 matches!(
966 ch,
967 '-' | '?'
968 | ':'
969 | '['
970 | ']'
971 | '{'
972 | '}'
973 | ','
974 | '>'
975 | '<'
976 | '!'
977 | '&'
978 | '*'
979 | '#'
980 | '%'
981 | '@'
982 | '`'
983 )
984 }) {
985 return true;
986 }
987
988 if value.starts_with("...") || value.starts_with("---") {
992 return true;
993 }
994
995 let mut prev: Option<char> = None;
1001 let mut chars = value.chars().peekable();
1002 while let Some(ch) = chars.next() {
1003 match ch {
1004 '&' | '*' | '!' | '|' | '\'' | '"' | '%' => return true,
1005 ':' if chars.peek().map_or(true, |next| next.is_whitespace()) => {
1006 return true;
1007 }
1008 '#' if prev.map_or(true, char::is_whitespace) => {
1009 return true;
1010 }
1011 _ => {}
1012 }
1013 prev = Some(ch);
1014 }
1015
1016 if value != value.trim() {
1018 return true;
1019 }
1020
1021 if value.contains('\r') {
1025 return true;
1026 }
1027
1028 false
1029 }
1030
1031 pub fn to_yaml_string(&self) -> String {
1038 self.to_yaml_string_with_indent(2)
1039 }
1040
1041 pub fn to_yaml_string_with_indent(&self, block_indent: usize) -> String {
1044 let tag_prefix = match self.scalar_type {
1046 #[cfg(feature = "base64")]
1047 ScalarType::Binary => "!!binary ",
1048 ScalarType::Timestamp => "!!timestamp ",
1049 ScalarType::Regex => "!!regex ",
1050 _ => "",
1051 };
1052
1053 let content = match self.style {
1054 ScalarStyle::Plain => {
1055 match self.scalar_type {
1057 ScalarType::String => {
1058 if Self::needs_quoting(&self.value) {
1060 self.to_single_quoted()
1061 } else {
1062 self.value.clone()
1063 }
1064 }
1065 ScalarType::Integer
1067 | ScalarType::Float
1068 | ScalarType::Boolean
1069 | ScalarType::Null
1070 | ScalarType::Timestamp
1071 | ScalarType::Regex => self.value.clone(),
1072 #[cfg(feature = "base64")]
1073 ScalarType::Binary => self.value.clone(),
1074 }
1075 }
1076 ScalarStyle::SingleQuoted => self.to_single_quoted(),
1077 ScalarStyle::DoubleQuoted => self.to_double_quoted(),
1078 ScalarStyle::Literal => self.to_literal_with_indent(block_indent),
1079 ScalarStyle::Folded => self.to_folded_with_indent(block_indent),
1080 };
1081
1082 format!("{tag_prefix}{content}")
1083 }
1084
1085 fn to_single_quoted(&self) -> String {
1087 let escaped = self.value.replace('\'', "''");
1089 format!("'{escaped}'")
1090 }
1091
1092 fn to_double_quoted(&self) -> String {
1094 let mut result = String::from("\"");
1095 for ch in self.value.chars() {
1096 match ch {
1097 '"' => result.push_str("\\\""),
1098 '\\' => result.push_str("\\\\"),
1099 '\n' => result.push_str("\\n"),
1100 '\r' => result.push_str("\\r"),
1101 '\t' => result.push_str("\\t"),
1102 '\x08' => result.push_str("\\b"),
1103 '\x0C' => result.push_str("\\f"),
1104 '\x07' => result.push_str("\\a"), '\x1B' => result.push_str("\\e"), '\x0B' => result.push_str("\\v"), '\0' => result.push_str("\\0"), c if c.is_control() || (c as u32) > 0x7F => {
1109 let code_point = c as u32;
1111 if code_point <= 0xFF {
1112 result.push_str(&format!("\\x{code_point:02X}"));
1113 } else if code_point <= 0xFFFF {
1114 result.push_str(&format!("\\u{code_point:04X}"));
1115 } else {
1116 result.push_str(&format!("\\U{code_point:08X}"));
1117 }
1118 }
1119 c => result.push(c),
1120 }
1121 }
1122 result.push('"');
1123 result
1124 }
1125
1126 pub fn to_literal_with_indent(&self, indent: usize) -> String {
1128 self.to_block_with_indent('|', indent)
1129 }
1130
1131 pub fn to_folded_with_indent(&self, indent: usize) -> String {
1133 self.to_block_with_indent('>', indent)
1134 }
1135
1136 fn to_block_with_indent(&self, marker: char, indent: usize) -> String {
1138 let trailing = self.value.len() - self.value.trim_end_matches('\n').len();
1143 let chomp = match trailing {
1144 0 => "-",
1145 1 => "",
1146 _ => "+",
1147 };
1148 if let Some(content_indent) = self.detect_content_indentation() {
1153 if content_indent > 0 {
1154 let body = " ".repeat(indent);
1158 let shifted = self
1159 .value
1160 .lines()
1161 .map(|line| {
1162 if line.is_empty() {
1163 String::new()
1164 } else {
1165 format!("{body}{line}")
1166 }
1167 })
1168 .collect::<Vec<_>>()
1169 .join("\n");
1170 return format!("{marker}{indent}{chomp}\n{shifted}");
1171 }
1172 return format!("{marker}{chomp}\n{}", self.value);
1173 }
1174 let indent_str = " ".repeat(indent);
1175 let indented = self
1176 .value
1177 .lines()
1178 .map(|line| {
1179 if line.trim().is_empty() {
1180 String::new()
1181 } else {
1182 format!("{indent_str}{line}")
1183 }
1184 })
1185 .collect::<Vec<_>>()
1186 .join("\n");
1187 format!("{marker}{chomp}\n{indented}")
1188 }
1189
1190 fn detect_content_indentation(&self) -> Option<usize> {
1192 let non_empty_lines: Vec<&str> = self
1193 .value
1194 .lines()
1195 .filter(|line| !line.trim().is_empty())
1196 .collect();
1197
1198 if non_empty_lines.is_empty() {
1199 return None;
1200 }
1201
1202 let mut min_indent = None;
1203 let mut all_have_same_indent = true;
1204
1205 for line in non_empty_lines {
1206 let indent = line.len() - line.trim_start().len();
1207 match min_indent {
1208 None => min_indent = Some(indent),
1209 Some(current_min) => {
1210 if indent != current_min {
1211 all_have_same_indent = false;
1212 }
1213 min_indent = Some(current_min.min(indent));
1214 }
1215 }
1216 }
1217
1218 if all_have_same_indent && min_indent.unwrap_or(0) > 0 {
1220 min_indent
1221 } else {
1222 None
1223 }
1224 }
1225}
1226
1227impl fmt::Display for ScalarValue {
1228 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1229 write!(f, "{}", self.to_yaml_string())
1230 }
1231}
1232
1233impl From<String> for ScalarValue {
1234 fn from(value: String) -> Self {
1235 Self::string(value)
1236 }
1237}
1238
1239impl From<&str> for ScalarValue {
1240 fn from(value: &str) -> Self {
1241 Self::string(value)
1242 }
1243}
1244
1245macro_rules! impl_from_number {
1248 ($($t:ty => $ty:expr),* $(,)?) => {
1249 $(
1250 impl From<$t> for ScalarValue {
1251 fn from(value: $t) -> Self {
1252 Self {
1253 value: value.to_string(),
1254 style: ScalarStyle::Plain,
1255 scalar_type: $ty,
1256 }
1257 }
1258 }
1259 )*
1260 };
1261}
1262
1263impl_from_number! {
1264 i32 => ScalarType::Integer,
1265 i64 => ScalarType::Integer,
1266 f32 => ScalarType::Float,
1267 f64 => ScalarType::Float,
1268}
1269
1270impl From<bool> for ScalarValue {
1271 fn from(value: bool) -> Self {
1272 Self {
1273 value: if value { "true" } else { "false" }.to_string(),
1274 style: ScalarStyle::Plain,
1275 scalar_type: ScalarType::Boolean,
1276 }
1277 }
1278}
1279
1280impl From<crate::yaml::Scalar> for ScalarValue {
1281 fn from(scalar: crate::yaml::Scalar) -> Self {
1282 let value = scalar.as_string();
1283 ScalarValue::parse(&value)
1284 }
1285}
1286
1287impl crate::AsYaml for ScalarValue {
1288 fn as_node(&self) -> Option<&crate::yaml::SyntaxNode> {
1289 None
1290 }
1291
1292 fn kind(&self) -> crate::as_yaml::YamlKind {
1293 crate::as_yaml::YamlKind::Scalar
1294 }
1295
1296 fn build_content(
1297 &self,
1298 builder: &mut rowan::GreenNodeBuilder,
1299 indent: usize,
1300 _flow_context: bool,
1301 ) -> bool {
1302 use crate::lex::SyntaxKind;
1303 let text = self.to_yaml_string_with_indent(indent + 2);
1311 let quoted = text.starts_with('\'') || text.starts_with('"');
1312 let token_kind = if quoted {
1313 SyntaxKind::STRING
1314 } else {
1315 match self.scalar_type() {
1316 ScalarType::Integer => SyntaxKind::INT,
1317 ScalarType::Float => SyntaxKind::FLOAT,
1318 ScalarType::Boolean => SyntaxKind::BOOL,
1319 ScalarType::Null => SyntaxKind::NULL,
1320 _ => SyntaxKind::STRING,
1321 }
1322 };
1323 builder.start_node(SyntaxKind::SCALAR.into());
1324 builder.token(token_kind.into(), &text);
1325 builder.finish_node();
1326 false
1327 }
1328
1329 fn is_inline(&self) -> bool {
1330 true
1331 }
1332}
1333
1334#[cfg(test)]
1335mod tests {
1336 use super::*;
1337
1338 #[test]
1339 fn test_plain_scalars() {
1340 let scalar = ScalarValue::string("simple");
1341 assert_eq!(scalar.to_yaml_string(), "simple");
1342
1343 let scalar = ScalarValue::string("hello world");
1344 assert_eq!(scalar.to_yaml_string(), "hello world");
1345 }
1346
1347 #[test]
1348 fn test_values_needing_quotes() {
1349 let scalar = ScalarValue::string("true");
1351 assert_eq!(scalar.to_yaml_string(), "'true'");
1352
1353 let scalar = ScalarValue::string("false");
1354 assert_eq!(scalar.to_yaml_string(), "'false'");
1355
1356 let scalar = ScalarValue::string("yes");
1357 assert_eq!(scalar.to_yaml_string(), "'yes'");
1358
1359 let scalar = ScalarValue::string("no");
1360 assert_eq!(scalar.to_yaml_string(), "'no'");
1361
1362 let scalar = ScalarValue::string("null");
1364 assert_eq!(scalar.to_yaml_string(), "'null'");
1365
1366 let scalar = ScalarValue::string("~");
1367 assert_eq!(scalar.to_yaml_string(), "'~'");
1368
1369 let scalar = ScalarValue::string("123");
1371 assert_eq!(scalar.to_yaml_string(), "'123'");
1372
1373 let scalar = ScalarValue::string("3.14");
1374 assert_eq!(scalar.to_yaml_string(), "'3.14'");
1375
1376 let scalar = ScalarValue::string("value: something");
1378 assert_eq!(scalar.to_yaml_string(), "'value: something'");
1379
1380 let scalar = ScalarValue::string("# comment");
1381 assert_eq!(scalar.to_yaml_string(), "'# comment'");
1382
1383 let scalar = ScalarValue::string(" spaces ");
1385 assert_eq!(scalar.to_yaml_string(), "' spaces '");
1386 }
1387
1388 #[test]
1389 fn test_single_quoted() {
1390 let scalar = ScalarValue::single_quoted("value with 'quotes'");
1391 assert_eq!(scalar.to_yaml_string(), "'value with ''quotes'''");
1392 }
1393
1394 #[test]
1395 fn test_double_quoted() {
1396 let scalar = ScalarValue::double_quoted("value with \"quotes\" and \\backslash");
1397 assert_eq!(
1398 scalar.to_yaml_string(),
1399 "\"value with \\\"quotes\\\" and \\\\backslash\""
1400 );
1401
1402 let scalar = ScalarValue::double_quoted("line1\nline2\ttab");
1403 assert_eq!(scalar.to_yaml_string(), "\"line1\\nline2\\ttab\"");
1404 }
1405
1406 #[test]
1407 fn round_trips_keys_needing_quotes() {
1408 use crate::Document;
1409 use std::str::FromStr;
1410
1411 for key in [
1416 "a. #a", "a #c", "a#b", "a\nb", "a\rb", "x: y", "a:b", "plain", "a b", "#lead", "a:",
1417 "", "true", "123", "- x", "? x",
1418 ] {
1419 let doc = Document::from_str("z: 1\n").unwrap();
1420 doc.as_mapping().unwrap().set(key, "v");
1421 let text = doc.to_string();
1422
1423 let reparsed = Document::from_str(&text)
1424 .unwrap_or_else(|e| panic!("{key:?} produced unparseable {text:?}: {e}"));
1425 let value = reparsed
1426 .as_mapping()
1427 .and_then(|m| m.get(key))
1428 .unwrap_or_else(|| panic!("{key:?} did not survive as {text:?}"));
1429 assert_eq!(value.to_string(), "v", "{key:?} in {text:?}");
1430 }
1431 }
1432
1433 #[test]
1434 fn test_multiline() {
1435 let scalar = ScalarValue::string("line1\nline2\nline3");
1436 assert_eq!(scalar.style(), ScalarStyle::Literal);
1438 }
1439
1440 #[test]
1441 fn test_from_types() {
1442 let scalar = ScalarValue::from(42);
1443 assert_eq!(scalar.to_yaml_string(), "42");
1444
1445 let scalar = ScalarValue::from(1.234);
1446 assert_eq!(scalar.to_yaml_string(), "1.234");
1447
1448 let scalar = ScalarValue::from(true);
1449 assert_eq!(scalar.to_yaml_string(), "true");
1450
1451 let scalar = ScalarValue::from(false);
1452 assert_eq!(scalar.to_yaml_string(), "false");
1453 }
1454
1455 #[test]
1456 fn test_empty_string() {
1457 let scalar = ScalarValue::string("");
1458 assert_eq!(scalar.to_yaml_string(), "''");
1459 }
1460
1461 #[test]
1462 fn test_special_start_chars() {
1463 let scalar = ScalarValue::string("-item");
1464 assert_eq!(scalar.to_yaml_string(), "'-item'");
1465
1466 let scalar = ScalarValue::string("?key");
1467 assert_eq!(scalar.to_yaml_string(), "'?key'");
1468
1469 let scalar = ScalarValue::string("[array]");
1470 assert_eq!(scalar.to_yaml_string(), "'[array]'");
1471 }
1472
1473 #[test]
1474 fn test_null_scalar() {
1475 let scalar = ScalarValue::null();
1476 assert_eq!(scalar.to_yaml_string(), "null");
1477 assert_eq!(scalar.scalar_type, ScalarType::Null);
1478 }
1479
1480 #[test]
1481 fn test_escape_sequences_basic() {
1482 assert_eq!(
1484 ScalarValue::parse_escape_sequences("hello\\nworld"),
1485 "hello\nworld"
1486 );
1487 assert_eq!(
1488 ScalarValue::parse_escape_sequences("tab\\there"),
1489 "tab\there"
1490 );
1491 assert_eq!(
1492 ScalarValue::parse_escape_sequences("quote\\\"test"),
1493 "quote\"test"
1494 );
1495 assert_eq!(
1496 ScalarValue::parse_escape_sequences("back\\\\slash"),
1497 "back\\slash"
1498 );
1499 assert_eq!(
1500 ScalarValue::parse_escape_sequences("return\\rtest"),
1501 "return\rtest"
1502 );
1503 }
1504
1505 #[test]
1506 fn test_escape_sequences_control_chars() {
1507 assert_eq!(ScalarValue::parse_escape_sequences("bell\\a"), "bell\x07");
1509 assert_eq!(
1510 ScalarValue::parse_escape_sequences("backspace\\b"),
1511 "backspace\x08"
1512 );
1513 assert_eq!(
1514 ScalarValue::parse_escape_sequences("formfeed\\f"),
1515 "formfeed\x0C"
1516 );
1517 assert_eq!(
1518 ScalarValue::parse_escape_sequences("escape\\e"),
1519 "escape\x1B"
1520 );
1521 assert_eq!(ScalarValue::parse_escape_sequences("vtab\\v"), "vtab\x0B");
1522 assert_eq!(ScalarValue::parse_escape_sequences("null\\0"), "null\0");
1523 assert_eq!(ScalarValue::parse_escape_sequences("slash\\/"), "slash/");
1524 }
1525
1526 #[test]
1527 fn test_escape_sequences_unicode_x() {
1528 assert_eq!(ScalarValue::parse_escape_sequences("\\x41"), "A"); assert_eq!(ScalarValue::parse_escape_sequences("\\x7A"), "z"); assert_eq!(ScalarValue::parse_escape_sequences("\\x20"), " "); assert_eq!(ScalarValue::parse_escape_sequences("\\xFF"), "\u{FF}"); assert_eq!(ScalarValue::parse_escape_sequences("\\xGH"), "\\xGH"); assert_eq!(ScalarValue::parse_escape_sequences("\\x4"), "\\x4"); }
1538
1539 #[test]
1540 fn test_escape_sequences_unicode_u() {
1541 assert_eq!(ScalarValue::parse_escape_sequences("\\u0041"), "A"); assert_eq!(ScalarValue::parse_escape_sequences("\\u03B1"), "α"); assert_eq!(ScalarValue::parse_escape_sequences("\\u2603"), "☃"); assert_eq!(ScalarValue::parse_escape_sequences("\\u4E2D"), "中"); assert_eq!(ScalarValue::parse_escape_sequences("\\uGHIJ"), "\\uGHIJ"); assert_eq!(ScalarValue::parse_escape_sequences("\\u041"), "\\u041"); }
1551
1552 #[test]
1553 fn test_escape_sequences_unicode_capital_u() {
1554 assert_eq!(ScalarValue::parse_escape_sequences("\\U00000041"), "A"); assert_eq!(ScalarValue::parse_escape_sequences("\\U0001F603"), "😃"); assert_eq!(ScalarValue::parse_escape_sequences("\\U0001F4A9"), "💩"); assert_eq!(
1561 ScalarValue::parse_escape_sequences("\\UGHIJKLMN"),
1562 "\\UGHIJKLMN"
1563 ); assert_eq!(
1565 ScalarValue::parse_escape_sequences("\\U0000004"),
1566 "\\U0000004"
1567 ); assert_eq!(
1569 ScalarValue::parse_escape_sequences("\\UFFFFFFFF"),
1570 "\\UFFFFFFFF"
1571 ); }
1573
1574 #[test]
1575 fn test_escape_sequences_line_folding() {
1576 assert_eq!(
1578 ScalarValue::parse_escape_sequences("line\\ \nfolding"),
1579 "linefolding"
1580 );
1581 assert_eq!(
1582 ScalarValue::parse_escape_sequences("escaped\\nline\\nbreak"),
1583 "escaped\nline\nbreak"
1584 );
1585 assert_eq!(
1586 ScalarValue::parse_escape_sequences("remove\\\nline\\nbreak"),
1587 "removeline\nbreak"
1588 );
1589 }
1590
1591 #[test]
1592 fn test_escape_sequences_mixed() {
1593 let input = "Hello\\nWorld\\u0021\\x20\\U0001F44D";
1595 let expected = "Hello\nWorld! 👍";
1596 assert_eq!(ScalarValue::parse_escape_sequences(input), expected);
1597
1598 let input = "Quote\\\"back\\\\slash\\ttab";
1600 let expected = "Quote\"back\\slash\ttab";
1601 assert_eq!(ScalarValue::parse_escape_sequences(input), expected);
1602 }
1603
1604 #[test]
1605 fn test_escape_sequences_unknown() {
1606 assert_eq!(ScalarValue::parse_escape_sequences("\\q"), "\\q");
1608 assert_eq!(ScalarValue::parse_escape_sequences("\\z"), "\\z");
1609 assert_eq!(ScalarValue::parse_escape_sequences("\\1"), "\\1");
1610 }
1611
1612 #[test]
1613 fn test_indentation_preservation() {
1614 let content_with_indent = " Line 1\n Line 2 more indented\n Line 3";
1616 let scalar = ScalarValue::literal(content_with_indent);
1617
1618 let yaml_output = scalar.to_literal_with_indent(2);
1620 assert_eq!(
1621 yaml_output,
1622 "|-\n Line 1\n Line 2 more indented\n Line 3"
1623 );
1624 }
1625
1626 #[test]
1627 fn test_indentation_detection() {
1628 let consistent_content = " Line 1\n Line 2\n Line 3";
1630 let scalar1 = ScalarValue::literal(consistent_content);
1631 assert_eq!(scalar1.detect_content_indentation(), Some(2));
1632
1633 let no_indent_content = "Line 1\nLine 2\nLine 3";
1635 let scalar2 = ScalarValue::literal(no_indent_content);
1636 assert_eq!(scalar2.detect_content_indentation(), None);
1637
1638 let inconsistent_content = " Line 1\n Line 2\n Line 3";
1640 let scalar3 = ScalarValue::literal(inconsistent_content);
1641 assert_eq!(scalar3.detect_content_indentation(), None);
1642
1643 let empty_content = "";
1645 let scalar4 = ScalarValue::literal(empty_content);
1646 assert_eq!(scalar4.detect_content_indentation(), None);
1647
1648 let whitespace_content = " Line 1\n\n Line 3";
1650 let scalar5 = ScalarValue::literal(whitespace_content);
1651 assert_eq!(scalar5.detect_content_indentation(), Some(2));
1652 }
1653
1654 #[test]
1655 fn test_literal_with_custom_indent() {
1656 let content = "Line 1\nLine 2\nLine 3";
1658 let scalar = ScalarValue::literal(content);
1659
1660 let yaml_4_spaces = scalar.to_literal_with_indent(4);
1661 assert_eq!(yaml_4_spaces, "|-\n Line 1\n Line 2\n Line 3");
1662
1663 let yaml_1_space = scalar.to_literal_with_indent(1);
1664 assert_eq!(yaml_1_space, "|-\n Line 1\n Line 2\n Line 3");
1665 }
1666
1667 #[test]
1668 fn test_folded_with_custom_indent() {
1669 let content = "Line 1\nLine 2\nLine 3";
1671 let scalar = ScalarValue::folded(content);
1672
1673 let yaml_3_spaces = scalar.to_folded_with_indent(3);
1674 assert_eq!(yaml_3_spaces, ">-\n Line 1\n Line 2\n Line 3");
1675 }
1676
1677 #[test]
1678 fn test_mixed_empty_lines_preservation() {
1679 let content_with_empty_lines = "Line 1\n\nLine 3\n\n\nLine 6";
1681 let scalar = ScalarValue::literal(content_with_empty_lines);
1682
1683 let yaml_output = scalar.to_literal_with_indent(2);
1684 assert_eq!(yaml_output, "|-\n Line 1\n\n Line 3\n\n\n Line 6");
1685
1686 let lines: Vec<&str> = yaml_output.lines().collect();
1689 let empty_line_count = lines.iter().filter(|line| line.is_empty()).count();
1690 assert_eq!(empty_line_count, 3);
1691 }
1692
1693 #[test]
1694 fn test_escape_sequences_edge_cases() {
1695 assert_eq!(ScalarValue::parse_escape_sequences(""), "");
1697 assert_eq!(ScalarValue::parse_escape_sequences("\\"), "\\");
1698 assert_eq!(
1699 ScalarValue::parse_escape_sequences("no escapes"),
1700 "no escapes"
1701 );
1702 assert_eq!(ScalarValue::parse_escape_sequences("\\\\\\\\"), "\\\\");
1703 }
1704
1705 #[test]
1706 fn test_double_quoted_with_escapes() {
1707 let original = "Hello\nWorld\t😃";
1709 let scalar = ScalarValue::double_quoted(original);
1710 let yaml_string = scalar.to_yaml_string();
1711
1712 assert_eq!(yaml_string, "\"Hello\\nWorld\\t\\U0001F603\"");
1714
1715 let parsed = ScalarValue::parse_escape_sequences(&yaml_string[1..yaml_string.len() - 1]);
1717 assert_eq!(parsed, original);
1718 }
1719
1720 #[test]
1721 fn test_unicode_output_formatting() {
1722 let scalar = ScalarValue::double_quoted("Hello 世界 🌍");
1724 let yaml_string = scalar.to_yaml_string();
1725
1726 assert_eq!(yaml_string, "\"Hello \\u4E16\\u754C \\U0001F30D\"");
1728
1729 assert_eq!(scalar.value(), "Hello 世界 🌍");
1731 }
1732
1733 #[test]
1734 #[cfg(feature = "base64")]
1735 fn test_binary_data_encoding() {
1736 let data = b"Hello, World!";
1738 let scalar = ScalarValue::binary(data);
1739
1740 assert!(scalar.is_binary());
1741 assert_eq!(scalar.scalar_type(), ScalarType::Binary);
1742
1743 let yaml_output = scalar.to_yaml_string();
1745 assert!(yaml_output.starts_with("!!binary "));
1746
1747 if let Some(decoded_result) = scalar.as_binary() {
1749 let decoded = decoded_result.expect("Should decode successfully");
1750 assert_eq!(decoded, data);
1751 } else {
1752 panic!("Should be able to extract binary data");
1753 }
1754 }
1755
1756 #[test]
1757 #[cfg(feature = "base64")]
1758 fn test_base64_encoding_decoding() {
1759 let test_cases = [
1761 b"".as_slice(),
1762 b"A",
1763 b"AB",
1764 b"ABC",
1765 b"ABCD",
1766 b"Hello, World!",
1767 &[0, 1, 2, 3, 255, 254, 253],
1768 ];
1769
1770 for data in test_cases {
1771 let encoded = base64_encode(data);
1772 let decoded = base64_decode(&encoded).expect("Should decode successfully");
1773 assert_eq!(decoded, data, "Failed for data: {:?}", data);
1774 }
1775 }
1776
1777 #[test]
1778 fn test_timestamp_creation_and_validation() {
1779 let valid_timestamps = [
1781 "2023-12-25",
1782 "2023-12-25T10:30:45",
1783 "2023-12-25 10:30:45",
1784 "2023-12-25T10:30:45Z",
1785 "2001-12-14 21:59:43.10 -5", "2001-12-15T02:59:43.1Z", "2001-12-14t21:59:43.10-05:00", ];
1789
1790 for ts in valid_timestamps {
1791 let scalar = ScalarValue::timestamp(ts);
1792 assert!(scalar.is_timestamp());
1793 assert_eq!(scalar.scalar_type(), ScalarType::Timestamp);
1794 assert_eq!(scalar.value(), ts);
1795
1796 let yaml_output = scalar.to_yaml_string();
1797 assert_eq!(yaml_output, format!("!!timestamp {}", ts));
1798
1799 let auto_scalar = ScalarValue::parse(ts);
1801 assert_eq!(
1802 auto_scalar.scalar_type(),
1803 ScalarType::Timestamp,
1804 "Failed to auto-detect '{}' as timestamp",
1805 ts
1806 );
1807 }
1808
1809 let invalid_timestamps = [
1811 "not-a-date",
1812 "2023-13-01", "2023-12-32", "12:34:56", "2023/12/25", ];
1817
1818 for ts in invalid_timestamps {
1819 let auto_scalar = ScalarValue::parse(ts);
1820 assert_ne!(
1821 auto_scalar.scalar_type(),
1822 ScalarType::Timestamp,
1823 "'{}' should not be detected as timestamp",
1824 ts
1825 );
1826 }
1827 }
1828
1829 #[test]
1830 fn test_regex_creation() {
1831 let pattern = r"^\d{3}-\d{2}-\d{4}$";
1832 let scalar = ScalarValue::regex(pattern);
1833
1834 assert!(scalar.is_regex());
1835 assert_eq!(scalar.scalar_type(), ScalarType::Regex);
1836 assert_eq!(scalar.value(), pattern);
1837
1838 let yaml_output = scalar.to_yaml_string();
1839 assert_eq!(yaml_output, format!("!!regex {}", pattern));
1840 }
1841
1842 #[test]
1843 fn test_regex_edge_cases() {
1844 let empty_regex = ScalarValue::regex("");
1846 assert!(empty_regex.is_regex());
1847 assert_eq!(empty_regex.value(), "");
1848 assert_eq!(empty_regex.to_yaml_string(), "!!regex ");
1849
1850 let special_chars = ScalarValue::regex(r"[.*+?^${}()|[\]\\]");
1852 assert!(special_chars.is_regex());
1853 assert_eq!(special_chars.value(), r"[.*+?^${}()|[\]\\]");
1854
1855 let unicode_regex = ScalarValue::regex(r"\p{L}+");
1857 assert!(unicode_regex.is_regex());
1858 assert_eq!(unicode_regex.value(), r"\p{L}+");
1859
1860 let long_pattern = "a".repeat(1000);
1862 let long_regex = ScalarValue::regex(&long_pattern);
1863 assert!(long_regex.is_regex());
1864 assert_eq!(long_regex.value(), long_pattern);
1865
1866 let quoted_regex = ScalarValue::regex(r#"'quoted' and "double quoted" with \\ backslash"#);
1868 assert!(quoted_regex.is_regex());
1869 assert_eq!(
1870 quoted_regex.value(),
1871 r#"'quoted' and "double quoted" with \\ backslash"#
1872 );
1873 }
1874
1875 #[test]
1876 fn test_regex_type_coercion() {
1877 let regex_scalar = ScalarValue::regex(r"\d+");
1878
1879 let string_scalar = regex_scalar.coerce_to_type(ScalarType::String).unwrap();
1881 assert_eq!(string_scalar.scalar_type(), ScalarType::String);
1882 assert_eq!(string_scalar.value(), r"\d+");
1883 assert!(!string_scalar.is_regex());
1884
1885 let str_scalar = ScalarValue::string("test.*");
1887 let regex_from_string = str_scalar.coerce_to_type(ScalarType::Regex).unwrap();
1888 assert_eq!(regex_from_string.scalar_type(), ScalarType::Regex);
1889 assert_eq!(regex_from_string.value(), "test.*");
1890 assert!(regex_from_string.is_regex());
1891
1892 assert!(regex_scalar.coerce_to_type(ScalarType::Integer).is_none());
1894 assert!(regex_scalar.coerce_to_type(ScalarType::Float).is_none());
1895 assert!(regex_scalar.coerce_to_type(ScalarType::Boolean).is_none());
1896 }
1897
1898 #[test]
1899 #[cfg(feature = "regex")]
1900 fn test_regex_compilation() {
1901 let regex_scalar = ScalarValue::regex(r"\d{3}-\d{4}");
1903 let compiled = regex_scalar.as_regex().unwrap();
1904 assert!(compiled.is_match("555-1234"));
1905 assert!(!compiled.is_match("not-a-phone"));
1906
1907 let string_scalar = ScalarValue::string("not a regex");
1909 assert!(string_scalar.as_regex().is_none());
1910
1911 let pattern_scalar = ScalarValue::string(r"^\w+@\w+\.\w+$");
1913 let email_regex = pattern_scalar.try_as_regex().unwrap();
1914 assert!(email_regex.is_match("test@example.com"));
1915 assert!(!email_regex.is_match("not-an-email"));
1916
1917 let invalid_scalar = ScalarValue::regex(r"[invalid(");
1919 assert!(invalid_scalar.as_regex().is_none());
1920
1921 let invalid_pattern = ScalarValue::string(r"[invalid(");
1923 assert!(invalid_pattern.try_as_regex().is_err());
1924 }
1925
1926 #[test]
1927 #[cfg(feature = "regex")]
1928 fn test_regex_extraction_use_cases() {
1929 let validation_rules = [
1931 ScalarValue::regex(r"^\d{5}$"), ScalarValue::regex(r"^[A-Z]{2}$"), ScalarValue::regex(r"^\(\d{3}\) \d{3}-\d{4}$"), ];
1935
1936 let test_values = ["12345", "CA", "(555) 123-4567"];
1937
1938 for (rule, value) in validation_rules.iter().zip(test_values.iter()) {
1939 let regex = rule.as_regex().unwrap();
1940 assert!(regex.is_match(value), "Pattern should match {}", value);
1941 }
1942
1943 let email_regex = ScalarValue::regex(r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$");
1945 let compiled = email_regex.as_regex().unwrap();
1946 assert!(compiled.is_match("user@example.com"));
1947 assert!(compiled.is_match("test.user+tag@sub.domain.org"));
1948 assert!(!compiled.is_match("invalid.email"));
1949
1950 let version_regex = ScalarValue::regex(r"^v(\d+)\.(\d+)\.(\d+)$");
1952 let compiled = version_regex.as_regex().unwrap();
1953 if let Some(captures) = compiled.captures("v1.2.3") {
1954 assert_eq!(captures.get(1).unwrap().as_str(), "1");
1955 assert_eq!(captures.get(2).unwrap().as_str(), "2");
1956 assert_eq!(captures.get(3).unwrap().as_str(), "3");
1957 } else {
1958 panic!("Should have matched version string");
1959 }
1960 }
1961
1962 #[test]
1963 fn test_type_coercion() {
1964 let str_scalar = ScalarValue::string("42");
1966 let int_scalar = str_scalar.coerce_to_type(ScalarType::Integer).unwrap();
1967 assert_eq!(int_scalar.scalar_type(), ScalarType::Integer);
1968 assert_eq!(int_scalar.value(), "42");
1969
1970 let bool_scalar = ScalarValue::string("true")
1972 .coerce_to_type(ScalarType::Boolean)
1973 .unwrap();
1974 assert_eq!(bool_scalar.scalar_type(), ScalarType::Boolean);
1975 assert_eq!(bool_scalar.value(), "true");
1976
1977 let yes_scalar = ScalarValue::string("yes")
1979 .coerce_to_type(ScalarType::Boolean)
1980 .unwrap();
1981 assert_eq!(yes_scalar.value(), "true");
1982
1983 let no_scalar = ScalarValue::string("no")
1984 .coerce_to_type(ScalarType::Boolean)
1985 .unwrap();
1986 assert_eq!(no_scalar.value(), "false");
1987
1988 let str_scalar = ScalarValue::string("not_a_number");
1990 assert!(str_scalar.coerce_to_type(ScalarType::Integer).is_none());
1991 }
1992
1993 #[test]
1994 fn test_auto_type_detection() {
1995 assert_eq!(ScalarValue::auto_detect_type("42"), ScalarType::Integer);
1997 assert_eq!(ScalarValue::auto_detect_type("3.14"), ScalarType::Float);
1998 assert_eq!(ScalarValue::auto_detect_type("true"), ScalarType::Boolean);
1999 assert_eq!(ScalarValue::auto_detect_type("false"), ScalarType::Boolean);
2000 assert_eq!(ScalarValue::auto_detect_type("yes"), ScalarType::Boolean);
2001 assert_eq!(ScalarValue::auto_detect_type("null"), ScalarType::Null);
2002 assert_eq!(ScalarValue::auto_detect_type("~"), ScalarType::Null);
2003 assert_eq!(ScalarValue::auto_detect_type(""), ScalarType::Null);
2004 assert_eq!(
2005 ScalarValue::auto_detect_type("2023-12-25"),
2006 ScalarType::Timestamp
2007 );
2008 assert_eq!(
2009 ScalarValue::auto_detect_type("2023-12-25T10:30:45"),
2010 ScalarType::Timestamp
2011 );
2012 #[cfg(feature = "base64")]
2013 assert_eq!(
2014 ScalarValue::auto_detect_type("SGVsbG8gV29ybGQ="),
2015 ScalarType::Binary
2016 );
2017 #[cfg(not(feature = "base64"))]
2018 assert_eq!(
2019 ScalarValue::auto_detect_type("SGVsbG8gV29ybGQ="),
2020 ScalarType::String
2021 );
2022 assert_eq!(
2023 ScalarValue::auto_detect_type("hello world"),
2024 ScalarType::String
2025 );
2026 }
2027
2028 #[test]
2029 fn test_from_yaml_scalar_creation() {
2030 let int_scalar = ScalarValue::parse("123");
2031 assert_eq!(int_scalar.scalar_type(), ScalarType::Integer);
2032
2033 let bool_scalar = ScalarValue::parse("true");
2034 assert_eq!(bool_scalar.scalar_type(), ScalarType::Boolean);
2035
2036 let timestamp_scalar = ScalarValue::parse("2023-12-25");
2037 assert_eq!(timestamp_scalar.scalar_type(), ScalarType::Timestamp);
2038
2039 let string_scalar = ScalarValue::parse("hello world");
2040 assert_eq!(string_scalar.scalar_type(), ScalarType::String);
2041 }
2042
2043 #[test]
2044 fn test_timestamp_pattern_matching() {
2045 assert!(ScalarValue::matches_iso8601_pattern("2023-12-25"));
2047 assert!(ScalarValue::matches_iso8601_pattern("2023-12-25T10:30:45"));
2048 assert!(ScalarValue::matches_iso8601_pattern("2023-12-25t10:30:45")); assert!(ScalarValue::matches_iso8601_pattern("2023-12-25 10:30:45"));
2050 assert!(ScalarValue::matches_iso8601_pattern("2023-01-01T00:00:00"));
2051 assert!(ScalarValue::matches_iso8601_pattern(
2052 "2001-12-14t21:59:43.10-05:00"
2053 )); assert!(!ScalarValue::matches_iso8601_pattern("2023-13-25")); assert!(!ScalarValue::matches_iso8601_pattern("23-12-25")); assert!(!ScalarValue::matches_iso8601_pattern("2023/12/25")); assert!(!ScalarValue::matches_iso8601_pattern("not-a-date"));
2060 assert!(!ScalarValue::matches_iso8601_pattern("2023"));
2061 }
2062
2063 #[test]
2064 #[cfg(feature = "base64")]
2065 fn test_base64_detection() {
2066 assert!(ScalarValue::looks_like_base64("SGVsbG8=")); assert!(ScalarValue::looks_like_base64("V29ybGQ=")); assert!(ScalarValue::looks_like_base64("SGVsbG8gV29ybGQ=")); assert!(ScalarValue::looks_like_base64("AAAA")); assert!(!ScalarValue::looks_like_base64("Hello")); assert!(!ScalarValue::looks_like_base64("SGVsbG8")); assert!(!ScalarValue::looks_like_base64("")); assert!(!ScalarValue::looks_like_base64("SGV@")); assert!(!ScalarValue::looks_like_base64("SGVsbG8g===")); }
2079
2080 #[test]
2081 #[cfg(feature = "base64")]
2082 fn test_binary_yaml_output_with_tags() {
2083 let data = b"Binary data here";
2084 let scalar = ScalarValue::binary(data);
2085 let yaml_output = scalar.to_yaml_string();
2086
2087 assert!(yaml_output.starts_with("!!binary "));
2088
2089 let base64_part = &yaml_output[9..]; let decoded = base64_decode(base64_part).expect("Should decode");
2092 assert_eq!(decoded, data);
2093 }
2094
2095 #[test]
2096 #[cfg(feature = "base64")]
2097 fn test_special_data_types_with_different_styles() {
2098 let data = b"test";
2100 let binary_scalar = ScalarValue::binary(data);
2101
2102 let mut styled_binary = binary_scalar;
2104 styled_binary.style = ScalarStyle::DoubleQuoted;
2105
2106 assert_eq!(styled_binary.to_yaml_string(), "!!binary \"dGVzdA==\"");
2108 }
2109
2110 #[test]
2111 fn test_type_checking_methods() {
2112 #[cfg(feature = "base64")]
2113 let binary_scalar = ScalarValue::binary(b"test");
2114 let timestamp_scalar = ScalarValue::timestamp("2023-12-25");
2115 let regex_scalar = ScalarValue::regex(r"\d+");
2116 let string_scalar = ScalarValue::string("hello");
2117
2118 #[cfg(feature = "base64")]
2120 assert!(binary_scalar.is_binary());
2121 #[cfg(feature = "base64")]
2122 assert!(!binary_scalar.is_timestamp());
2123 #[cfg(feature = "base64")]
2124 assert!(!binary_scalar.is_regex());
2125
2126 #[cfg(feature = "base64")]
2127 assert!(!timestamp_scalar.is_binary());
2128 assert!(timestamp_scalar.is_timestamp());
2129 assert!(!timestamp_scalar.is_regex());
2130
2131 #[cfg(feature = "base64")]
2132 assert!(!regex_scalar.is_binary());
2133 assert!(!regex_scalar.is_timestamp());
2134 assert!(regex_scalar.is_regex());
2135
2136 #[cfg(feature = "base64")]
2137 assert!(!string_scalar.is_binary());
2138 assert!(!string_scalar.is_timestamp());
2139 assert!(!string_scalar.is_regex());
2140 }
2141
2142 #[test]
2143 fn test_binary_number_parsing() {
2144 assert_eq!(ScalarValue::parse_integer("0b1010"), Some(10));
2146 assert_eq!(ScalarValue::parse_integer("0b11111111"), Some(255));
2147 assert_eq!(ScalarValue::parse_integer("0B101"), Some(5)); assert_eq!(ScalarValue::parse_integer("-0b1010"), Some(-10));
2149 assert_eq!(ScalarValue::parse_integer("+0b101"), Some(5));
2150
2151 assert_eq!(ScalarValue::auto_detect_type("0b1010"), ScalarType::Integer);
2153 assert_eq!(
2154 ScalarValue::auto_detect_type("0B11111111"),
2155 ScalarType::Integer
2156 );
2157
2158 assert_eq!(ScalarValue::parse_integer("0b1012"), None); assert_eq!(ScalarValue::parse_integer("0b"), None); }
2162
2163 #[test]
2164 fn test_modern_octal_number_parsing() {
2165 assert_eq!(ScalarValue::parse_integer("0o755"), Some(493)); assert_eq!(ScalarValue::parse_integer("0o644"), Some(420)); assert_eq!(ScalarValue::parse_integer("0O777"), Some(511)); assert_eq!(ScalarValue::parse_integer("-0o755"), Some(-493));
2170 assert_eq!(ScalarValue::parse_integer("+0o644"), Some(420));
2171
2172 assert_eq!(ScalarValue::auto_detect_type("0o755"), ScalarType::Integer);
2174 assert_eq!(ScalarValue::auto_detect_type("0O644"), ScalarType::Integer);
2175
2176 assert_eq!(ScalarValue::parse_integer("0o789"), None); assert_eq!(ScalarValue::parse_integer("0o"), None); }
2180
2181 #[test]
2182 fn test_legacy_octal_number_parsing() {
2183 assert_eq!(ScalarValue::parse_integer("0755"), Some(493));
2185 assert_eq!(ScalarValue::parse_integer("0644"), Some(420));
2186 assert_eq!(ScalarValue::parse_integer("0777"), Some(511));
2187
2188 assert_eq!(ScalarValue::auto_detect_type("0755"), ScalarType::Integer);
2190 assert_eq!(ScalarValue::auto_detect_type("0644"), ScalarType::Integer);
2191
2192 assert_eq!(ScalarValue::parse_integer("0"), Some(0)); assert_eq!(ScalarValue::parse_integer("00"), Some(0)); assert_eq!(ScalarValue::parse_integer("0789"), None);
2198 assert_eq!(ScalarValue::parse_integer("0128"), None);
2199 }
2200
2201 #[test]
2202 fn test_hexadecimal_number_parsing() {
2203 assert_eq!(ScalarValue::parse_integer("0xFF"), Some(255));
2205 assert_eq!(ScalarValue::parse_integer("0x1A"), Some(26));
2206 assert_eq!(ScalarValue::parse_integer("0XFF"), Some(255)); assert_eq!(ScalarValue::parse_integer("-0xFF"), Some(-255));
2208 assert_eq!(ScalarValue::parse_integer("+0x1A"), Some(26));
2209
2210 assert_eq!(ScalarValue::auto_detect_type("0xFF"), ScalarType::Integer);
2212 assert_eq!(ScalarValue::auto_detect_type("0X1A"), ScalarType::Integer);
2213 }
2214
2215 #[test]
2216 fn test_decimal_number_parsing() {
2217 assert_eq!(ScalarValue::parse_integer("42"), Some(42));
2219 assert_eq!(ScalarValue::parse_integer("123"), Some(123));
2220 assert_eq!(ScalarValue::parse_integer("-42"), Some(-42));
2221 assert_eq!(ScalarValue::parse_integer("+123"), Some(123));
2222
2223 assert_eq!(ScalarValue::auto_detect_type("42"), ScalarType::Integer);
2225 assert_eq!(ScalarValue::auto_detect_type("-123"), ScalarType::Integer);
2226 }
2227
2228 #[test]
2229 fn test_number_format_yaml_output() {
2230 let binary_scalar = ScalarValue::parse("0b1010");
2232 assert_eq!(binary_scalar.scalar_type(), ScalarType::Integer);
2233 assert_eq!(binary_scalar.value(), "0b1010");
2234
2235 let octal_scalar = ScalarValue::parse("0o755");
2236 assert_eq!(octal_scalar.scalar_type(), ScalarType::Integer);
2237 assert_eq!(octal_scalar.value(), "0o755");
2238
2239 let hex_scalar = ScalarValue::parse("0xFF");
2240 assert_eq!(hex_scalar.scalar_type(), ScalarType::Integer);
2241 assert_eq!(hex_scalar.value(), "0xFF");
2242
2243 let legacy_octal_scalar = ScalarValue::parse("0755");
2244 assert_eq!(legacy_octal_scalar.scalar_type(), ScalarType::Integer);
2245 assert_eq!(legacy_octal_scalar.value(), "0755");
2246 }
2247
2248 #[test]
2249 fn test_unix_timestamp_bounds() {
2250 assert!(!ScalarValue::is_valid_timestamp_static("0"));
2253 assert!(ScalarValue::is_valid_timestamp_static("1"));
2254 assert!(ScalarValue::is_valid_timestamp_static("4102444799"));
2255 assert!(!ScalarValue::is_valid_timestamp_static("4102444800"));
2256
2257 let s = ScalarValue::string("x");
2259 assert!(s.is_valid_timestamp("1"));
2260 assert!(!s.is_valid_timestamp("0"));
2261 }
2262
2263 #[test]
2264 fn test_iso8601_time_component_bounds() {
2265 assert!(ScalarValue::matches_iso8601_pattern("2023-01-01T23:59:59"));
2267 assert!(!ScalarValue::matches_iso8601_pattern("2023-01-01T24:00:00"));
2268 assert!(!ScalarValue::matches_iso8601_pattern("2023-01-01T10:60:00"));
2269 assert!(!ScalarValue::matches_iso8601_pattern("2023-01-01T10:30:60"));
2270
2271 assert!(ScalarValue::matches_iso8601_pattern("2023-01-31"));
2273 assert!(!ScalarValue::matches_iso8601_pattern("2023-01-00"));
2274 assert!(!ScalarValue::matches_iso8601_pattern("2023-01-32"));
2275
2276 assert!(!ScalarValue::matches_iso8601_pattern("2023-01-01X10:30:45"));
2278 assert!(!ScalarValue::matches_iso8601_pattern("2023-01-01T10:30:xx"));
2280
2281 assert!(!ScalarValue::matches_iso8601_pattern("2023X12-25"));
2285 assert!(!ScalarValue::matches_iso8601_pattern("2023-12X25"));
2286 }
2287
2288 #[test]
2289 #[cfg(feature = "base64")]
2290 fn test_base64_padding_bounds() {
2291 assert!(ScalarValue::looks_like_base64("QQ==")); assert!(ScalarValue::looks_like_base64("SGVsbG8=")); assert!(!ScalarValue::looks_like_base64("QQ=Q"));
2297 assert!(!ScalarValue::looks_like_base64("QQQ"));
2299 }
2300
2301 #[test]
2302 fn test_coerce_to_null() {
2303 let s = ScalarValue::string("null");
2305 assert_eq!(
2306 s.coerce_to_type(ScalarType::Null).map(|v| v.scalar_type()),
2307 Some(ScalarType::Null)
2308 );
2309 assert!(ScalarValue::string("~")
2310 .coerce_to_type(ScalarType::Null)
2311 .is_some());
2312 assert!(ScalarValue::string("")
2313 .coerce_to_type(ScalarType::Null)
2314 .is_some());
2315 assert!(ScalarValue::string("notnull")
2316 .coerce_to_type(ScalarType::Null)
2317 .is_none());
2318 }
2319
2320 #[test]
2321 fn test_parse_escape_single_quote() {
2322 assert_eq!(ScalarValue::parse_escape_sequences("a\\'b"), "a'b");
2324 }
2325
2326 #[test]
2327 fn test_parse_preserves_string_style() {
2328 let plain = ScalarValue::parse("hello");
2330 assert_eq!(plain.scalar_type(), ScalarType::String);
2331 assert_eq!(plain.style, ScalarStyle::Plain);
2332
2333 let multiline = ScalarValue::parse("multi line\nvalue");
2336 assert_eq!(multiline.scalar_type(), ScalarType::String);
2337 assert_eq!(multiline.style, ScalarStyle::Literal);
2338 }
2339
2340 #[test]
2341 fn test_from_scalar_type_and_style() {
2342 use crate::yaml::Document;
2343 use rowan::ast::AstNode;
2344 use std::str::FromStr;
2345
2346 let doc = Document::from_str("k: null\n").unwrap();
2348 let mapping = doc.as_mapping().unwrap();
2349 let null_node = mapping.get("k").unwrap();
2350 let null_scalar = crate::yaml::Scalar::cast(null_node.syntax().clone()).unwrap();
2351 let sv = ScalarValue::from_scalar(&null_scalar);
2352 assert_eq!(sv.scalar_type(), ScalarType::Null);
2353 assert_eq!(sv.style, ScalarStyle::Plain);
2354
2355 let doc = Document::from_str("k: \"hi\"\n").unwrap();
2357 let node = doc.as_mapping().unwrap().get("k").unwrap();
2358 let scalar = crate::yaml::Scalar::cast(node.syntax().clone()).unwrap();
2359 let sv = ScalarValue::from_scalar(&scalar);
2360 assert_eq!(sv.scalar_type(), ScalarType::String);
2361 assert_eq!(sv.style, ScalarStyle::DoubleQuoted);
2362
2363 let doc = Document::from_str("k: 'hi'\n").unwrap();
2365 let node = doc.as_mapping().unwrap().get("k").unwrap();
2366 let scalar = crate::yaml::Scalar::cast(node.syntax().clone()).unwrap();
2367 let sv = ScalarValue::from_scalar(&scalar);
2368 assert_eq!(sv.style, ScalarStyle::SingleQuoted);
2369 }
2370
2371 #[test]
2372 fn test_from_scalar_ampersand_in_plain_is_string() {
2373 use crate::yaml::Document;
2377 use rowan::ast::AstNode;
2378 use std::str::FromStr;
2379
2380 let doc = Document::from_str("k: 3.1&1\n").unwrap();
2381 let node = doc.as_mapping().unwrap().get("k").unwrap();
2382 let scalar = crate::yaml::Scalar::cast(node.syntax().clone()).unwrap();
2383 let sv = ScalarValue::from_scalar(&scalar);
2384 assert_eq!(sv.scalar_type(), ScalarType::String);
2385 assert_eq!(sv.style, ScalarStyle::Plain);
2386 }
2387
2388 #[test]
2389 fn test_from_scalar_signed_yaml_infinity_is_float() {
2390 use crate::yaml::Document;
2393 use rowan::ast::AstNode;
2394 use std::str::FromStr;
2395
2396 let doc = Document::from_str("k: +.INF\n").unwrap();
2397 let node = doc.as_mapping().unwrap().get("k").unwrap();
2398 let scalar = crate::yaml::Scalar::cast(node.syntax().clone()).unwrap();
2399 let sv = ScalarValue::from_scalar(&scalar);
2400 assert_eq!(sv.scalar_type(), ScalarType::Float);
2401 }
2402
2403 #[test]
2404 fn test_classify_plain_bare_inf_nan_is_string() {
2405 for input in [
2409 "inf", "Inf", "InF", "INF", "iNf", "nan", "NaN", "NAN", "nAn", "infinity", "Infinity",
2410 "INFINITY", "+inf", "-Inf", "+nan",
2411 ] {
2412 assert_eq!(
2413 ScalarValue::classify_plain(input),
2414 CoreScalarType::String,
2415 "bare {input:?} should classify as String"
2416 );
2417 }
2418 for input in [".inf", ".Inf", ".INF", "+.inf", "-.INF", ".nan"] {
2420 assert_eq!(
2421 ScalarValue::classify_plain(input),
2422 CoreScalarType::Float,
2423 "{input:?} should classify as Float"
2424 );
2425 }
2426 }
2427
2428 #[test]
2429 fn test_from_scalar_block_style_is_string() {
2430 use crate::yaml::Document;
2433 use rowan::ast::AstNode;
2434 use std::str::FromStr;
2435
2436 let doc = Document::from_str("k: |\n 1\n").unwrap();
2437 let node = doc.as_mapping().unwrap().get("k").unwrap();
2438 let scalar = crate::yaml::Scalar::cast(node.syntax().clone()).unwrap();
2439 let sv = ScalarValue::from_scalar(&scalar);
2440 assert_eq!(sv.style, ScalarStyle::Literal);
2441 assert_eq!(sv.scalar_type(), ScalarType::String);
2442
2443 let doc = Document::from_str("k: >\n x\n").unwrap();
2444 let node = doc.as_mapping().unwrap().get("k").unwrap();
2445 let scalar = crate::yaml::Scalar::cast(node.syntax().clone()).unwrap();
2446 let sv = ScalarValue::from_scalar(&scalar);
2447 assert_eq!(sv.style, ScalarStyle::Folded);
2448 assert_eq!(sv.scalar_type(), ScalarType::String);
2449 }
2450
2451 #[test]
2452 fn test_needs_quoting_special_chars() {
2453 let cases = [
2458 ("&anchor", "'&anchor'"),
2459 ("*alias", "'*alias'"),
2460 ("!tag", "'!tag'"),
2461 ("a|b", "'a|b'"),
2462 ("it's", "\"it's\""),
2463 ("a\"b", "'a\"b'"),
2464 ("50%", "'50%'"),
2465 ("on", "'on'"),
2466 ("off", "'off'"),
2467 ("On", "'On'"),
2468 ("OFF", "'OFF'"),
2469 ("yes", "'yes'"),
2470 ("no", "'no'"),
2471 ("true", "'true'"),
2472 ("false", "'false'"),
2473 ("null", "'null'"),
2474 ];
2475 for (input, expected) in cases {
2476 assert_eq!(ScalarValue::string(input).to_yaml_string(), expected);
2477 }
2478 assert_eq!(ScalarValue::string("plain").to_yaml_string(), "plain");
2480 }
2481
2482 #[test]
2483 fn test_literal_and_folded_nonempty() {
2484 let s = ScalarValue::with_style("line one\nline two", ScalarStyle::Literal);
2487 assert_eq!(s.to_yaml_string(), "|-\n line one\n line two");
2488
2489 let s = ScalarValue::with_style("line one\nline two", ScalarStyle::Folded);
2490 assert_eq!(s.to_yaml_string(), ">-\n line one\n line two");
2491 }
2492
2493 #[test]
2494 fn test_display_matches_yaml_string() {
2495 let s = ScalarValue::string("true");
2496 assert_eq!(format!("{}", s), s.to_yaml_string());
2497 assert_eq!(format!("{}", s), "'true'");
2498 }
2499
2500 #[test]
2501 fn test_as_yaml_token_kind_per_type() {
2502 use crate::lex::SyntaxKind;
2503 use rowan::ast::AstNode;
2504
2505 let cases = [
2506 (ScalarValue::parse("42"), SyntaxKind::INT),
2507 (ScalarValue::parse("3.14"), SyntaxKind::FLOAT),
2508 (ScalarValue::parse("true"), SyntaxKind::BOOL),
2509 (ScalarValue::parse("null"), SyntaxKind::NULL),
2510 (ScalarValue::string("hello"), SyntaxKind::STRING),
2511 ];
2512 for (value, expected) in cases {
2513 let mut builder = rowan::GreenNodeBuilder::new();
2514 crate::AsYaml::build_content(&value, &mut builder, 0, false);
2515 let green = builder.finish();
2516 let node = rowan::SyntaxNode::<crate::yaml::Lang>::new_root(green);
2517 let scalar = crate::yaml::Scalar::cast(node).unwrap();
2518 let kind = scalar
2519 .syntax()
2520 .children_with_tokens()
2521 .filter_map(|c| c.into_token())
2522 .next()
2523 .unwrap()
2524 .kind();
2525 assert_eq!(kind, expected, "value {:?}", value.value());
2526 }
2527 }
2528
2529 #[test]
2530 fn test_as_yaml_quotes_string_that_looks_special() {
2531 use std::str::FromStr;
2532
2533 let doc = crate::yaml::Document::from_str("key: x\n").unwrap();
2536 doc.as_mapping()
2537 .unwrap()
2538 .set("key", ScalarValue::string("null"));
2539 assert_eq!(doc.to_string(), "key: 'null'\n");
2540
2541 let doc = crate::yaml::Document::from_str("key: x\n").unwrap();
2543 doc.as_mapping()
2544 .unwrap()
2545 .set("key", ScalarValue::string("has: colon"));
2546 assert_eq!(doc.to_string(), "key: 'has: colon'\n");
2547
2548 let doc = crate::yaml::Document::from_str("key: x\n").unwrap();
2550 doc.as_mapping().unwrap().set("key", ScalarValue::null());
2551 assert_eq!(doc.to_string(), "key: null\n");
2552 }
2553
2554 #[test]
2555 #[cfg(feature = "regex")]
2556 fn test_as_regex_type_gated() {
2557 let re = ScalarValue::regex(r"\d+");
2559 assert!(re.as_regex().is_some());
2560
2561 let not_re = ScalarValue::string(r"\d+");
2562 assert!(not_re.as_regex().is_none());
2563
2564 assert!(not_re.try_as_regex().is_ok());
2566 assert!(ScalarValue::string("(").try_as_regex().is_err());
2567 }
2568}