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)]
39pub enum ScalarType {
40 String,
42 Integer,
44 Float,
46 Boolean,
48 Null,
50 #[cfg(feature = "base64")]
52 Binary,
53 Timestamp,
55 Regex,
57}
58
59#[derive(Debug, Clone, PartialEq, Eq)]
61pub struct ScalarValue {
62 value: String,
64 style: ScalarStyle,
66 scalar_type: ScalarType,
68}
69
70impl ScalarValue {
71 pub fn string(value: impl Into<String>) -> Self {
100 let value = value.into();
101 let style = Self::detect_style(&value);
102 let scalar_type = ScalarType::String;
104 Self {
105 value,
106 style,
107 scalar_type,
108 }
109 }
110
111 pub fn parse_escape_sequences(text: &str) -> String {
113 let mut result = String::with_capacity(text.len()); let mut chars = text.chars().peekable();
115
116 while let Some(ch) = chars.next() {
117 if ch == '\\' {
118 if let Some(&escaped) = chars.peek() {
119 chars.next(); match escaped {
121 'n' => result.push('\n'),
123 't' => result.push('\t'),
124 'r' => result.push('\r'),
125 'b' => result.push('\x08'),
126 'f' => result.push('\x0C'),
127 'a' => result.push('\x07'), 'e' => result.push('\x1B'), 'v' => result.push('\x0B'), '0' => result.push('\0'), '\\' => result.push('\\'),
132 '"' => result.push('"'),
133 '\'' => result.push('\''),
134 '/' => result.push('/'),
135 ' ' => {
137 if let Some(&'\n') = chars.peek() {
139 chars.next(); continue;
142 } else {
143 result.push(' ');
144 }
145 }
146 '\n' => {
147 continue;
149 }
150 'x' => {
152 let mut hex_chars = [0u8; 2];
154 let mut count = 0;
155 for (i, ch) in chars.by_ref().take(2).enumerate() {
156 if let Some(digit) = ch.to_digit(16) {
157 hex_chars[i] = digit as u8;
158 count += 1;
159 } else {
160 result.push('\\');
162 result.push('x');
163 for &hex_char in hex_chars.iter().take(count) {
164 result.push(char::from_digit(hex_char as u32, 16).unwrap());
165 }
166 result.push(ch);
167 break;
168 }
169 }
170 if count == 2 {
171 let code = hex_chars[0] * 16 + hex_chars[1];
172 result.push(code as char);
173 } else if count > 0 {
174 result.push('\\');
176 result.push('x');
177 for &hex_char in hex_chars.iter().take(count) {
178 result.push(char::from_digit(hex_char as u32, 16).unwrap());
179 }
180 }
181 }
182 'u' => {
183 let hex_digits: String = chars.by_ref().take(4).collect();
185 if hex_digits.len() == 4 {
186 if let Ok(code) = u16::from_str_radix(&hex_digits, 16) {
187 if let Some(unicode_char) = char::from_u32(code as u32) {
188 result.push(unicode_char);
189 } else {
190 result.push('\\');
192 result.push('u');
193 result.push_str(&hex_digits);
194 }
195 } else {
196 result.push('\\');
198 result.push('u');
199 result.push_str(&hex_digits);
200 }
201 } else {
202 result.push('\\');
204 result.push('u');
205 result.push_str(&hex_digits);
206 }
207 }
208 'U' => {
209 let hex_digits: String = chars.by_ref().take(8).collect();
211 if hex_digits.len() == 8 {
212 if let Ok(code) = u32::from_str_radix(&hex_digits, 16) {
213 if let Some(unicode_char) = char::from_u32(code) {
214 result.push(unicode_char);
215 } else {
216 result.push('\\');
218 result.push('U');
219 result.push_str(&hex_digits);
220 }
221 } else {
222 result.push('\\');
224 result.push('U');
225 result.push_str(&hex_digits);
226 }
227 } else {
228 result.push('\\');
230 result.push('U');
231 result.push_str(&hex_digits);
232 }
233 }
234 _ => {
236 result.push('\\');
237 result.push(escaped);
238 }
239 }
240 } else {
241 result.push('\\');
243 }
244 } else {
245 result.push(ch);
246 }
247 }
248
249 result
250 }
251
252 pub fn with_style(value: impl Into<String>, style: ScalarStyle) -> Self {
254 Self {
255 value: value.into(),
256 style,
257 scalar_type: ScalarType::String,
258 }
259 }
260
261 pub fn plain(value: impl Into<String>) -> Self {
263 Self::with_style(value, ScalarStyle::Plain)
264 }
265
266 pub fn single_quoted(value: impl Into<String>) -> Self {
268 Self::with_style(value, ScalarStyle::SingleQuoted)
269 }
270
271 pub fn double_quoted(value: impl Into<String>) -> Self {
273 Self::with_style(value, ScalarStyle::DoubleQuoted)
274 }
275
276 pub fn literal(value: impl Into<String>) -> Self {
278 Self::with_style(value, ScalarStyle::Literal)
279 }
280
281 pub fn folded(value: impl Into<String>) -> Self {
283 Self::with_style(value, ScalarStyle::Folded)
284 }
285
286 pub fn null() -> Self {
288 Self {
289 value: "null".to_string(),
290 style: ScalarStyle::Plain,
291 scalar_type: ScalarType::Null,
292 }
293 }
294
295 #[cfg(feature = "base64")]
297 pub fn binary(data: &[u8]) -> Self {
298 let encoded = base64_encode(data);
299 Self {
300 value: encoded,
301 style: ScalarStyle::Plain,
302 scalar_type: ScalarType::Binary,
303 }
304 }
305
306 pub fn timestamp(value: impl Into<String>) -> Self {
308 Self {
309 value: value.into(),
310 style: ScalarStyle::Plain,
311 scalar_type: ScalarType::Timestamp,
312 }
313 }
314
315 pub fn regex(pattern: impl Into<String>) -> Self {
317 Self {
318 value: pattern.into(),
319 style: ScalarStyle::Plain,
320 scalar_type: ScalarType::Regex,
321 }
322 }
323
324 pub fn value(&self) -> &str {
326 &self.value
327 }
328
329 pub fn style(&self) -> ScalarStyle {
331 self.style
332 }
333
334 pub fn scalar_type(&self) -> ScalarType {
336 self.scalar_type
337 }
338
339 pub fn to_i64(&self) -> Option<i64> {
343 if self.scalar_type == ScalarType::Integer {
344 Self::parse_integer(&self.value)
345 } else {
346 None
347 }
348 }
349
350 pub fn to_f64(&self) -> Option<f64> {
354 if self.scalar_type == ScalarType::Float {
355 self.value.trim().parse::<f64>().ok()
356 } else {
357 None
358 }
359 }
360
361 pub fn to_bool(&self) -> Option<bool> {
366 if self.scalar_type == ScalarType::Boolean {
367 match self.value.to_lowercase().as_str() {
368 "true" | "yes" | "on" => Some(true),
369 "false" | "no" | "off" => Some(false),
370 _ => None,
371 }
372 } else {
373 None
374 }
375 }
376
377 #[cfg(feature = "base64")]
379 pub fn as_binary(&self) -> Option<Result<Vec<u8>, String>> {
380 match self.scalar_type {
381 ScalarType::Binary => Some(base64_decode(&self.value)),
382 _ => None,
383 }
384 }
385
386 #[cfg(feature = "base64")]
388 pub fn is_binary(&self) -> bool {
389 self.scalar_type == ScalarType::Binary
390 }
391
392 pub fn is_timestamp(&self) -> bool {
394 self.scalar_type == ScalarType::Timestamp
395 }
396
397 pub fn is_regex(&self) -> bool {
399 self.scalar_type == ScalarType::Regex
400 }
401
402 #[cfg(feature = "regex")]
419 pub fn as_regex(&self) -> Option<regex::Regex> {
420 if self.scalar_type == ScalarType::Regex {
421 regex::Regex::new(&self.value).ok()
422 } else {
423 None
424 }
425 }
426
427 #[cfg(feature = "regex")]
445 pub fn try_as_regex(&self) -> Result<regex::Regex, regex::Error> {
446 regex::Regex::new(&self.value)
447 }
448
449 pub fn coerce_to_type(&self, target_type: ScalarType) -> Option<ScalarValue> {
451 if self.scalar_type == target_type {
452 return Some(self.clone());
453 }
454
455 match target_type {
456 ScalarType::String => Some(ScalarValue {
457 value: self.value.clone(),
458 style: ScalarStyle::Plain,
459 scalar_type: ScalarType::String,
460 }),
461 ScalarType::Integer => Self::parse_integer(&self.value).map(ScalarValue::from),
462 ScalarType::Float => self.value.parse::<f64>().ok().map(ScalarValue::from),
463 ScalarType::Boolean => match self.value.to_lowercase().as_str() {
464 "true" | "yes" | "on" | "1" => Some(ScalarValue::from(true)),
465 "false" | "no" | "off" | "0" => Some(ScalarValue::from(false)),
466 _ => None,
467 },
468 ScalarType::Null => match self.value.to_lowercase().as_str() {
469 "null" | "~" | "" => Some(ScalarValue::null()),
470 _ => None,
471 },
472 #[cfg(feature = "base64")]
473 ScalarType::Binary => {
474 if base64_decode(&self.value).is_ok() {
476 Some(ScalarValue {
477 value: self.value.clone(),
478 style: ScalarStyle::Plain,
479 scalar_type: ScalarType::Binary,
480 })
481 } else {
482 None
483 }
484 }
485 ScalarType::Timestamp => {
486 if self.is_valid_timestamp(&self.value) {
488 Some(ScalarValue::timestamp(&self.value))
489 } else {
490 None
491 }
492 }
493 ScalarType::Regex => {
494 Some(ScalarValue::regex(&self.value))
496 }
497 }
498 }
499
500 pub(crate) fn parse_integer(value: &str) -> Option<i64> {
503 let value = value.trim();
504
505 let (is_negative, value) = if let Some(stripped) = value.strip_prefix('-') {
507 (true, stripped)
508 } else if let Some(stripped) = value.strip_prefix('+') {
509 (false, stripped)
510 } else {
511 (false, value)
512 };
513
514 let parsed = if let Some(hex_part) = value
515 .strip_prefix("0x")
516 .or_else(|| value.strip_prefix("0X"))
517 {
518 i64::from_str_radix(hex_part, 16).ok()
520 } else if let Some(bin_part) = value
521 .strip_prefix("0b")
522 .or_else(|| value.strip_prefix("0B"))
523 {
524 i64::from_str_radix(bin_part, 2).ok()
526 } else if let Some(oct_part) = value
527 .strip_prefix("0o")
528 .or_else(|| value.strip_prefix("0O"))
529 {
530 i64::from_str_radix(oct_part, 8).ok()
532 } else if value.starts_with('0')
533 && value.len() > 1
534 && value.chars().all(|c| c.is_ascii_digit())
535 {
536 i64::from_str_radix(value, 8).ok()
538 } else {
539 value.parse::<i64>().ok()
541 };
542
543 parsed.map(|n| if is_negative { -n } else { n })
544 }
545
546 pub fn auto_detect_type(value: &str) -> ScalarType {
548 match value.to_lowercase().as_str() {
550 "null" | "~" | "" => return ScalarType::Null,
551 _ => {}
552 }
553
554 match value.to_lowercase().as_str() {
556 "true" | "false" | "yes" | "no" | "on" | "off" => return ScalarType::Boolean,
557 _ => {}
558 }
559
560 if Self::parse_integer(value).is_some() {
562 return ScalarType::Integer;
563 }
564 if value.parse::<f64>().is_ok() {
565 return ScalarType::Float;
566 }
567
568 if Self::is_valid_timestamp_static(value) {
570 return ScalarType::Timestamp;
571 }
572
573 #[cfg(feature = "base64")]
575 if Self::looks_like_base64(value) && base64_decode(value).is_ok() {
576 return ScalarType::Binary;
577 }
578
579 ScalarType::String
581 }
582
583 pub fn parse(value: impl Into<String>) -> Self {
613 let value = value.into();
614 let scalar_type = Self::auto_detect_type(&value);
615 let style = match scalar_type {
618 ScalarType::String => Self::detect_style(&value),
619 _ => ScalarStyle::Plain,
621 };
622
623 Self {
624 value,
625 style,
626 scalar_type,
627 }
628 }
629
630 pub fn from_scalar(scalar: &crate::yaml::Scalar) -> Self {
636 use crate::lex::SyntaxKind;
637 use rowan::ast::AstNode;
638
639 let value = scalar.as_string();
640
641 let syntax_node = scalar.syntax();
643 let scalar_type = if let Some(token) = syntax_node.first_token() {
644 match token.kind() {
645 SyntaxKind::INT => ScalarType::Integer,
646 SyntaxKind::FLOAT => ScalarType::Float,
647 SyntaxKind::BOOL => ScalarType::Boolean,
648 SyntaxKind::NULL => ScalarType::Null,
649 SyntaxKind::STRING => ScalarType::String,
650 _ => ScalarType::String, }
652 } else {
653 ScalarType::String
654 };
655
656 let raw_text = scalar.value();
658 let style = if raw_text.starts_with('"') && raw_text.ends_with('"') {
659 ScalarStyle::DoubleQuoted
660 } else if raw_text.starts_with('\'') && raw_text.ends_with('\'') {
661 ScalarStyle::SingleQuoted
662 } else {
663 ScalarStyle::Plain
664 };
665
666 Self {
667 value,
668 style,
669 scalar_type,
670 }
671 }
672
673 #[cfg(feature = "base64")]
675 fn looks_like_base64(value: &str) -> bool {
676 if value.is_empty() {
677 return false;
678 }
679
680 if value.len() < 4 || value.len() % 4 != 0 {
683 return false;
684 }
685
686 let padding_count = value.chars().filter(|&c| c == '=').count();
687 if padding_count > 2 {
688 return false;
689 }
690
691 if !value
693 .chars()
694 .all(|c| matches!(c, 'A'..='Z' | 'a'..='z' | '0'..='9' | '+' | '/' | '='))
695 {
696 return false;
697 }
698
699 if padding_count > 0 {
701 let padding_start = value.len() - padding_count;
702 if !value[padding_start..].chars().all(|c| c == '=') {
703 return false;
704 }
705 if value[..padding_start].contains('=') {
707 return false;
708 }
709 }
710
711 base64_decode(value).is_ok()
714 }
715
716 fn is_valid_timestamp(&self, value: &str) -> bool {
718 Self::is_valid_timestamp_static(value)
719 }
720
721 fn is_valid_timestamp_static(value: &str) -> bool {
723 if Self::matches_iso8601_pattern(value) {
726 return true;
727 }
728
729 if let Ok(timestamp) = value.parse::<u64>() {
731 return timestamp > 0 && timestamp < 4_102_444_800; }
734
735 false
736 }
737
738 fn matches_iso8601_pattern(value: &str) -> bool {
740 let chars: Vec<char> = value.chars().collect();
741
742 if chars.len() < 10 {
744 return false;
745 }
746
747 if !(chars[0..4].iter().all(|c| c.is_ascii_digit())
749 && chars[4] == '-'
750 && chars[5..7].iter().all(|c| c.is_ascii_digit())
751 && chars[7] == '-'
752 && chars[8..10].iter().all(|c| c.is_ascii_digit()))
753 {
754 return false;
755 }
756
757 let month_str: String = chars[5..7].iter().collect();
759 let day_str: String = chars[8..10].iter().collect();
760
761 if let (Ok(month), Ok(day)) = (month_str.parse::<u8>(), day_str.parse::<u8>()) {
762 if !(1..=12).contains(&month) || !(1..=31).contains(&day) {
763 return false;
764 }
765 } else {
766 return false;
767 }
768
769 if chars.len() == 10 {
771 return true;
772 }
773
774 if chars.len() >= 19 {
776 let sep = chars[10];
777 if (sep == 'T' || sep == 't' || sep == ' ')
778 && chars[11..13].iter().all(|c| c.is_ascii_digit())
779 && chars[13] == ':'
780 && chars[14..16].iter().all(|c| c.is_ascii_digit())
781 && chars[16] == ':'
782 && chars[17..19].iter().all(|c| c.is_ascii_digit())
783 {
784 let hour_str: String = chars[11..13].iter().collect();
786 let minute_str: String = chars[14..16].iter().collect();
787 let second_str: String = chars[17..19].iter().collect();
788
789 if let (Ok(hour), Ok(minute), Ok(second)) = (
790 hour_str.parse::<u8>(),
791 minute_str.parse::<u8>(),
792 second_str.parse::<u8>(),
793 ) {
794 if hour > 23 || minute > 59 || second > 59 {
795 return false;
796 }
797 } else {
798 return false;
799 }
800
801 return true;
802 }
803 }
804
805 false
806 }
807
808 fn detect_style(value: &str) -> ScalarStyle {
810 if Self::needs_quoting(value) {
812 if !value.contains('\'') {
814 ScalarStyle::SingleQuoted
815 } else {
816 ScalarStyle::DoubleQuoted
817 }
818 } else if value.contains('\n') {
819 ScalarStyle::Literal
821 } else {
822 ScalarStyle::Plain
823 }
824 }
825
826 fn needs_quoting(value: &str) -> bool {
828 if value.is_empty() {
830 return true;
831 }
832
833 if value.eq_ignore_ascii_case("true")
836 || value.eq_ignore_ascii_case("false")
837 || value.eq_ignore_ascii_case("yes")
838 || value.eq_ignore_ascii_case("no")
839 || value.eq_ignore_ascii_case("on")
840 || value.eq_ignore_ascii_case("off")
841 || value.eq_ignore_ascii_case("null")
842 || value == "~"
843 {
844 return true;
845 }
846
847 if value.parse::<f64>().is_ok() || Self::parse_integer(value).is_some() {
849 return true;
850 }
851
852 if value.starts_with(|ch: char| {
854 matches!(ch, '-' | '?' | '[' | ']' | '{' | '}' | ',' | '>' | '<')
855 }) {
856 return true;
857 }
858
859 let mut chars = value.chars().peekable();
862 while let Some(ch) = chars.next() {
863 match ch {
864 '&' | '*' | '!' | '|' | '\'' | '"' | '%' => return true,
865 ':' | '#' if chars.peek().map_or(true, |next| next.is_whitespace()) => {
866 return true;
867 }
868 _ => {}
869 }
870 }
871
872 if value != value.trim() {
874 return true;
875 }
876
877 false
878 }
879
880 pub fn to_yaml_string(&self) -> String {
882 let tag_prefix = match self.scalar_type {
884 #[cfg(feature = "base64")]
885 ScalarType::Binary => "!!binary ",
886 ScalarType::Timestamp => "!!timestamp ",
887 ScalarType::Regex => "!!regex ",
888 _ => "",
889 };
890
891 let content = match self.style {
892 ScalarStyle::Plain => {
893 match self.scalar_type {
895 ScalarType::String => {
896 if Self::needs_quoting(&self.value) {
898 self.to_single_quoted()
899 } else {
900 self.value.clone()
901 }
902 }
903 ScalarType::Integer
905 | ScalarType::Float
906 | ScalarType::Boolean
907 | ScalarType::Null
908 | ScalarType::Timestamp
909 | ScalarType::Regex => self.value.clone(),
910 #[cfg(feature = "base64")]
911 ScalarType::Binary => self.value.clone(),
912 }
913 }
914 ScalarStyle::SingleQuoted => self.to_single_quoted(),
915 ScalarStyle::DoubleQuoted => self.to_double_quoted(),
916 ScalarStyle::Literal => self.to_literal(),
917 ScalarStyle::Folded => self.to_folded(),
918 };
919
920 format!("{}{}", tag_prefix, content)
921 }
922
923 fn to_single_quoted(&self) -> String {
925 let escaped = self.value.replace('\'', "''");
927 format!("'{}'", escaped)
928 }
929
930 fn to_double_quoted(&self) -> String {
932 let mut result = String::from("\"");
933 for ch in self.value.chars() {
934 match ch {
935 '"' => result.push_str("\\\""),
936 '\\' => result.push_str("\\\\"),
937 '\n' => result.push_str("\\n"),
938 '\r' => result.push_str("\\r"),
939 '\t' => result.push_str("\\t"),
940 '\x08' => result.push_str("\\b"),
941 '\x0C' => result.push_str("\\f"),
942 '\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 => {
947 let code_point = c as u32;
949 if code_point <= 0xFF {
950 result.push_str(&format!("\\x{:02X}", code_point));
951 } else if code_point <= 0xFFFF {
952 result.push_str(&format!("\\u{:04X}", code_point));
953 } else {
954 result.push_str(&format!("\\U{:08X}", code_point));
955 }
956 }
957 c => result.push(c),
958 }
959 }
960 result.push('"');
961 result
962 }
963
964 fn to_literal(&self) -> String {
966 self.to_literal_with_indent(2)
967 }
968
969 fn to_folded(&self) -> String {
971 self.to_folded_with_indent(2)
972 }
973
974 pub fn to_literal_with_indent(&self, indent: usize) -> String {
976 let indent_str = " ".repeat(indent);
977
978 let existing_indent = self.detect_content_indentation();
980
981 if existing_indent.is_some() {
983 format!("|\n{}", self.value)
984 } else {
985 let indented = self
987 .value
988 .lines()
989 .map(|line| {
990 if line.trim().is_empty() {
991 String::new()
992 } else {
993 format!("{}{}", indent_str, line)
994 }
995 })
996 .collect::<Vec<_>>()
997 .join("\n");
998 format!("|\n{}", indented)
999 }
1000 }
1001
1002 pub fn to_folded_with_indent(&self, indent: usize) -> String {
1004 let indent_str = " ".repeat(indent);
1005
1006 let existing_indent = self.detect_content_indentation();
1008
1009 if existing_indent.is_some() {
1011 format!(">\n{}", self.value)
1012 } else {
1013 let indented = self
1015 .value
1016 .lines()
1017 .map(|line| {
1018 if line.trim().is_empty() {
1019 String::new()
1020 } else {
1021 format!("{}{}", indent_str, line)
1022 }
1023 })
1024 .collect::<Vec<_>>()
1025 .join("\n");
1026 format!(">\n{}", indented)
1027 }
1028 }
1029
1030 fn detect_content_indentation(&self) -> Option<usize> {
1032 let non_empty_lines: Vec<&str> = self
1033 .value
1034 .lines()
1035 .filter(|line| !line.trim().is_empty())
1036 .collect();
1037
1038 if non_empty_lines.is_empty() {
1039 return None;
1040 }
1041
1042 let mut min_indent = None;
1043 let mut all_have_same_indent = true;
1044
1045 for line in non_empty_lines {
1046 let indent = line.len() - line.trim_start().len();
1047 match min_indent {
1048 None => min_indent = Some(indent),
1049 Some(current_min) => {
1050 if indent != current_min {
1051 all_have_same_indent = false;
1052 }
1053 min_indent = Some(current_min.min(indent));
1054 }
1055 }
1056 }
1057
1058 if all_have_same_indent && min_indent.unwrap_or(0) > 0 {
1060 min_indent
1061 } else {
1062 None
1063 }
1064 }
1065}
1066
1067impl fmt::Display for ScalarValue {
1068 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1069 write!(f, "{}", self.to_yaml_string())
1070 }
1071}
1072
1073impl From<String> for ScalarValue {
1074 fn from(value: String) -> Self {
1075 Self::string(value)
1076 }
1077}
1078
1079impl From<&str> for ScalarValue {
1080 fn from(value: &str) -> Self {
1081 Self::string(value)
1082 }
1083}
1084
1085impl From<i32> for ScalarValue {
1086 fn from(value: i32) -> Self {
1087 Self {
1088 value: value.to_string(),
1089 style: ScalarStyle::Plain,
1090 scalar_type: ScalarType::Integer,
1091 }
1092 }
1093}
1094
1095impl From<i64> for ScalarValue {
1096 fn from(value: i64) -> Self {
1097 Self {
1098 value: value.to_string(),
1099 style: ScalarStyle::Plain,
1100 scalar_type: ScalarType::Integer,
1101 }
1102 }
1103}
1104
1105impl From<f32> for ScalarValue {
1106 fn from(value: f32) -> Self {
1107 Self {
1108 value: value.to_string(),
1109 style: ScalarStyle::Plain,
1110 scalar_type: ScalarType::Float,
1111 }
1112 }
1113}
1114
1115impl From<f64> for ScalarValue {
1116 fn from(value: f64) -> Self {
1117 Self {
1118 value: value.to_string(),
1119 style: ScalarStyle::Plain,
1120 scalar_type: ScalarType::Float,
1121 }
1122 }
1123}
1124
1125impl From<bool> for ScalarValue {
1126 fn from(value: bool) -> Self {
1127 Self {
1128 value: if value { "true" } else { "false" }.to_string(),
1129 style: ScalarStyle::Plain,
1130 scalar_type: ScalarType::Boolean,
1131 }
1132 }
1133}
1134
1135impl From<crate::yaml::Scalar> for ScalarValue {
1136 fn from(scalar: crate::yaml::Scalar) -> Self {
1137 let value = scalar.as_string();
1138 ScalarValue::parse(&value)
1139 }
1140}
1141
1142impl crate::AsYaml for ScalarValue {
1143 fn as_node(&self) -> Option<&crate::yaml::SyntaxNode> {
1144 None
1145 }
1146
1147 fn kind(&self) -> crate::as_yaml::YamlKind {
1148 crate::as_yaml::YamlKind::Scalar
1149 }
1150
1151 fn build_content(
1152 &self,
1153 builder: &mut rowan::GreenNodeBuilder,
1154 _indent: usize,
1155 _flow_context: bool,
1156 ) -> bool {
1157 use crate::lex::SyntaxKind;
1158 let text = self.to_yaml_string();
1163 let quoted = text.starts_with('\'') || text.starts_with('"');
1164 let token_kind = if quoted {
1165 SyntaxKind::STRING
1166 } else {
1167 match self.scalar_type() {
1168 ScalarType::Integer => SyntaxKind::INT,
1169 ScalarType::Float => SyntaxKind::FLOAT,
1170 ScalarType::Boolean => SyntaxKind::BOOL,
1171 ScalarType::Null => SyntaxKind::NULL,
1172 _ => SyntaxKind::STRING,
1173 }
1174 };
1175 builder.start_node(SyntaxKind::SCALAR.into());
1176 builder.token(token_kind.into(), &text);
1177 builder.finish_node();
1178 false
1179 }
1180
1181 fn is_inline(&self) -> bool {
1182 true
1183 }
1184}
1185
1186#[cfg(test)]
1187mod tests {
1188 use super::*;
1189
1190 #[test]
1191 fn test_plain_scalars() {
1192 let scalar = ScalarValue::string("simple");
1193 assert_eq!(scalar.to_yaml_string(), "simple");
1194
1195 let scalar = ScalarValue::string("hello world");
1196 assert_eq!(scalar.to_yaml_string(), "hello world");
1197 }
1198
1199 #[test]
1200 fn test_values_needing_quotes() {
1201 let scalar = ScalarValue::string("true");
1203 assert_eq!(scalar.to_yaml_string(), "'true'");
1204
1205 let scalar = ScalarValue::string("false");
1206 assert_eq!(scalar.to_yaml_string(), "'false'");
1207
1208 let scalar = ScalarValue::string("yes");
1209 assert_eq!(scalar.to_yaml_string(), "'yes'");
1210
1211 let scalar = ScalarValue::string("no");
1212 assert_eq!(scalar.to_yaml_string(), "'no'");
1213
1214 let scalar = ScalarValue::string("null");
1216 assert_eq!(scalar.to_yaml_string(), "'null'");
1217
1218 let scalar = ScalarValue::string("~");
1219 assert_eq!(scalar.to_yaml_string(), "'~'");
1220
1221 let scalar = ScalarValue::string("123");
1223 assert_eq!(scalar.to_yaml_string(), "'123'");
1224
1225 let scalar = ScalarValue::string("3.14");
1226 assert_eq!(scalar.to_yaml_string(), "'3.14'");
1227
1228 let scalar = ScalarValue::string("value: something");
1230 assert_eq!(scalar.to_yaml_string(), "'value: something'");
1231
1232 let scalar = ScalarValue::string("# comment");
1233 assert_eq!(scalar.to_yaml_string(), "'# comment'");
1234
1235 let scalar = ScalarValue::string(" spaces ");
1237 assert_eq!(scalar.to_yaml_string(), "' spaces '");
1238 }
1239
1240 #[test]
1241 fn test_single_quoted() {
1242 let scalar = ScalarValue::single_quoted("value with 'quotes'");
1243 assert_eq!(scalar.to_yaml_string(), "'value with ''quotes'''");
1244 }
1245
1246 #[test]
1247 fn test_double_quoted() {
1248 let scalar = ScalarValue::double_quoted("value with \"quotes\" and \\backslash");
1249 assert_eq!(
1250 scalar.to_yaml_string(),
1251 "\"value with \\\"quotes\\\" and \\\\backslash\""
1252 );
1253
1254 let scalar = ScalarValue::double_quoted("line1\nline2\ttab");
1255 assert_eq!(scalar.to_yaml_string(), "\"line1\\nline2\\ttab\"");
1256 }
1257
1258 #[test]
1259 fn test_multiline() {
1260 let scalar = ScalarValue::string("line1\nline2\nline3");
1261 assert_eq!(scalar.style(), ScalarStyle::Literal);
1263 }
1264
1265 #[test]
1266 fn test_from_types() {
1267 let scalar = ScalarValue::from(42);
1268 assert_eq!(scalar.to_yaml_string(), "42");
1269
1270 let scalar = ScalarValue::from(1.234);
1271 assert_eq!(scalar.to_yaml_string(), "1.234");
1272
1273 let scalar = ScalarValue::from(true);
1274 assert_eq!(scalar.to_yaml_string(), "true");
1275
1276 let scalar = ScalarValue::from(false);
1277 assert_eq!(scalar.to_yaml_string(), "false");
1278 }
1279
1280 #[test]
1281 fn test_empty_string() {
1282 let scalar = ScalarValue::string("");
1283 assert_eq!(scalar.to_yaml_string(), "''");
1284 }
1285
1286 #[test]
1287 fn test_special_start_chars() {
1288 let scalar = ScalarValue::string("-item");
1289 assert_eq!(scalar.to_yaml_string(), "'-item'");
1290
1291 let scalar = ScalarValue::string("?key");
1292 assert_eq!(scalar.to_yaml_string(), "'?key'");
1293
1294 let scalar = ScalarValue::string("[array]");
1295 assert_eq!(scalar.to_yaml_string(), "'[array]'");
1296 }
1297
1298 #[test]
1299 fn test_null_scalar() {
1300 let scalar = ScalarValue::null();
1301 assert_eq!(scalar.to_yaml_string(), "null");
1302 assert_eq!(scalar.scalar_type, ScalarType::Null);
1303 }
1304
1305 #[test]
1306 fn test_escape_sequences_basic() {
1307 assert_eq!(
1309 ScalarValue::parse_escape_sequences("hello\\nworld"),
1310 "hello\nworld"
1311 );
1312 assert_eq!(
1313 ScalarValue::parse_escape_sequences("tab\\there"),
1314 "tab\there"
1315 );
1316 assert_eq!(
1317 ScalarValue::parse_escape_sequences("quote\\\"test"),
1318 "quote\"test"
1319 );
1320 assert_eq!(
1321 ScalarValue::parse_escape_sequences("back\\\\slash"),
1322 "back\\slash"
1323 );
1324 assert_eq!(
1325 ScalarValue::parse_escape_sequences("return\\rtest"),
1326 "return\rtest"
1327 );
1328 }
1329
1330 #[test]
1331 fn test_escape_sequences_control_chars() {
1332 assert_eq!(ScalarValue::parse_escape_sequences("bell\\a"), "bell\x07");
1334 assert_eq!(
1335 ScalarValue::parse_escape_sequences("backspace\\b"),
1336 "backspace\x08"
1337 );
1338 assert_eq!(
1339 ScalarValue::parse_escape_sequences("formfeed\\f"),
1340 "formfeed\x0C"
1341 );
1342 assert_eq!(
1343 ScalarValue::parse_escape_sequences("escape\\e"),
1344 "escape\x1B"
1345 );
1346 assert_eq!(ScalarValue::parse_escape_sequences("vtab\\v"), "vtab\x0B");
1347 assert_eq!(ScalarValue::parse_escape_sequences("null\\0"), "null\0");
1348 assert_eq!(ScalarValue::parse_escape_sequences("slash\\/"), "slash/");
1349 }
1350
1351 #[test]
1352 fn test_escape_sequences_unicode_x() {
1353 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"); }
1363
1364 #[test]
1365 fn test_escape_sequences_unicode_u() {
1366 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"); }
1376
1377 #[test]
1378 fn test_escape_sequences_unicode_capital_u() {
1379 assert_eq!(ScalarValue::parse_escape_sequences("\\U00000041"), "A"); assert_eq!(ScalarValue::parse_escape_sequences("\\U0001F603"), "😃"); assert_eq!(ScalarValue::parse_escape_sequences("\\U0001F4A9"), "💩"); assert_eq!(
1386 ScalarValue::parse_escape_sequences("\\UGHIJKLMN"),
1387 "\\UGHIJKLMN"
1388 ); assert_eq!(
1390 ScalarValue::parse_escape_sequences("\\U0000004"),
1391 "\\U0000004"
1392 ); assert_eq!(
1394 ScalarValue::parse_escape_sequences("\\UFFFFFFFF"),
1395 "\\UFFFFFFFF"
1396 ); }
1398
1399 #[test]
1400 fn test_escape_sequences_line_folding() {
1401 assert_eq!(
1403 ScalarValue::parse_escape_sequences("line\\ \nfolding"),
1404 "linefolding"
1405 );
1406 assert_eq!(
1407 ScalarValue::parse_escape_sequences("escaped\\nline\\nbreak"),
1408 "escaped\nline\nbreak"
1409 );
1410 assert_eq!(
1411 ScalarValue::parse_escape_sequences("remove\\\nline\\nbreak"),
1412 "removeline\nbreak"
1413 );
1414 }
1415
1416 #[test]
1417 fn test_escape_sequences_mixed() {
1418 let input = "Hello\\nWorld\\u0021\\x20\\U0001F44D";
1420 let expected = "Hello\nWorld! 👍";
1421 assert_eq!(ScalarValue::parse_escape_sequences(input), expected);
1422
1423 let input = "Quote\\\"back\\\\slash\\ttab";
1425 let expected = "Quote\"back\\slash\ttab";
1426 assert_eq!(ScalarValue::parse_escape_sequences(input), expected);
1427 }
1428
1429 #[test]
1430 fn test_escape_sequences_unknown() {
1431 assert_eq!(ScalarValue::parse_escape_sequences("\\q"), "\\q");
1433 assert_eq!(ScalarValue::parse_escape_sequences("\\z"), "\\z");
1434 assert_eq!(ScalarValue::parse_escape_sequences("\\1"), "\\1");
1435 }
1436
1437 #[test]
1438 fn test_indentation_preservation() {
1439 let content_with_indent = " Line 1\n Line 2 more indented\n Line 3";
1441 let scalar = ScalarValue::literal(content_with_indent);
1442
1443 let yaml_output = scalar.to_literal_with_indent(2);
1445 assert_eq!(
1446 yaml_output,
1447 "|\n Line 1\n Line 2 more indented\n Line 3"
1448 );
1449 }
1450
1451 #[test]
1452 fn test_indentation_detection() {
1453 let consistent_content = " Line 1\n Line 2\n Line 3";
1455 let scalar1 = ScalarValue::literal(consistent_content);
1456 assert_eq!(scalar1.detect_content_indentation(), Some(2));
1457
1458 let no_indent_content = "Line 1\nLine 2\nLine 3";
1460 let scalar2 = ScalarValue::literal(no_indent_content);
1461 assert_eq!(scalar2.detect_content_indentation(), None);
1462
1463 let inconsistent_content = " Line 1\n Line 2\n Line 3";
1465 let scalar3 = ScalarValue::literal(inconsistent_content);
1466 assert_eq!(scalar3.detect_content_indentation(), None);
1467
1468 let empty_content = "";
1470 let scalar4 = ScalarValue::literal(empty_content);
1471 assert_eq!(scalar4.detect_content_indentation(), None);
1472
1473 let whitespace_content = " Line 1\n\n Line 3";
1475 let scalar5 = ScalarValue::literal(whitespace_content);
1476 assert_eq!(scalar5.detect_content_indentation(), Some(2));
1477 }
1478
1479 #[test]
1480 fn test_literal_with_custom_indent() {
1481 let content = "Line 1\nLine 2\nLine 3";
1483 let scalar = ScalarValue::literal(content);
1484
1485 let yaml_4_spaces = scalar.to_literal_with_indent(4);
1486 assert_eq!(yaml_4_spaces, "|\n Line 1\n Line 2\n Line 3");
1487
1488 let yaml_1_space = scalar.to_literal_with_indent(1);
1489 assert_eq!(yaml_1_space, "|\n Line 1\n Line 2\n Line 3");
1490 }
1491
1492 #[test]
1493 fn test_folded_with_custom_indent() {
1494 let content = "Line 1\nLine 2\nLine 3";
1496 let scalar = ScalarValue::folded(content);
1497
1498 let yaml_3_spaces = scalar.to_folded_with_indent(3);
1499 assert_eq!(yaml_3_spaces, ">\n Line 1\n Line 2\n Line 3");
1500 }
1501
1502 #[test]
1503 fn test_mixed_empty_lines_preservation() {
1504 let content_with_empty_lines = "Line 1\n\nLine 3\n\n\nLine 6";
1506 let scalar = ScalarValue::literal(content_with_empty_lines);
1507
1508 let yaml_output = scalar.to_literal_with_indent(2);
1509 assert_eq!(yaml_output, "|\n Line 1\n\n Line 3\n\n\n Line 6");
1510
1511 let lines: Vec<&str> = yaml_output.lines().collect();
1514 let empty_line_count = lines.iter().filter(|line| line.is_empty()).count();
1515 assert_eq!(empty_line_count, 3);
1516 }
1517
1518 #[test]
1519 fn test_escape_sequences_edge_cases() {
1520 assert_eq!(ScalarValue::parse_escape_sequences(""), "");
1522 assert_eq!(ScalarValue::parse_escape_sequences("\\"), "\\");
1523 assert_eq!(
1524 ScalarValue::parse_escape_sequences("no escapes"),
1525 "no escapes"
1526 );
1527 assert_eq!(ScalarValue::parse_escape_sequences("\\\\\\\\"), "\\\\");
1528 }
1529
1530 #[test]
1531 fn test_double_quoted_with_escapes() {
1532 let original = "Hello\nWorld\t😃";
1534 let scalar = ScalarValue::double_quoted(original);
1535 let yaml_string = scalar.to_yaml_string();
1536
1537 assert_eq!(yaml_string, "\"Hello\\nWorld\\t\\U0001F603\"");
1539
1540 let parsed = ScalarValue::parse_escape_sequences(&yaml_string[1..yaml_string.len() - 1]);
1542 assert_eq!(parsed, original);
1543 }
1544
1545 #[test]
1546 fn test_unicode_output_formatting() {
1547 let scalar = ScalarValue::double_quoted("Hello 世界 🌍");
1549 let yaml_string = scalar.to_yaml_string();
1550
1551 assert_eq!(yaml_string, "\"Hello \\u4E16\\u754C \\U0001F30D\"");
1553
1554 assert_eq!(scalar.value(), "Hello 世界 🌍");
1556 }
1557
1558 #[test]
1559 #[cfg(feature = "base64")]
1560 fn test_binary_data_encoding() {
1561 let data = b"Hello, World!";
1563 let scalar = ScalarValue::binary(data);
1564
1565 assert!(scalar.is_binary());
1566 assert_eq!(scalar.scalar_type(), ScalarType::Binary);
1567
1568 let yaml_output = scalar.to_yaml_string();
1570 assert!(yaml_output.starts_with("!!binary "));
1571
1572 if let Some(decoded_result) = scalar.as_binary() {
1574 let decoded = decoded_result.expect("Should decode successfully");
1575 assert_eq!(decoded, data);
1576 } else {
1577 panic!("Should be able to extract binary data");
1578 }
1579 }
1580
1581 #[test]
1582 #[cfg(feature = "base64")]
1583 fn test_base64_encoding_decoding() {
1584 let test_cases = [
1586 b"".as_slice(),
1587 b"A",
1588 b"AB",
1589 b"ABC",
1590 b"ABCD",
1591 b"Hello, World!",
1592 &[0, 1, 2, 3, 255, 254, 253],
1593 ];
1594
1595 for data in test_cases {
1596 let encoded = base64_encode(data);
1597 let decoded = base64_decode(&encoded).expect("Should decode successfully");
1598 assert_eq!(decoded, data, "Failed for data: {:?}", data);
1599 }
1600 }
1601
1602 #[test]
1603 fn test_timestamp_creation_and_validation() {
1604 let valid_timestamps = [
1606 "2023-12-25",
1607 "2023-12-25T10:30:45",
1608 "2023-12-25 10:30:45",
1609 "2023-12-25T10:30:45Z",
1610 "2001-12-14 21:59:43.10 -5", "2001-12-15T02:59:43.1Z", "2001-12-14t21:59:43.10-05:00", ];
1614
1615 for ts in valid_timestamps {
1616 let scalar = ScalarValue::timestamp(ts);
1617 assert!(scalar.is_timestamp());
1618 assert_eq!(scalar.scalar_type(), ScalarType::Timestamp);
1619 assert_eq!(scalar.value(), ts);
1620
1621 let yaml_output = scalar.to_yaml_string();
1622 assert_eq!(yaml_output, format!("!!timestamp {}", ts));
1623
1624 let auto_scalar = ScalarValue::parse(ts);
1626 assert_eq!(
1627 auto_scalar.scalar_type(),
1628 ScalarType::Timestamp,
1629 "Failed to auto-detect '{}' as timestamp",
1630 ts
1631 );
1632 }
1633
1634 let invalid_timestamps = [
1636 "not-a-date",
1637 "2023-13-01", "2023-12-32", "12:34:56", "2023/12/25", ];
1642
1643 for ts in invalid_timestamps {
1644 let auto_scalar = ScalarValue::parse(ts);
1645 assert_ne!(
1646 auto_scalar.scalar_type(),
1647 ScalarType::Timestamp,
1648 "'{}' should not be detected as timestamp",
1649 ts
1650 );
1651 }
1652 }
1653
1654 #[test]
1655 fn test_regex_creation() {
1656 let pattern = r"^\d{3}-\d{2}-\d{4}$";
1657 let scalar = ScalarValue::regex(pattern);
1658
1659 assert!(scalar.is_regex());
1660 assert_eq!(scalar.scalar_type(), ScalarType::Regex);
1661 assert_eq!(scalar.value(), pattern);
1662
1663 let yaml_output = scalar.to_yaml_string();
1664 assert_eq!(yaml_output, format!("!!regex {}", pattern));
1665 }
1666
1667 #[test]
1668 fn test_regex_edge_cases() {
1669 let empty_regex = ScalarValue::regex("");
1671 assert!(empty_regex.is_regex());
1672 assert_eq!(empty_regex.value(), "");
1673 assert_eq!(empty_regex.to_yaml_string(), "!!regex ");
1674
1675 let special_chars = ScalarValue::regex(r"[.*+?^${}()|[\]\\]");
1677 assert!(special_chars.is_regex());
1678 assert_eq!(special_chars.value(), r"[.*+?^${}()|[\]\\]");
1679
1680 let unicode_regex = ScalarValue::regex(r"\p{L}+");
1682 assert!(unicode_regex.is_regex());
1683 assert_eq!(unicode_regex.value(), r"\p{L}+");
1684
1685 let long_pattern = "a".repeat(1000);
1687 let long_regex = ScalarValue::regex(&long_pattern);
1688 assert!(long_regex.is_regex());
1689 assert_eq!(long_regex.value(), long_pattern);
1690
1691 let quoted_regex = ScalarValue::regex(r#"'quoted' and "double quoted" with \\ backslash"#);
1693 assert!(quoted_regex.is_regex());
1694 assert_eq!(
1695 quoted_regex.value(),
1696 r#"'quoted' and "double quoted" with \\ backslash"#
1697 );
1698 }
1699
1700 #[test]
1701 fn test_regex_type_coercion() {
1702 let regex_scalar = ScalarValue::regex(r"\d+");
1703
1704 let string_scalar = regex_scalar.coerce_to_type(ScalarType::String).unwrap();
1706 assert_eq!(string_scalar.scalar_type(), ScalarType::String);
1707 assert_eq!(string_scalar.value(), r"\d+");
1708 assert!(!string_scalar.is_regex());
1709
1710 let str_scalar = ScalarValue::string("test.*");
1712 let regex_from_string = str_scalar.coerce_to_type(ScalarType::Regex).unwrap();
1713 assert_eq!(regex_from_string.scalar_type(), ScalarType::Regex);
1714 assert_eq!(regex_from_string.value(), "test.*");
1715 assert!(regex_from_string.is_regex());
1716
1717 assert!(regex_scalar.coerce_to_type(ScalarType::Integer).is_none());
1719 assert!(regex_scalar.coerce_to_type(ScalarType::Float).is_none());
1720 assert!(regex_scalar.coerce_to_type(ScalarType::Boolean).is_none());
1721 }
1722
1723 #[test]
1724 #[cfg(feature = "regex")]
1725 fn test_regex_compilation() {
1726 let regex_scalar = ScalarValue::regex(r"\d{3}-\d{4}");
1728 let compiled = regex_scalar.as_regex().unwrap();
1729 assert!(compiled.is_match("555-1234"));
1730 assert!(!compiled.is_match("not-a-phone"));
1731
1732 let string_scalar = ScalarValue::string("not a regex");
1734 assert!(string_scalar.as_regex().is_none());
1735
1736 let pattern_scalar = ScalarValue::string(r"^\w+@\w+\.\w+$");
1738 let email_regex = pattern_scalar.try_as_regex().unwrap();
1739 assert!(email_regex.is_match("test@example.com"));
1740 assert!(!email_regex.is_match("not-an-email"));
1741
1742 let invalid_scalar = ScalarValue::regex(r"[invalid(");
1744 assert!(invalid_scalar.as_regex().is_none());
1745
1746 let invalid_pattern = ScalarValue::string(r"[invalid(");
1748 assert!(invalid_pattern.try_as_regex().is_err());
1749 }
1750
1751 #[test]
1752 #[cfg(feature = "regex")]
1753 fn test_regex_extraction_use_cases() {
1754 let validation_rules = [
1756 ScalarValue::regex(r"^\d{5}$"), ScalarValue::regex(r"^[A-Z]{2}$"), ScalarValue::regex(r"^\(\d{3}\) \d{3}-\d{4}$"), ];
1760
1761 let test_values = ["12345", "CA", "(555) 123-4567"];
1762
1763 for (rule, value) in validation_rules.iter().zip(test_values.iter()) {
1764 let regex = rule.as_regex().unwrap();
1765 assert!(regex.is_match(value), "Pattern should match {}", value);
1766 }
1767
1768 let email_regex = ScalarValue::regex(r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$");
1770 let compiled = email_regex.as_regex().unwrap();
1771 assert!(compiled.is_match("user@example.com"));
1772 assert!(compiled.is_match("test.user+tag@sub.domain.org"));
1773 assert!(!compiled.is_match("invalid.email"));
1774
1775 let version_regex = ScalarValue::regex(r"^v(\d+)\.(\d+)\.(\d+)$");
1777 let compiled = version_regex.as_regex().unwrap();
1778 if let Some(captures) = compiled.captures("v1.2.3") {
1779 assert_eq!(captures.get(1).unwrap().as_str(), "1");
1780 assert_eq!(captures.get(2).unwrap().as_str(), "2");
1781 assert_eq!(captures.get(3).unwrap().as_str(), "3");
1782 } else {
1783 panic!("Should have matched version string");
1784 }
1785 }
1786
1787 #[test]
1788 fn test_type_coercion() {
1789 let str_scalar = ScalarValue::string("42");
1791 let int_scalar = str_scalar.coerce_to_type(ScalarType::Integer).unwrap();
1792 assert_eq!(int_scalar.scalar_type(), ScalarType::Integer);
1793 assert_eq!(int_scalar.value(), "42");
1794
1795 let bool_scalar = ScalarValue::string("true")
1797 .coerce_to_type(ScalarType::Boolean)
1798 .unwrap();
1799 assert_eq!(bool_scalar.scalar_type(), ScalarType::Boolean);
1800 assert_eq!(bool_scalar.value(), "true");
1801
1802 let yes_scalar = ScalarValue::string("yes")
1804 .coerce_to_type(ScalarType::Boolean)
1805 .unwrap();
1806 assert_eq!(yes_scalar.value(), "true");
1807
1808 let no_scalar = ScalarValue::string("no")
1809 .coerce_to_type(ScalarType::Boolean)
1810 .unwrap();
1811 assert_eq!(no_scalar.value(), "false");
1812
1813 let str_scalar = ScalarValue::string("not_a_number");
1815 assert!(str_scalar.coerce_to_type(ScalarType::Integer).is_none());
1816 }
1817
1818 #[test]
1819 fn test_auto_type_detection() {
1820 assert_eq!(ScalarValue::auto_detect_type("42"), ScalarType::Integer);
1822 assert_eq!(ScalarValue::auto_detect_type("3.14"), ScalarType::Float);
1823 assert_eq!(ScalarValue::auto_detect_type("true"), ScalarType::Boolean);
1824 assert_eq!(ScalarValue::auto_detect_type("false"), ScalarType::Boolean);
1825 assert_eq!(ScalarValue::auto_detect_type("yes"), ScalarType::Boolean);
1826 assert_eq!(ScalarValue::auto_detect_type("null"), ScalarType::Null);
1827 assert_eq!(ScalarValue::auto_detect_type("~"), ScalarType::Null);
1828 assert_eq!(ScalarValue::auto_detect_type(""), ScalarType::Null);
1829 assert_eq!(
1830 ScalarValue::auto_detect_type("2023-12-25"),
1831 ScalarType::Timestamp
1832 );
1833 assert_eq!(
1834 ScalarValue::auto_detect_type("2023-12-25T10:30:45"),
1835 ScalarType::Timestamp
1836 );
1837 #[cfg(feature = "base64")]
1838 assert_eq!(
1839 ScalarValue::auto_detect_type("SGVsbG8gV29ybGQ="),
1840 ScalarType::Binary
1841 );
1842 #[cfg(not(feature = "base64"))]
1843 assert_eq!(
1844 ScalarValue::auto_detect_type("SGVsbG8gV29ybGQ="),
1845 ScalarType::String
1846 );
1847 assert_eq!(
1848 ScalarValue::auto_detect_type("hello world"),
1849 ScalarType::String
1850 );
1851 }
1852
1853 #[test]
1854 fn test_from_yaml_scalar_creation() {
1855 let int_scalar = ScalarValue::parse("123");
1856 assert_eq!(int_scalar.scalar_type(), ScalarType::Integer);
1857
1858 let bool_scalar = ScalarValue::parse("true");
1859 assert_eq!(bool_scalar.scalar_type(), ScalarType::Boolean);
1860
1861 let timestamp_scalar = ScalarValue::parse("2023-12-25");
1862 assert_eq!(timestamp_scalar.scalar_type(), ScalarType::Timestamp);
1863
1864 let string_scalar = ScalarValue::parse("hello world");
1865 assert_eq!(string_scalar.scalar_type(), ScalarType::String);
1866 }
1867
1868 #[test]
1869 fn test_timestamp_pattern_matching() {
1870 assert!(ScalarValue::matches_iso8601_pattern("2023-12-25"));
1872 assert!(ScalarValue::matches_iso8601_pattern("2023-12-25T10:30:45"));
1873 assert!(ScalarValue::matches_iso8601_pattern("2023-12-25t10:30:45")); assert!(ScalarValue::matches_iso8601_pattern("2023-12-25 10:30:45"));
1875 assert!(ScalarValue::matches_iso8601_pattern("2023-01-01T00:00:00"));
1876 assert!(ScalarValue::matches_iso8601_pattern(
1877 "2001-12-14t21:59:43.10-05:00"
1878 )); 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"));
1885 assert!(!ScalarValue::matches_iso8601_pattern("2023"));
1886 }
1887
1888 #[test]
1889 #[cfg(feature = "base64")]
1890 fn test_base64_detection() {
1891 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===")); }
1904
1905 #[test]
1906 #[cfg(feature = "base64")]
1907 fn test_binary_yaml_output_with_tags() {
1908 let data = b"Binary data here";
1909 let scalar = ScalarValue::binary(data);
1910 let yaml_output = scalar.to_yaml_string();
1911
1912 assert!(yaml_output.starts_with("!!binary "));
1913
1914 let base64_part = &yaml_output[9..]; let decoded = base64_decode(base64_part).expect("Should decode");
1917 assert_eq!(decoded, data);
1918 }
1919
1920 #[test]
1921 #[cfg(feature = "base64")]
1922 fn test_special_data_types_with_different_styles() {
1923 let data = b"test";
1925 let binary_scalar = ScalarValue::binary(data);
1926
1927 let mut styled_binary = binary_scalar;
1929 styled_binary.style = ScalarStyle::DoubleQuoted;
1930
1931 assert_eq!(styled_binary.to_yaml_string(), "!!binary \"dGVzdA==\"");
1933 }
1934
1935 #[test]
1936 fn test_type_checking_methods() {
1937 #[cfg(feature = "base64")]
1938 let binary_scalar = ScalarValue::binary(b"test");
1939 let timestamp_scalar = ScalarValue::timestamp("2023-12-25");
1940 let regex_scalar = ScalarValue::regex(r"\d+");
1941 let string_scalar = ScalarValue::string("hello");
1942
1943 #[cfg(feature = "base64")]
1945 assert!(binary_scalar.is_binary());
1946 #[cfg(feature = "base64")]
1947 assert!(!binary_scalar.is_timestamp());
1948 #[cfg(feature = "base64")]
1949 assert!(!binary_scalar.is_regex());
1950
1951 #[cfg(feature = "base64")]
1952 assert!(!timestamp_scalar.is_binary());
1953 assert!(timestamp_scalar.is_timestamp());
1954 assert!(!timestamp_scalar.is_regex());
1955
1956 #[cfg(feature = "base64")]
1957 assert!(!regex_scalar.is_binary());
1958 assert!(!regex_scalar.is_timestamp());
1959 assert!(regex_scalar.is_regex());
1960
1961 #[cfg(feature = "base64")]
1962 assert!(!string_scalar.is_binary());
1963 assert!(!string_scalar.is_timestamp());
1964 assert!(!string_scalar.is_regex());
1965 }
1966
1967 #[test]
1968 fn test_binary_number_parsing() {
1969 assert_eq!(ScalarValue::parse_integer("0b1010"), Some(10));
1971 assert_eq!(ScalarValue::parse_integer("0b11111111"), Some(255));
1972 assert_eq!(ScalarValue::parse_integer("0B101"), Some(5)); assert_eq!(ScalarValue::parse_integer("-0b1010"), Some(-10));
1974 assert_eq!(ScalarValue::parse_integer("+0b101"), Some(5));
1975
1976 assert_eq!(ScalarValue::auto_detect_type("0b1010"), ScalarType::Integer);
1978 assert_eq!(
1979 ScalarValue::auto_detect_type("0B11111111"),
1980 ScalarType::Integer
1981 );
1982
1983 assert_eq!(ScalarValue::parse_integer("0b1012"), None); assert_eq!(ScalarValue::parse_integer("0b"), None); }
1987
1988 #[test]
1989 fn test_modern_octal_number_parsing() {
1990 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));
1995 assert_eq!(ScalarValue::parse_integer("+0o644"), Some(420));
1996
1997 assert_eq!(ScalarValue::auto_detect_type("0o755"), ScalarType::Integer);
1999 assert_eq!(ScalarValue::auto_detect_type("0O644"), ScalarType::Integer);
2000
2001 assert_eq!(ScalarValue::parse_integer("0o789"), None); assert_eq!(ScalarValue::parse_integer("0o"), None); }
2005
2006 #[test]
2007 fn test_legacy_octal_number_parsing() {
2008 assert_eq!(ScalarValue::parse_integer("0755"), Some(493));
2010 assert_eq!(ScalarValue::parse_integer("0644"), Some(420));
2011 assert_eq!(ScalarValue::parse_integer("0777"), Some(511));
2012
2013 assert_eq!(ScalarValue::auto_detect_type("0755"), ScalarType::Integer);
2015 assert_eq!(ScalarValue::auto_detect_type("0644"), ScalarType::Integer);
2016
2017 assert_eq!(ScalarValue::parse_integer("0"), Some(0)); assert_eq!(ScalarValue::parse_integer("00"), Some(0)); assert_eq!(ScalarValue::parse_integer("0789"), None);
2023 assert_eq!(ScalarValue::parse_integer("0128"), None);
2024 }
2025
2026 #[test]
2027 fn test_hexadecimal_number_parsing() {
2028 assert_eq!(ScalarValue::parse_integer("0xFF"), Some(255));
2030 assert_eq!(ScalarValue::parse_integer("0x1A"), Some(26));
2031 assert_eq!(ScalarValue::parse_integer("0XFF"), Some(255)); assert_eq!(ScalarValue::parse_integer("-0xFF"), Some(-255));
2033 assert_eq!(ScalarValue::parse_integer("+0x1A"), Some(26));
2034
2035 assert_eq!(ScalarValue::auto_detect_type("0xFF"), ScalarType::Integer);
2037 assert_eq!(ScalarValue::auto_detect_type("0X1A"), ScalarType::Integer);
2038 }
2039
2040 #[test]
2041 fn test_decimal_number_parsing() {
2042 assert_eq!(ScalarValue::parse_integer("42"), Some(42));
2044 assert_eq!(ScalarValue::parse_integer("123"), Some(123));
2045 assert_eq!(ScalarValue::parse_integer("-42"), Some(-42));
2046 assert_eq!(ScalarValue::parse_integer("+123"), Some(123));
2047
2048 assert_eq!(ScalarValue::auto_detect_type("42"), ScalarType::Integer);
2050 assert_eq!(ScalarValue::auto_detect_type("-123"), ScalarType::Integer);
2051 }
2052
2053 #[test]
2054 fn test_number_format_yaml_output() {
2055 let binary_scalar = ScalarValue::parse("0b1010");
2057 assert_eq!(binary_scalar.scalar_type(), ScalarType::Integer);
2058 assert_eq!(binary_scalar.value(), "0b1010");
2059
2060 let octal_scalar = ScalarValue::parse("0o755");
2061 assert_eq!(octal_scalar.scalar_type(), ScalarType::Integer);
2062 assert_eq!(octal_scalar.value(), "0o755");
2063
2064 let hex_scalar = ScalarValue::parse("0xFF");
2065 assert_eq!(hex_scalar.scalar_type(), ScalarType::Integer);
2066 assert_eq!(hex_scalar.value(), "0xFF");
2067
2068 let legacy_octal_scalar = ScalarValue::parse("0755");
2069 assert_eq!(legacy_octal_scalar.scalar_type(), ScalarType::Integer);
2070 assert_eq!(legacy_octal_scalar.value(), "0755");
2071 }
2072
2073 #[test]
2074 fn test_unix_timestamp_bounds() {
2075 assert!(!ScalarValue::is_valid_timestamp_static("0"));
2078 assert!(ScalarValue::is_valid_timestamp_static("1"));
2079 assert!(ScalarValue::is_valid_timestamp_static("4102444799"));
2080 assert!(!ScalarValue::is_valid_timestamp_static("4102444800"));
2081
2082 let s = ScalarValue::string("x");
2084 assert!(s.is_valid_timestamp("1"));
2085 assert!(!s.is_valid_timestamp("0"));
2086 }
2087
2088 #[test]
2089 fn test_iso8601_time_component_bounds() {
2090 assert!(ScalarValue::matches_iso8601_pattern("2023-01-01T23:59:59"));
2092 assert!(!ScalarValue::matches_iso8601_pattern("2023-01-01T24:00:00"));
2093 assert!(!ScalarValue::matches_iso8601_pattern("2023-01-01T10:60:00"));
2094 assert!(!ScalarValue::matches_iso8601_pattern("2023-01-01T10:30:60"));
2095
2096 assert!(ScalarValue::matches_iso8601_pattern("2023-01-31"));
2098 assert!(!ScalarValue::matches_iso8601_pattern("2023-01-00"));
2099 assert!(!ScalarValue::matches_iso8601_pattern("2023-01-32"));
2100
2101 assert!(!ScalarValue::matches_iso8601_pattern("2023-01-01X10:30:45"));
2103 assert!(!ScalarValue::matches_iso8601_pattern("2023-01-01T10:30:xx"));
2105
2106 assert!(!ScalarValue::matches_iso8601_pattern("2023X12-25"));
2110 assert!(!ScalarValue::matches_iso8601_pattern("2023-12X25"));
2111 }
2112
2113 #[test]
2114 #[cfg(feature = "base64")]
2115 fn test_base64_padding_bounds() {
2116 assert!(ScalarValue::looks_like_base64("QQ==")); assert!(ScalarValue::looks_like_base64("SGVsbG8=")); assert!(!ScalarValue::looks_like_base64("QQ=Q"));
2122 assert!(!ScalarValue::looks_like_base64("QQQ"));
2124 }
2125
2126 #[test]
2127 fn test_coerce_to_null() {
2128 let s = ScalarValue::string("null");
2130 assert_eq!(
2131 s.coerce_to_type(ScalarType::Null).map(|v| v.scalar_type()),
2132 Some(ScalarType::Null)
2133 );
2134 assert!(ScalarValue::string("~")
2135 .coerce_to_type(ScalarType::Null)
2136 .is_some());
2137 assert!(ScalarValue::string("")
2138 .coerce_to_type(ScalarType::Null)
2139 .is_some());
2140 assert!(ScalarValue::string("notnull")
2141 .coerce_to_type(ScalarType::Null)
2142 .is_none());
2143 }
2144
2145 #[test]
2146 fn test_parse_escape_single_quote() {
2147 assert_eq!(ScalarValue::parse_escape_sequences("a\\'b"), "a'b");
2149 }
2150
2151 #[test]
2152 fn test_parse_preserves_string_style() {
2153 let plain = ScalarValue::parse("hello");
2155 assert_eq!(plain.scalar_type(), ScalarType::String);
2156 assert_eq!(plain.style, ScalarStyle::Plain);
2157
2158 let multiline = ScalarValue::parse("multi line\nvalue");
2161 assert_eq!(multiline.scalar_type(), ScalarType::String);
2162 assert_eq!(multiline.style, ScalarStyle::Literal);
2163 }
2164
2165 #[test]
2166 fn test_from_scalar_type_and_style() {
2167 use crate::yaml::Document;
2168 use rowan::ast::AstNode;
2169 use std::str::FromStr;
2170
2171 let doc = Document::from_str("k: null\n").unwrap();
2173 let mapping = doc.as_mapping().unwrap();
2174 let null_node = mapping.get("k").unwrap();
2175 let null_scalar = crate::yaml::Scalar::cast(null_node.syntax().clone()).unwrap();
2176 let sv = ScalarValue::from_scalar(&null_scalar);
2177 assert_eq!(sv.scalar_type(), ScalarType::Null);
2178 assert_eq!(sv.style, ScalarStyle::Plain);
2179
2180 let doc = Document::from_str("k: \"hi\"\n").unwrap();
2182 let node = doc.as_mapping().unwrap().get("k").unwrap();
2183 let scalar = crate::yaml::Scalar::cast(node.syntax().clone()).unwrap();
2184 let sv = ScalarValue::from_scalar(&scalar);
2185 assert_eq!(sv.scalar_type(), ScalarType::String);
2186 assert_eq!(sv.style, ScalarStyle::DoubleQuoted);
2187
2188 let doc = Document::from_str("k: 'hi'\n").unwrap();
2190 let node = doc.as_mapping().unwrap().get("k").unwrap();
2191 let scalar = crate::yaml::Scalar::cast(node.syntax().clone()).unwrap();
2192 let sv = ScalarValue::from_scalar(&scalar);
2193 assert_eq!(sv.style, ScalarStyle::SingleQuoted);
2194 }
2195
2196 #[test]
2197 fn test_needs_quoting_special_chars() {
2198 let cases = [
2203 ("&anchor", "'&anchor'"),
2204 ("*alias", "'*alias'"),
2205 ("!tag", "'!tag'"),
2206 ("a|b", "'a|b'"),
2207 ("it's", "\"it's\""),
2208 ("a\"b", "'a\"b'"),
2209 ("50%", "'50%'"),
2210 ("on", "'on'"),
2211 ("off", "'off'"),
2212 ("On", "'On'"),
2213 ("OFF", "'OFF'"),
2214 ("yes", "'yes'"),
2215 ("no", "'no'"),
2216 ("true", "'true'"),
2217 ("false", "'false'"),
2218 ("null", "'null'"),
2219 ];
2220 for (input, expected) in cases {
2221 assert_eq!(ScalarValue::string(input).to_yaml_string(), expected);
2222 }
2223 assert_eq!(ScalarValue::string("plain").to_yaml_string(), "plain");
2225 }
2226
2227 #[test]
2228 fn test_literal_and_folded_nonempty() {
2229 let s = ScalarValue::with_style("line one\nline two", ScalarStyle::Literal);
2232 assert_eq!(s.to_yaml_string(), "|\n line one\n line two");
2233
2234 let s = ScalarValue::with_style("line one\nline two", ScalarStyle::Folded);
2235 assert_eq!(s.to_yaml_string(), ">\n line one\n line two");
2236 }
2237
2238 #[test]
2239 fn test_display_matches_yaml_string() {
2240 let s = ScalarValue::string("true");
2241 assert_eq!(format!("{}", s), s.to_yaml_string());
2242 assert_eq!(format!("{}", s), "'true'");
2243 }
2244
2245 #[test]
2246 fn test_as_yaml_token_kind_per_type() {
2247 use crate::lex::SyntaxKind;
2248 use rowan::ast::AstNode;
2249
2250 let cases = [
2251 (ScalarValue::parse("42"), SyntaxKind::INT),
2252 (ScalarValue::parse("3.14"), SyntaxKind::FLOAT),
2253 (ScalarValue::parse("true"), SyntaxKind::BOOL),
2254 (ScalarValue::parse("null"), SyntaxKind::NULL),
2255 (ScalarValue::string("hello"), SyntaxKind::STRING),
2256 ];
2257 for (value, expected) in cases {
2258 let mut builder = rowan::GreenNodeBuilder::new();
2259 crate::AsYaml::build_content(&value, &mut builder, 0, false);
2260 let green = builder.finish();
2261 let node = rowan::SyntaxNode::<crate::yaml::Lang>::new_root(green);
2262 let scalar = crate::yaml::Scalar::cast(node).unwrap();
2263 let kind = scalar
2264 .syntax()
2265 .children_with_tokens()
2266 .filter_map(|c| c.into_token())
2267 .next()
2268 .unwrap()
2269 .kind();
2270 assert_eq!(kind, expected, "value {:?}", value.value());
2271 }
2272 }
2273
2274 #[test]
2275 fn test_as_yaml_quotes_string_that_looks_special() {
2276 use std::str::FromStr;
2277
2278 let doc = crate::yaml::Document::from_str("key: x\n").unwrap();
2281 doc.as_mapping()
2282 .unwrap()
2283 .set("key", ScalarValue::string("null"));
2284 assert_eq!(doc.to_string(), "key: 'null'\n");
2285
2286 let doc = crate::yaml::Document::from_str("key: x\n").unwrap();
2288 doc.as_mapping()
2289 .unwrap()
2290 .set("key", ScalarValue::string("has: colon"));
2291 assert_eq!(doc.to_string(), "key: 'has: colon'\n");
2292
2293 let doc = crate::yaml::Document::from_str("key: x\n").unwrap();
2295 doc.as_mapping().unwrap().set("key", ScalarValue::null());
2296 assert_eq!(doc.to_string(), "key: null\n");
2297 }
2298
2299 #[test]
2300 #[cfg(feature = "regex")]
2301 fn test_as_regex_type_gated() {
2302 let re = ScalarValue::regex(r"\d+");
2304 assert!(re.as_regex().is_some());
2305
2306 let not_re = ScalarValue::string(r"\d+");
2307 assert!(not_re.as_regex().is_none());
2308
2309 assert!(not_re.try_as_regex().is_ok());
2311 assert!(ScalarValue::string("(").try_as_regex().is_err());
2312 }
2313}