use std::fmt;
#[cfg(feature = "base64")]
use base64::{engine::general_purpose, Engine as _};
#[cfg(feature = "base64")]
fn base64_encode(input: &[u8]) -> String {
general_purpose::STANDARD.encode(input)
}
#[cfg(feature = "base64")]
fn base64_decode(input: &str) -> Result<Vec<u8>, String> {
general_purpose::STANDARD
.decode(input.trim())
.map_err(|e| format!("Base64 decode error: {}", e))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ScalarStyle {
Plain,
SingleQuoted,
DoubleQuoted,
Literal,
Folded,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ScalarType {
String,
Integer,
Float,
Boolean,
Null,
#[cfg(feature = "base64")]
Binary,
Timestamp,
Regex,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ScalarValue {
value: String,
style: ScalarStyle,
scalar_type: ScalarType,
}
impl ScalarValue {
pub fn string(value: impl Into<String>) -> Self {
let value = value.into();
let style = Self::detect_style(&value);
let scalar_type = ScalarType::String;
Self {
value,
style,
scalar_type,
}
}
pub fn parse_escape_sequences(text: &str) -> String {
let mut result = String::with_capacity(text.len()); let mut chars = text.chars().peekable();
while let Some(ch) = chars.next() {
if ch == '\\' {
if let Some(&escaped) = chars.peek() {
chars.next(); match escaped {
'n' => result.push('\n'),
't' => result.push('\t'),
'r' => result.push('\r'),
'b' => result.push('\x08'),
'f' => result.push('\x0C'),
'a' => result.push('\x07'), 'e' => result.push('\x1B'), 'v' => result.push('\x0B'), '0' => result.push('\0'), '\\' => result.push('\\'),
'"' => result.push('"'),
'\'' => result.push('\''),
'/' => result.push('/'),
' ' => {
if let Some(&'\n') = chars.peek() {
chars.next(); continue;
} else {
result.push(' ');
}
}
'\n' => {
continue;
}
'x' => {
let mut hex_chars = [0u8; 2];
let mut count = 0;
for (i, ch) in chars.by_ref().take(2).enumerate() {
if let Some(digit) = ch.to_digit(16) {
hex_chars[i] = digit as u8;
count += 1;
} else {
result.push('\\');
result.push('x');
for &hex_char in hex_chars.iter().take(count) {
result.push(char::from_digit(hex_char as u32, 16).unwrap());
}
result.push(ch);
break;
}
}
if count == 2 {
let code = hex_chars[0] * 16 + hex_chars[1];
result.push(code as char);
} else if count > 0 {
result.push('\\');
result.push('x');
for &hex_char in hex_chars.iter().take(count) {
result.push(char::from_digit(hex_char as u32, 16).unwrap());
}
}
}
'u' => {
let hex_digits: String = chars.by_ref().take(4).collect();
if hex_digits.len() == 4 {
if let Ok(code) = u16::from_str_radix(&hex_digits, 16) {
if let Some(unicode_char) = char::from_u32(code as u32) {
result.push(unicode_char);
} else {
result.push('\\');
result.push('u');
result.push_str(&hex_digits);
}
} else {
result.push('\\');
result.push('u');
result.push_str(&hex_digits);
}
} else {
result.push('\\');
result.push('u');
result.push_str(&hex_digits);
}
}
'U' => {
let hex_digits: String = chars.by_ref().take(8).collect();
if hex_digits.len() == 8 {
if let Ok(code) = u32::from_str_radix(&hex_digits, 16) {
if let Some(unicode_char) = char::from_u32(code) {
result.push(unicode_char);
} else {
result.push('\\');
result.push('U');
result.push_str(&hex_digits);
}
} else {
result.push('\\');
result.push('U');
result.push_str(&hex_digits);
}
} else {
result.push('\\');
result.push('U');
result.push_str(&hex_digits);
}
}
_ => {
result.push('\\');
result.push(escaped);
}
}
} else {
result.push('\\');
}
} else {
result.push(ch);
}
}
result
}
pub fn with_style(value: impl Into<String>, style: ScalarStyle) -> Self {
Self {
value: value.into(),
style,
scalar_type: ScalarType::String,
}
}
pub fn plain(value: impl Into<String>) -> Self {
Self::with_style(value, ScalarStyle::Plain)
}
pub fn single_quoted(value: impl Into<String>) -> Self {
Self::with_style(value, ScalarStyle::SingleQuoted)
}
pub fn double_quoted(value: impl Into<String>) -> Self {
Self::with_style(value, ScalarStyle::DoubleQuoted)
}
pub fn literal(value: impl Into<String>) -> Self {
Self::with_style(value, ScalarStyle::Literal)
}
pub fn folded(value: impl Into<String>) -> Self {
Self::with_style(value, ScalarStyle::Folded)
}
pub fn null() -> Self {
Self {
value: "null".to_string(),
style: ScalarStyle::Plain,
scalar_type: ScalarType::Null,
}
}
#[cfg(feature = "base64")]
pub fn binary(data: &[u8]) -> Self {
let encoded = base64_encode(data);
Self {
value: encoded,
style: ScalarStyle::Plain,
scalar_type: ScalarType::Binary,
}
}
pub fn timestamp(value: impl Into<String>) -> Self {
Self {
value: value.into(),
style: ScalarStyle::Plain,
scalar_type: ScalarType::Timestamp,
}
}
pub fn regex(pattern: impl Into<String>) -> Self {
Self {
value: pattern.into(),
style: ScalarStyle::Plain,
scalar_type: ScalarType::Regex,
}
}
pub fn value(&self) -> &str {
&self.value
}
pub fn style(&self) -> ScalarStyle {
self.style
}
pub fn scalar_type(&self) -> ScalarType {
self.scalar_type
}
pub fn to_i64(&self) -> Option<i64> {
if self.scalar_type == ScalarType::Integer {
Self::parse_integer(&self.value)
} else {
None
}
}
pub fn to_f64(&self) -> Option<f64> {
if self.scalar_type == ScalarType::Float {
self.value.trim().parse::<f64>().ok()
} else {
None
}
}
pub fn to_bool(&self) -> Option<bool> {
if self.scalar_type == ScalarType::Boolean {
match self.value.to_lowercase().as_str() {
"true" | "yes" | "on" => Some(true),
"false" | "no" | "off" => Some(false),
_ => None,
}
} else {
None
}
}
#[cfg(feature = "base64")]
pub fn as_binary(&self) -> Option<Result<Vec<u8>, String>> {
match self.scalar_type {
ScalarType::Binary => Some(base64_decode(&self.value)),
_ => None,
}
}
#[cfg(feature = "base64")]
pub fn is_binary(&self) -> bool {
self.scalar_type == ScalarType::Binary
}
pub fn is_timestamp(&self) -> bool {
self.scalar_type == ScalarType::Timestamp
}
pub fn is_regex(&self) -> bool {
self.scalar_type == ScalarType::Regex
}
#[cfg(feature = "regex")]
pub fn as_regex(&self) -> Option<regex::Regex> {
if self.scalar_type == ScalarType::Regex {
regex::Regex::new(&self.value).ok()
} else {
None
}
}
#[cfg(feature = "regex")]
pub fn try_as_regex(&self) -> Result<regex::Regex, regex::Error> {
regex::Regex::new(&self.value)
}
pub fn coerce_to_type(&self, target_type: ScalarType) -> Option<ScalarValue> {
if self.scalar_type == target_type {
return Some(self.clone());
}
match target_type {
ScalarType::String => Some(ScalarValue {
value: self.value.clone(),
style: ScalarStyle::Plain,
scalar_type: ScalarType::String,
}),
ScalarType::Integer => Self::parse_integer(&self.value).map(ScalarValue::from),
ScalarType::Float => self.value.parse::<f64>().ok().map(ScalarValue::from),
ScalarType::Boolean => match self.value.to_lowercase().as_str() {
"true" | "yes" | "on" | "1" => Some(ScalarValue::from(true)),
"false" | "no" | "off" | "0" => Some(ScalarValue::from(false)),
_ => None,
},
ScalarType::Null => match self.value.to_lowercase().as_str() {
"null" | "~" | "" => Some(ScalarValue::null()),
_ => None,
},
#[cfg(feature = "base64")]
ScalarType::Binary => {
if base64_decode(&self.value).is_ok() {
Some(ScalarValue {
value: self.value.clone(),
style: ScalarStyle::Plain,
scalar_type: ScalarType::Binary,
})
} else {
None
}
}
ScalarType::Timestamp => {
if self.is_valid_timestamp(&self.value) {
Some(ScalarValue::timestamp(&self.value))
} else {
None
}
}
ScalarType::Regex => {
Some(ScalarValue::regex(&self.value))
}
}
}
pub(crate) fn parse_integer(value: &str) -> Option<i64> {
let value = value.trim();
let (is_negative, value) = if let Some(stripped) = value.strip_prefix('-') {
(true, stripped)
} else if let Some(stripped) = value.strip_prefix('+') {
(false, stripped)
} else {
(false, value)
};
let parsed = if let Some(hex_part) = value
.strip_prefix("0x")
.or_else(|| value.strip_prefix("0X"))
{
i64::from_str_radix(hex_part, 16).ok()
} else if let Some(bin_part) = value
.strip_prefix("0b")
.or_else(|| value.strip_prefix("0B"))
{
i64::from_str_radix(bin_part, 2).ok()
} else if let Some(oct_part) = value
.strip_prefix("0o")
.or_else(|| value.strip_prefix("0O"))
{
i64::from_str_radix(oct_part, 8).ok()
} else if value.starts_with('0')
&& value.len() > 1
&& value.chars().all(|c| c.is_ascii_digit())
{
i64::from_str_radix(value, 8).ok()
} else {
value.parse::<i64>().ok()
};
parsed.map(|n| if is_negative { -n } else { n })
}
pub fn auto_detect_type(value: &str) -> ScalarType {
match value.to_lowercase().as_str() {
"null" | "~" | "" => return ScalarType::Null,
_ => {}
}
match value.to_lowercase().as_str() {
"true" | "false" | "yes" | "no" | "on" | "off" => return ScalarType::Boolean,
_ => {}
}
if Self::parse_integer(value).is_some() {
return ScalarType::Integer;
}
if value.parse::<f64>().is_ok() {
return ScalarType::Float;
}
if Self::is_valid_timestamp_static(value) {
return ScalarType::Timestamp;
}
#[cfg(feature = "base64")]
if Self::looks_like_base64(value) && base64_decode(value).is_ok() {
return ScalarType::Binary;
}
ScalarType::String
}
pub fn parse(value: impl Into<String>) -> Self {
let value = value.into();
let scalar_type = Self::auto_detect_type(&value);
let style = match scalar_type {
ScalarType::String => Self::detect_style(&value),
_ => ScalarStyle::Plain,
};
Self {
value,
style,
scalar_type,
}
}
pub fn from_scalar(scalar: &crate::yaml::Scalar) -> Self {
use crate::lex::SyntaxKind;
use rowan::ast::AstNode;
let value = scalar.as_string();
let syntax_node = scalar.syntax();
let scalar_type = if let Some(token) = syntax_node.first_token() {
match token.kind() {
SyntaxKind::INT => ScalarType::Integer,
SyntaxKind::FLOAT => ScalarType::Float,
SyntaxKind::BOOL => ScalarType::Boolean,
SyntaxKind::NULL => ScalarType::Null,
SyntaxKind::STRING => ScalarType::String,
_ => ScalarType::String, }
} else {
ScalarType::String
};
let raw_text = scalar.value();
let style = if raw_text.starts_with('"') && raw_text.ends_with('"') {
ScalarStyle::DoubleQuoted
} else if raw_text.starts_with('\'') && raw_text.ends_with('\'') {
ScalarStyle::SingleQuoted
} else {
ScalarStyle::Plain
};
Self {
value,
style,
scalar_type,
}
}
#[cfg(feature = "base64")]
fn looks_like_base64(value: &str) -> bool {
if value.is_empty() {
return false;
}
if value.len() < 4 || value.len() % 4 != 0 {
return false;
}
let padding_count = value.chars().filter(|&c| c == '=').count();
if padding_count > 2 {
return false;
}
if !value
.chars()
.all(|c| matches!(c, 'A'..='Z' | 'a'..='z' | '0'..='9' | '+' | '/' | '='))
{
return false;
}
if padding_count > 0 {
let padding_start = value.len() - padding_count;
if !value[padding_start..].chars().all(|c| c == '=') {
return false;
}
if value[..padding_start].contains('=') {
return false;
}
}
base64_decode(value).is_ok()
}
fn is_valid_timestamp(&self, value: &str) -> bool {
Self::is_valid_timestamp_static(value)
}
fn is_valid_timestamp_static(value: &str) -> bool {
if Self::matches_iso8601_pattern(value) {
return true;
}
if let Ok(timestamp) = value.parse::<u64>() {
return timestamp > 0 && timestamp < 4_102_444_800; }
false
}
fn matches_iso8601_pattern(value: &str) -> bool {
let chars: Vec<char> = value.chars().collect();
if chars.len() < 10 {
return false;
}
if !(chars[0..4].iter().all(|c| c.is_ascii_digit())
&& chars[4] == '-'
&& chars[5..7].iter().all(|c| c.is_ascii_digit())
&& chars[7] == '-'
&& chars[8..10].iter().all(|c| c.is_ascii_digit()))
{
return false;
}
let month_str: String = chars[5..7].iter().collect();
let day_str: String = chars[8..10].iter().collect();
if let (Ok(month), Ok(day)) = (month_str.parse::<u8>(), day_str.parse::<u8>()) {
if !(1..=12).contains(&month) || !(1..=31).contains(&day) {
return false;
}
} else {
return false;
}
if chars.len() == 10 {
return true;
}
if chars.len() >= 19 {
let sep = chars[10];
if (sep == 'T' || sep == 't' || sep == ' ')
&& chars[11..13].iter().all(|c| c.is_ascii_digit())
&& chars[13] == ':'
&& chars[14..16].iter().all(|c| c.is_ascii_digit())
&& chars[16] == ':'
&& chars[17..19].iter().all(|c| c.is_ascii_digit())
{
let hour_str: String = chars[11..13].iter().collect();
let minute_str: String = chars[14..16].iter().collect();
let second_str: String = chars[17..19].iter().collect();
if let (Ok(hour), Ok(minute), Ok(second)) = (
hour_str.parse::<u8>(),
minute_str.parse::<u8>(),
second_str.parse::<u8>(),
) {
if hour > 23 || minute > 59 || second > 59 {
return false;
}
} else {
return false;
}
return true;
}
}
false
}
fn detect_style(value: &str) -> ScalarStyle {
if Self::needs_quoting(value) {
if !value.contains('\'') {
ScalarStyle::SingleQuoted
} else {
ScalarStyle::DoubleQuoted
}
} else if value.contains('\n') {
ScalarStyle::Literal
} else {
ScalarStyle::Plain
}
}
fn needs_quoting(value: &str) -> bool {
if value.is_empty() {
return true;
}
if value.eq_ignore_ascii_case("true")
|| value.eq_ignore_ascii_case("false")
|| value.eq_ignore_ascii_case("yes")
|| value.eq_ignore_ascii_case("no")
|| value.eq_ignore_ascii_case("on")
|| value.eq_ignore_ascii_case("off")
|| value.eq_ignore_ascii_case("null")
|| value == "~"
{
return true;
}
if value.parse::<f64>().is_ok() || Self::parse_integer(value).is_some() {
return true;
}
if value.starts_with(|ch: char| {
matches!(ch, '-' | '?' | '[' | ']' | '{' | '}' | ',' | '>' | '<')
}) {
return true;
}
let mut chars = value.chars().peekable();
while let Some(ch) = chars.next() {
match ch {
'&' | '*' | '!' | '|' | '\'' | '"' | '%' => return true,
':' | '#' => {
if chars.peek().map_or(true, |next| next.is_whitespace()) {
return true;
}
}
_ => {}
}
}
if value != value.trim() {
return true;
}
false
}
pub fn to_yaml_string(&self) -> String {
let tag_prefix = match self.scalar_type {
#[cfg(feature = "base64")]
ScalarType::Binary => "!!binary ",
ScalarType::Timestamp => "!!timestamp ",
ScalarType::Regex => "!!regex ",
_ => "",
};
let content = match self.style {
ScalarStyle::Plain => {
match self.scalar_type {
ScalarType::String => {
if Self::needs_quoting(&self.value) {
self.to_single_quoted()
} else {
self.value.clone()
}
}
ScalarType::Integer
| ScalarType::Float
| ScalarType::Boolean
| ScalarType::Null
| ScalarType::Timestamp
| ScalarType::Regex => self.value.clone(),
#[cfg(feature = "base64")]
ScalarType::Binary => self.value.clone(),
}
}
ScalarStyle::SingleQuoted => self.to_single_quoted(),
ScalarStyle::DoubleQuoted => self.to_double_quoted(),
ScalarStyle::Literal => self.to_literal(),
ScalarStyle::Folded => self.to_folded(),
};
format!("{}{}", tag_prefix, content)
}
fn to_single_quoted(&self) -> String {
let escaped = self.value.replace('\'', "''");
format!("'{}'", escaped)
}
fn to_double_quoted(&self) -> String {
let mut result = String::from("\"");
for ch in self.value.chars() {
match ch {
'"' => result.push_str("\\\""),
'\\' => result.push_str("\\\\"),
'\n' => result.push_str("\\n"),
'\r' => result.push_str("\\r"),
'\t' => result.push_str("\\t"),
'\x08' => result.push_str("\\b"),
'\x0C' => result.push_str("\\f"),
'\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 => {
let code_point = c as u32;
if code_point <= 0xFF {
result.push_str(&format!("\\x{:02X}", code_point));
} else if code_point <= 0xFFFF {
result.push_str(&format!("\\u{:04X}", code_point));
} else {
result.push_str(&format!("\\U{:08X}", code_point));
}
}
c => result.push(c),
}
}
result.push('"');
result
}
fn to_literal(&self) -> String {
self.to_literal_with_indent(2)
}
fn to_folded(&self) -> String {
self.to_folded_with_indent(2)
}
pub fn to_literal_with_indent(&self, indent: usize) -> String {
let indent_str = " ".repeat(indent);
let existing_indent = self.detect_content_indentation();
if existing_indent.is_some() {
format!("|\n{}", self.value)
} else {
let indented = self
.value
.lines()
.map(|line| {
if line.trim().is_empty() {
String::new()
} else {
format!("{}{}", indent_str, line)
}
})
.collect::<Vec<_>>()
.join("\n");
format!("|\n{}", indented)
}
}
pub fn to_folded_with_indent(&self, indent: usize) -> String {
let indent_str = " ".repeat(indent);
let existing_indent = self.detect_content_indentation();
if existing_indent.is_some() {
format!(">\n{}", self.value)
} else {
let indented = self
.value
.lines()
.map(|line| {
if line.trim().is_empty() {
String::new()
} else {
format!("{}{}", indent_str, line)
}
})
.collect::<Vec<_>>()
.join("\n");
format!(">\n{}", indented)
}
}
fn detect_content_indentation(&self) -> Option<usize> {
let non_empty_lines: Vec<&str> = self
.value
.lines()
.filter(|line| !line.trim().is_empty())
.collect();
if non_empty_lines.is_empty() {
return None;
}
let mut min_indent = None;
let mut all_have_same_indent = true;
for line in non_empty_lines {
let indent = line.len() - line.trim_start().len();
match min_indent {
None => min_indent = Some(indent),
Some(current_min) => {
if indent != current_min {
all_have_same_indent = false;
}
min_indent = Some(current_min.min(indent));
}
}
}
if all_have_same_indent && min_indent.unwrap_or(0) > 0 {
min_indent
} else {
None
}
}
}
impl fmt::Display for ScalarValue {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.to_yaml_string())
}
}
impl From<String> for ScalarValue {
fn from(value: String) -> Self {
Self::string(value)
}
}
impl From<&str> for ScalarValue {
fn from(value: &str) -> Self {
Self::string(value)
}
}
impl From<i32> for ScalarValue {
fn from(value: i32) -> Self {
Self {
value: value.to_string(),
style: ScalarStyle::Plain,
scalar_type: ScalarType::Integer,
}
}
}
impl From<i64> for ScalarValue {
fn from(value: i64) -> Self {
Self {
value: value.to_string(),
style: ScalarStyle::Plain,
scalar_type: ScalarType::Integer,
}
}
}
impl From<f32> for ScalarValue {
fn from(value: f32) -> Self {
Self {
value: value.to_string(),
style: ScalarStyle::Plain,
scalar_type: ScalarType::Float,
}
}
}
impl From<f64> for ScalarValue {
fn from(value: f64) -> Self {
Self {
value: value.to_string(),
style: ScalarStyle::Plain,
scalar_type: ScalarType::Float,
}
}
}
impl From<bool> for ScalarValue {
fn from(value: bool) -> Self {
Self {
value: if value { "true" } else { "false" }.to_string(),
style: ScalarStyle::Plain,
scalar_type: ScalarType::Boolean,
}
}
}
impl From<crate::yaml::Scalar> for ScalarValue {
fn from(scalar: crate::yaml::Scalar) -> Self {
let value = scalar.as_string();
ScalarValue::parse(&value)
}
}
impl crate::AsYaml for ScalarValue {
fn as_node(&self) -> Option<&crate::yaml::SyntaxNode> {
None
}
fn kind(&self) -> crate::as_yaml::YamlKind {
crate::as_yaml::YamlKind::Scalar
}
fn build_content(
&self,
builder: &mut rowan::GreenNodeBuilder,
_indent: usize,
_flow_context: bool,
) -> bool {
use crate::lex::SyntaxKind;
let token_kind = match self.scalar_type() {
ScalarType::Integer => SyntaxKind::INT,
ScalarType::Float => SyntaxKind::FLOAT,
ScalarType::Boolean => SyntaxKind::BOOL,
ScalarType::Null => SyntaxKind::NULL,
_ => SyntaxKind::STRING,
};
builder.start_node(SyntaxKind::SCALAR.into());
builder.token(token_kind.into(), self.value());
builder.finish_node();
false
}
fn is_inline(&self) -> bool {
true
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_plain_scalars() {
let scalar = ScalarValue::string("simple");
assert_eq!(scalar.to_yaml_string(), "simple");
let scalar = ScalarValue::string("hello world");
assert_eq!(scalar.to_yaml_string(), "hello world");
}
#[test]
fn test_values_needing_quotes() {
let scalar = ScalarValue::string("true");
assert_eq!(scalar.to_yaml_string(), "'true'");
let scalar = ScalarValue::string("false");
assert_eq!(scalar.to_yaml_string(), "'false'");
let scalar = ScalarValue::string("yes");
assert_eq!(scalar.to_yaml_string(), "'yes'");
let scalar = ScalarValue::string("no");
assert_eq!(scalar.to_yaml_string(), "'no'");
let scalar = ScalarValue::string("null");
assert_eq!(scalar.to_yaml_string(), "'null'");
let scalar = ScalarValue::string("~");
assert_eq!(scalar.to_yaml_string(), "'~'");
let scalar = ScalarValue::string("123");
assert_eq!(scalar.to_yaml_string(), "'123'");
let scalar = ScalarValue::string("3.14");
assert_eq!(scalar.to_yaml_string(), "'3.14'");
let scalar = ScalarValue::string("value: something");
assert_eq!(scalar.to_yaml_string(), "'value: something'");
let scalar = ScalarValue::string("# comment");
assert_eq!(scalar.to_yaml_string(), "'# comment'");
let scalar = ScalarValue::string(" spaces ");
assert_eq!(scalar.to_yaml_string(), "' spaces '");
}
#[test]
fn test_single_quoted() {
let scalar = ScalarValue::single_quoted("value with 'quotes'");
assert_eq!(scalar.to_yaml_string(), "'value with ''quotes'''");
}
#[test]
fn test_double_quoted() {
let scalar = ScalarValue::double_quoted("value with \"quotes\" and \\backslash");
assert_eq!(
scalar.to_yaml_string(),
"\"value with \\\"quotes\\\" and \\\\backslash\""
);
let scalar = ScalarValue::double_quoted("line1\nline2\ttab");
assert_eq!(scalar.to_yaml_string(), "\"line1\\nline2\\ttab\"");
}
#[test]
fn test_multiline() {
let scalar = ScalarValue::string("line1\nline2\nline3");
assert_eq!(scalar.style(), ScalarStyle::Literal);
}
#[test]
fn test_from_types() {
let scalar = ScalarValue::from(42);
assert_eq!(scalar.to_yaml_string(), "42");
let scalar = ScalarValue::from(1.234);
assert_eq!(scalar.to_yaml_string(), "1.234");
let scalar = ScalarValue::from(true);
assert_eq!(scalar.to_yaml_string(), "true");
let scalar = ScalarValue::from(false);
assert_eq!(scalar.to_yaml_string(), "false");
}
#[test]
fn test_empty_string() {
let scalar = ScalarValue::string("");
assert_eq!(scalar.to_yaml_string(), "''");
}
#[test]
fn test_special_start_chars() {
let scalar = ScalarValue::string("-item");
assert_eq!(scalar.to_yaml_string(), "'-item'");
let scalar = ScalarValue::string("?key");
assert_eq!(scalar.to_yaml_string(), "'?key'");
let scalar = ScalarValue::string("[array]");
assert_eq!(scalar.to_yaml_string(), "'[array]'");
}
#[test]
fn test_null_scalar() {
let scalar = ScalarValue::null();
assert_eq!(scalar.to_yaml_string(), "null");
assert_eq!(scalar.scalar_type, ScalarType::Null);
}
#[test]
fn test_escape_sequences_basic() {
assert_eq!(
ScalarValue::parse_escape_sequences("hello\\nworld"),
"hello\nworld"
);
assert_eq!(
ScalarValue::parse_escape_sequences("tab\\there"),
"tab\there"
);
assert_eq!(
ScalarValue::parse_escape_sequences("quote\\\"test"),
"quote\"test"
);
assert_eq!(
ScalarValue::parse_escape_sequences("back\\\\slash"),
"back\\slash"
);
assert_eq!(
ScalarValue::parse_escape_sequences("return\\rtest"),
"return\rtest"
);
}
#[test]
fn test_escape_sequences_control_chars() {
assert_eq!(ScalarValue::parse_escape_sequences("bell\\a"), "bell\x07");
assert_eq!(
ScalarValue::parse_escape_sequences("backspace\\b"),
"backspace\x08"
);
assert_eq!(
ScalarValue::parse_escape_sequences("formfeed\\f"),
"formfeed\x0C"
);
assert_eq!(
ScalarValue::parse_escape_sequences("escape\\e"),
"escape\x1B"
);
assert_eq!(ScalarValue::parse_escape_sequences("vtab\\v"), "vtab\x0B");
assert_eq!(ScalarValue::parse_escape_sequences("null\\0"), "null\0");
assert_eq!(ScalarValue::parse_escape_sequences("slash\\/"), "slash/");
}
#[test]
fn test_escape_sequences_unicode_x() {
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"); }
#[test]
fn test_escape_sequences_unicode_u() {
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"); }
#[test]
fn test_escape_sequences_unicode_capital_u() {
assert_eq!(ScalarValue::parse_escape_sequences("\\U00000041"), "A"); assert_eq!(ScalarValue::parse_escape_sequences("\\U0001F603"), "😃"); assert_eq!(ScalarValue::parse_escape_sequences("\\U0001F4A9"), "💩");
assert_eq!(
ScalarValue::parse_escape_sequences("\\UGHIJKLMN"),
"\\UGHIJKLMN"
); assert_eq!(
ScalarValue::parse_escape_sequences("\\U0000004"),
"\\U0000004"
); assert_eq!(
ScalarValue::parse_escape_sequences("\\UFFFFFFFF"),
"\\UFFFFFFFF"
); }
#[test]
fn test_escape_sequences_line_folding() {
assert_eq!(
ScalarValue::parse_escape_sequences("line\\ \nfolding"),
"linefolding"
);
assert_eq!(
ScalarValue::parse_escape_sequences("escaped\\nline\\nbreak"),
"escaped\nline\nbreak"
);
assert_eq!(
ScalarValue::parse_escape_sequences("remove\\\nline\\nbreak"),
"removeline\nbreak"
);
}
#[test]
fn test_escape_sequences_mixed() {
let input = "Hello\\nWorld\\u0021\\x20\\U0001F44D";
let expected = "Hello\nWorld! 👍";
assert_eq!(ScalarValue::parse_escape_sequences(input), expected);
let input = "Quote\\\"back\\\\slash\\ttab";
let expected = "Quote\"back\\slash\ttab";
assert_eq!(ScalarValue::parse_escape_sequences(input), expected);
}
#[test]
fn test_escape_sequences_unknown() {
assert_eq!(ScalarValue::parse_escape_sequences("\\q"), "\\q");
assert_eq!(ScalarValue::parse_escape_sequences("\\z"), "\\z");
assert_eq!(ScalarValue::parse_escape_sequences("\\1"), "\\1");
}
#[test]
fn test_indentation_preservation() {
let content_with_indent = " Line 1\n Line 2 more indented\n Line 3";
let scalar = ScalarValue::literal(content_with_indent);
let yaml_output = scalar.to_literal_with_indent(2);
assert_eq!(
yaml_output,
"|\n Line 1\n Line 2 more indented\n Line 3"
);
}
#[test]
fn test_indentation_detection() {
let consistent_content = " Line 1\n Line 2\n Line 3";
let scalar1 = ScalarValue::literal(consistent_content);
assert_eq!(scalar1.detect_content_indentation(), Some(2));
let no_indent_content = "Line 1\nLine 2\nLine 3";
let scalar2 = ScalarValue::literal(no_indent_content);
assert_eq!(scalar2.detect_content_indentation(), None);
let inconsistent_content = " Line 1\n Line 2\n Line 3";
let scalar3 = ScalarValue::literal(inconsistent_content);
assert_eq!(scalar3.detect_content_indentation(), None);
let empty_content = "";
let scalar4 = ScalarValue::literal(empty_content);
assert_eq!(scalar4.detect_content_indentation(), None);
let whitespace_content = " Line 1\n\n Line 3";
let scalar5 = ScalarValue::literal(whitespace_content);
assert_eq!(scalar5.detect_content_indentation(), Some(2));
}
#[test]
fn test_literal_with_custom_indent() {
let content = "Line 1\nLine 2\nLine 3";
let scalar = ScalarValue::literal(content);
let yaml_4_spaces = scalar.to_literal_with_indent(4);
assert_eq!(yaml_4_spaces, "|\n Line 1\n Line 2\n Line 3");
let yaml_1_space = scalar.to_literal_with_indent(1);
assert_eq!(yaml_1_space, "|\n Line 1\n Line 2\n Line 3");
}
#[test]
fn test_folded_with_custom_indent() {
let content = "Line 1\nLine 2\nLine 3";
let scalar = ScalarValue::folded(content);
let yaml_3_spaces = scalar.to_folded_with_indent(3);
assert_eq!(yaml_3_spaces, ">\n Line 1\n Line 2\n Line 3");
}
#[test]
fn test_mixed_empty_lines_preservation() {
let content_with_empty_lines = "Line 1\n\nLine 3\n\n\nLine 6";
let scalar = ScalarValue::literal(content_with_empty_lines);
let yaml_output = scalar.to_literal_with_indent(2);
assert_eq!(yaml_output, "|\n Line 1\n\n Line 3\n\n\n Line 6");
let lines: Vec<&str> = yaml_output.lines().collect();
let empty_line_count = lines.iter().filter(|line| line.is_empty()).count();
assert_eq!(empty_line_count, 3);
}
#[test]
fn test_escape_sequences_edge_cases() {
assert_eq!(ScalarValue::parse_escape_sequences(""), "");
assert_eq!(ScalarValue::parse_escape_sequences("\\"), "\\");
assert_eq!(
ScalarValue::parse_escape_sequences("no escapes"),
"no escapes"
);
assert_eq!(ScalarValue::parse_escape_sequences("\\\\\\\\"), "\\\\");
}
#[test]
fn test_double_quoted_with_escapes() {
let original = "Hello\nWorld\t😃";
let scalar = ScalarValue::double_quoted(original);
let yaml_string = scalar.to_yaml_string();
assert_eq!(yaml_string, "\"Hello\\nWorld\\t\\U0001F603\"");
let parsed = ScalarValue::parse_escape_sequences(&yaml_string[1..yaml_string.len() - 1]);
assert_eq!(parsed, original);
}
#[test]
fn test_unicode_output_formatting() {
let scalar = ScalarValue::double_quoted("Hello 世界 🌍");
let yaml_string = scalar.to_yaml_string();
assert_eq!(yaml_string, "\"Hello \\u4E16\\u754C \\U0001F30D\"");
assert_eq!(scalar.value(), "Hello 世界 🌍");
}
#[test]
#[cfg(feature = "base64")]
fn test_binary_data_encoding() {
let data = b"Hello, World!";
let scalar = ScalarValue::binary(data);
assert!(scalar.is_binary());
assert_eq!(scalar.scalar_type(), ScalarType::Binary);
let yaml_output = scalar.to_yaml_string();
assert!(yaml_output.starts_with("!!binary "));
if let Some(decoded_result) = scalar.as_binary() {
let decoded = decoded_result.expect("Should decode successfully");
assert_eq!(decoded, data);
} else {
panic!("Should be able to extract binary data");
}
}
#[test]
#[cfg(feature = "base64")]
fn test_base64_encoding_decoding() {
let test_cases = [
b"".as_slice(),
b"A",
b"AB",
b"ABC",
b"ABCD",
b"Hello, World!",
&[0, 1, 2, 3, 255, 254, 253],
];
for data in test_cases {
let encoded = base64_encode(data);
let decoded = base64_decode(&encoded).expect("Should decode successfully");
assert_eq!(decoded, data, "Failed for data: {:?}", data);
}
}
#[test]
fn test_timestamp_creation_and_validation() {
let valid_timestamps = [
"2023-12-25",
"2023-12-25T10:30:45",
"2023-12-25 10:30:45",
"2023-12-25T10:30:45Z",
"2001-12-14 21:59:43.10 -5", "2001-12-15T02:59:43.1Z", "2001-12-14t21:59:43.10-05:00", ];
for ts in valid_timestamps {
let scalar = ScalarValue::timestamp(ts);
assert!(scalar.is_timestamp());
assert_eq!(scalar.scalar_type(), ScalarType::Timestamp);
assert_eq!(scalar.value(), ts);
let yaml_output = scalar.to_yaml_string();
assert_eq!(yaml_output, format!("!!timestamp {}", ts));
let auto_scalar = ScalarValue::parse(ts);
assert_eq!(
auto_scalar.scalar_type(),
ScalarType::Timestamp,
"Failed to auto-detect '{}' as timestamp",
ts
);
}
let invalid_timestamps = [
"not-a-date",
"2023-13-01", "2023-12-32", "12:34:56", "2023/12/25", ];
for ts in invalid_timestamps {
let auto_scalar = ScalarValue::parse(ts);
assert_ne!(
auto_scalar.scalar_type(),
ScalarType::Timestamp,
"'{}' should not be detected as timestamp",
ts
);
}
}
#[test]
fn test_regex_creation() {
let pattern = r"^\d{3}-\d{2}-\d{4}$";
let scalar = ScalarValue::regex(pattern);
assert!(scalar.is_regex());
assert_eq!(scalar.scalar_type(), ScalarType::Regex);
assert_eq!(scalar.value(), pattern);
let yaml_output = scalar.to_yaml_string();
assert_eq!(yaml_output, format!("!!regex {}", pattern));
}
#[test]
fn test_regex_edge_cases() {
let empty_regex = ScalarValue::regex("");
assert!(empty_regex.is_regex());
assert_eq!(empty_regex.value(), "");
assert_eq!(empty_regex.to_yaml_string(), "!!regex ");
let special_chars = ScalarValue::regex(r"[.*+?^${}()|[\]\\]");
assert!(special_chars.is_regex());
assert_eq!(special_chars.value(), r"[.*+?^${}()|[\]\\]");
let unicode_regex = ScalarValue::regex(r"\p{L}+");
assert!(unicode_regex.is_regex());
assert_eq!(unicode_regex.value(), r"\p{L}+");
let long_pattern = "a".repeat(1000);
let long_regex = ScalarValue::regex(&long_pattern);
assert!(long_regex.is_regex());
assert_eq!(long_regex.value(), long_pattern);
let quoted_regex = ScalarValue::regex(r#"'quoted' and "double quoted" with \\ backslash"#);
assert!(quoted_regex.is_regex());
assert_eq!(
quoted_regex.value(),
r#"'quoted' and "double quoted" with \\ backslash"#
);
}
#[test]
fn test_regex_type_coercion() {
let regex_scalar = ScalarValue::regex(r"\d+");
let string_scalar = regex_scalar.coerce_to_type(ScalarType::String).unwrap();
assert_eq!(string_scalar.scalar_type(), ScalarType::String);
assert_eq!(string_scalar.value(), r"\d+");
assert!(!string_scalar.is_regex());
let str_scalar = ScalarValue::string("test.*");
let regex_from_string = str_scalar.coerce_to_type(ScalarType::Regex).unwrap();
assert_eq!(regex_from_string.scalar_type(), ScalarType::Regex);
assert_eq!(regex_from_string.value(), "test.*");
assert!(regex_from_string.is_regex());
assert!(regex_scalar.coerce_to_type(ScalarType::Integer).is_none());
assert!(regex_scalar.coerce_to_type(ScalarType::Float).is_none());
assert!(regex_scalar.coerce_to_type(ScalarType::Boolean).is_none());
}
#[test]
#[cfg(feature = "regex")]
fn test_regex_compilation() {
let regex_scalar = ScalarValue::regex(r"\d{3}-\d{4}");
let compiled = regex_scalar.as_regex().unwrap();
assert!(compiled.is_match("555-1234"));
assert!(!compiled.is_match("not-a-phone"));
let string_scalar = ScalarValue::string("not a regex");
assert!(string_scalar.as_regex().is_none());
let pattern_scalar = ScalarValue::string(r"^\w+@\w+\.\w+$");
let email_regex = pattern_scalar.try_as_regex().unwrap();
assert!(email_regex.is_match("test@example.com"));
assert!(!email_regex.is_match("not-an-email"));
let invalid_scalar = ScalarValue::regex(r"[invalid(");
assert!(invalid_scalar.as_regex().is_none());
let invalid_pattern = ScalarValue::string(r"[invalid(");
assert!(invalid_pattern.try_as_regex().is_err());
}
#[test]
#[cfg(feature = "regex")]
fn test_regex_extraction_use_cases() {
let validation_rules = [
ScalarValue::regex(r"^\d{5}$"), ScalarValue::regex(r"^[A-Z]{2}$"), ScalarValue::regex(r"^\(\d{3}\) \d{3}-\d{4}$"), ];
let test_values = ["12345", "CA", "(555) 123-4567"];
for (rule, value) in validation_rules.iter().zip(test_values.iter()) {
let regex = rule.as_regex().unwrap();
assert!(regex.is_match(value), "Pattern should match {}", value);
}
let email_regex = ScalarValue::regex(r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$");
let compiled = email_regex.as_regex().unwrap();
assert!(compiled.is_match("user@example.com"));
assert!(compiled.is_match("test.user+tag@sub.domain.org"));
assert!(!compiled.is_match("invalid.email"));
let version_regex = ScalarValue::regex(r"^v(\d+)\.(\d+)\.(\d+)$");
let compiled = version_regex.as_regex().unwrap();
if let Some(captures) = compiled.captures("v1.2.3") {
assert_eq!(captures.get(1).unwrap().as_str(), "1");
assert_eq!(captures.get(2).unwrap().as_str(), "2");
assert_eq!(captures.get(3).unwrap().as_str(), "3");
} else {
panic!("Should have matched version string");
}
}
#[test]
fn test_type_coercion() {
let str_scalar = ScalarValue::string("42");
let int_scalar = str_scalar.coerce_to_type(ScalarType::Integer).unwrap();
assert_eq!(int_scalar.scalar_type(), ScalarType::Integer);
assert_eq!(int_scalar.value(), "42");
let bool_scalar = ScalarValue::string("true")
.coerce_to_type(ScalarType::Boolean)
.unwrap();
assert_eq!(bool_scalar.scalar_type(), ScalarType::Boolean);
assert_eq!(bool_scalar.value(), "true");
let yes_scalar = ScalarValue::string("yes")
.coerce_to_type(ScalarType::Boolean)
.unwrap();
assert_eq!(yes_scalar.value(), "true");
let no_scalar = ScalarValue::string("no")
.coerce_to_type(ScalarType::Boolean)
.unwrap();
assert_eq!(no_scalar.value(), "false");
let str_scalar = ScalarValue::string("not_a_number");
assert!(str_scalar.coerce_to_type(ScalarType::Integer).is_none());
}
#[test]
fn test_auto_type_detection() {
assert_eq!(ScalarValue::auto_detect_type("42"), ScalarType::Integer);
assert_eq!(ScalarValue::auto_detect_type("3.14"), ScalarType::Float);
assert_eq!(ScalarValue::auto_detect_type("true"), ScalarType::Boolean);
assert_eq!(ScalarValue::auto_detect_type("false"), ScalarType::Boolean);
assert_eq!(ScalarValue::auto_detect_type("yes"), ScalarType::Boolean);
assert_eq!(ScalarValue::auto_detect_type("null"), ScalarType::Null);
assert_eq!(ScalarValue::auto_detect_type("~"), ScalarType::Null);
assert_eq!(ScalarValue::auto_detect_type(""), ScalarType::Null);
assert_eq!(
ScalarValue::auto_detect_type("2023-12-25"),
ScalarType::Timestamp
);
assert_eq!(
ScalarValue::auto_detect_type("2023-12-25T10:30:45"),
ScalarType::Timestamp
);
#[cfg(feature = "base64")]
assert_eq!(
ScalarValue::auto_detect_type("SGVsbG8gV29ybGQ="),
ScalarType::Binary
);
#[cfg(not(feature = "base64"))]
assert_eq!(
ScalarValue::auto_detect_type("SGVsbG8gV29ybGQ="),
ScalarType::String
);
assert_eq!(
ScalarValue::auto_detect_type("hello world"),
ScalarType::String
);
}
#[test]
fn test_from_yaml_scalar_creation() {
let int_scalar = ScalarValue::parse("123");
assert_eq!(int_scalar.scalar_type(), ScalarType::Integer);
let bool_scalar = ScalarValue::parse("true");
assert_eq!(bool_scalar.scalar_type(), ScalarType::Boolean);
let timestamp_scalar = ScalarValue::parse("2023-12-25");
assert_eq!(timestamp_scalar.scalar_type(), ScalarType::Timestamp);
let string_scalar = ScalarValue::parse("hello world");
assert_eq!(string_scalar.scalar_type(), ScalarType::String);
}
#[test]
fn test_timestamp_pattern_matching() {
assert!(ScalarValue::matches_iso8601_pattern("2023-12-25"));
assert!(ScalarValue::matches_iso8601_pattern("2023-12-25T10:30:45"));
assert!(ScalarValue::matches_iso8601_pattern("2023-12-25t10:30:45")); assert!(ScalarValue::matches_iso8601_pattern("2023-12-25 10:30:45"));
assert!(ScalarValue::matches_iso8601_pattern("2023-01-01T00:00:00"));
assert!(ScalarValue::matches_iso8601_pattern(
"2001-12-14t21:59:43.10-05:00"
));
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"));
assert!(!ScalarValue::matches_iso8601_pattern("2023"));
}
#[test]
#[cfg(feature = "base64")]
fn test_base64_detection() {
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===")); }
#[test]
#[cfg(feature = "base64")]
fn test_binary_yaml_output_with_tags() {
let data = b"Binary data here";
let scalar = ScalarValue::binary(data);
let yaml_output = scalar.to_yaml_string();
assert!(yaml_output.starts_with("!!binary "));
let base64_part = &yaml_output[9..]; let decoded = base64_decode(base64_part).expect("Should decode");
assert_eq!(decoded, data);
}
#[test]
#[cfg(feature = "base64")]
fn test_special_data_types_with_different_styles() {
let data = b"test";
let binary_scalar = ScalarValue::binary(data);
let mut styled_binary = binary_scalar;
styled_binary.style = ScalarStyle::DoubleQuoted;
assert_eq!(styled_binary.to_yaml_string(), "!!binary \"dGVzdA==\"");
}
#[test]
fn test_type_checking_methods() {
#[cfg(feature = "base64")]
let binary_scalar = ScalarValue::binary(b"test");
let timestamp_scalar = ScalarValue::timestamp("2023-12-25");
let regex_scalar = ScalarValue::regex(r"\d+");
let string_scalar = ScalarValue::string("hello");
#[cfg(feature = "base64")]
assert!(binary_scalar.is_binary());
#[cfg(feature = "base64")]
assert!(!binary_scalar.is_timestamp());
#[cfg(feature = "base64")]
assert!(!binary_scalar.is_regex());
#[cfg(feature = "base64")]
assert!(!timestamp_scalar.is_binary());
assert!(timestamp_scalar.is_timestamp());
assert!(!timestamp_scalar.is_regex());
#[cfg(feature = "base64")]
assert!(!regex_scalar.is_binary());
assert!(!regex_scalar.is_timestamp());
assert!(regex_scalar.is_regex());
#[cfg(feature = "base64")]
assert!(!string_scalar.is_binary());
assert!(!string_scalar.is_timestamp());
assert!(!string_scalar.is_regex());
}
#[test]
fn test_binary_number_parsing() {
assert_eq!(ScalarValue::parse_integer("0b1010"), Some(10));
assert_eq!(ScalarValue::parse_integer("0b11111111"), Some(255));
assert_eq!(ScalarValue::parse_integer("0B101"), Some(5)); assert_eq!(ScalarValue::parse_integer("-0b1010"), Some(-10));
assert_eq!(ScalarValue::parse_integer("+0b101"), Some(5));
assert_eq!(ScalarValue::auto_detect_type("0b1010"), ScalarType::Integer);
assert_eq!(
ScalarValue::auto_detect_type("0B11111111"),
ScalarType::Integer
);
assert_eq!(ScalarValue::parse_integer("0b1012"), None); assert_eq!(ScalarValue::parse_integer("0b"), None); }
#[test]
fn test_modern_octal_number_parsing() {
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));
assert_eq!(ScalarValue::parse_integer("+0o644"), Some(420));
assert_eq!(ScalarValue::auto_detect_type("0o755"), ScalarType::Integer);
assert_eq!(ScalarValue::auto_detect_type("0O644"), ScalarType::Integer);
assert_eq!(ScalarValue::parse_integer("0o789"), None); assert_eq!(ScalarValue::parse_integer("0o"), None); }
#[test]
fn test_legacy_octal_number_parsing() {
assert_eq!(ScalarValue::parse_integer("0755"), Some(493));
assert_eq!(ScalarValue::parse_integer("0644"), Some(420));
assert_eq!(ScalarValue::parse_integer("0777"), Some(511));
assert_eq!(ScalarValue::auto_detect_type("0755"), ScalarType::Integer);
assert_eq!(ScalarValue::auto_detect_type("0644"), ScalarType::Integer);
assert_eq!(ScalarValue::parse_integer("0"), Some(0)); assert_eq!(ScalarValue::parse_integer("00"), Some(0));
assert_eq!(ScalarValue::parse_integer("0789"), None);
assert_eq!(ScalarValue::parse_integer("0128"), None);
}
#[test]
fn test_hexadecimal_number_parsing() {
assert_eq!(ScalarValue::parse_integer("0xFF"), Some(255));
assert_eq!(ScalarValue::parse_integer("0x1A"), Some(26));
assert_eq!(ScalarValue::parse_integer("0XFF"), Some(255)); assert_eq!(ScalarValue::parse_integer("-0xFF"), Some(-255));
assert_eq!(ScalarValue::parse_integer("+0x1A"), Some(26));
assert_eq!(ScalarValue::auto_detect_type("0xFF"), ScalarType::Integer);
assert_eq!(ScalarValue::auto_detect_type("0X1A"), ScalarType::Integer);
}
#[test]
fn test_decimal_number_parsing() {
assert_eq!(ScalarValue::parse_integer("42"), Some(42));
assert_eq!(ScalarValue::parse_integer("123"), Some(123));
assert_eq!(ScalarValue::parse_integer("-42"), Some(-42));
assert_eq!(ScalarValue::parse_integer("+123"), Some(123));
assert_eq!(ScalarValue::auto_detect_type("42"), ScalarType::Integer);
assert_eq!(ScalarValue::auto_detect_type("-123"), ScalarType::Integer);
}
#[test]
fn test_number_format_yaml_output() {
let binary_scalar = ScalarValue::parse("0b1010");
assert_eq!(binary_scalar.scalar_type(), ScalarType::Integer);
assert_eq!(binary_scalar.value(), "0b1010");
let octal_scalar = ScalarValue::parse("0o755");
assert_eq!(octal_scalar.scalar_type(), ScalarType::Integer);
assert_eq!(octal_scalar.value(), "0o755");
let hex_scalar = ScalarValue::parse("0xFF");
assert_eq!(hex_scalar.scalar_type(), ScalarType::Integer);
assert_eq!(hex_scalar.value(), "0xFF");
let legacy_octal_scalar = ScalarValue::parse("0755");
assert_eq!(legacy_octal_scalar.scalar_type(), ScalarType::Integer);
assert_eq!(legacy_octal_scalar.value(), "0755");
}
}