use std::borrow::Cow;
use crate::Value;
pub(crate) fn seems_null(s: &str) -> bool {
matches!(s, "" | "~" | "null" | "Null" | "NULL")
}
pub(crate) fn seems_bool(s: &str) -> bool {
s == "true" || s == "false"
}
pub(crate) fn seems_int(s: &str) -> bool {
s.parse::<i64>().is_ok() || s.parse::<u64>().is_ok()
}
pub(crate) fn parse_yaml_int(s: &str) -> Option<Value<'static>> {
if let Some(hex) = s.strip_prefix("0x") {
return u64::from_str_radix(hex, 16).ok().map(Value::UInt);
}
if let Some(oct) = s.strip_prefix("0o") {
return u64::from_str_radix(oct, 8).ok().map(Value::UInt);
}
let body = s.strip_prefix('+').unwrap_or(s);
if let Ok(n) = body.parse::<u64>() {
return Some(Value::UInt(n));
}
s.parse::<i64>().ok().map(Value::Int)
}
pub(crate) fn seems_float(s: &str) -> bool {
matches!(s, ".inf" | "-.inf" | "+.inf" | ".nan")
|| (s.parse::<f64>().is_ok() && s.bytes().any(|b| b == b'.' || b == b'e' || b == b'E'))
}
pub(crate) fn parse_yaml_float(s: &str) -> Option<f64> {
match s {
".nan" | ".NaN" | ".NAN" => return Some(f64::NAN),
".inf" | ".Inf" | ".INF" | "+.inf" | "+.Inf" | "+.INF" => return Some(f64::INFINITY),
"-.inf" | "-.Inf" | "-.INF" => return Some(f64::NEG_INFINITY),
_ => {}
}
s.parse::<f64>().ok()
}
pub(crate) fn seems_scalar_typed(s: &str) -> bool {
seems_null(s) || seems_bool(s) || seems_int(s) || seems_float(s)
}
pub(crate) fn resolve_scalar<'a>(s: Cow<'a, str>) -> Value<'a> {
use Value::*;
match s.as_ref() {
"" | "~" | "null" | "Null" | "NULL" => return Null,
"true" | "True" | "TRUE" => return Bool(true),
"false" | "False" | "FALSE" => return Bool(false),
_ => {}
}
if let Some(v) = parse_yaml_int(&s) {
return v;
}
if s.contains(['.', 'e', 'E'])
&& let Some(n) = parse_yaml_float(&s)
{
return Float(n);
}
Value::String(s)
}
pub(crate) fn has_ctrl_chars(s: &str) -> bool {
s.bytes()
.any(|b| matches!(b, 0..=0x08 | 0x0b..=0x1f | 0x7f))
}
pub(crate) fn has_newline(s: &str) -> bool {
s.bytes().any(|b| b == b'\n')
}
pub(crate) fn has_yaml_special(s: &str) -> bool {
s.bytes().any(|b| matches!(b, b':' | b'#'))
}
pub(crate) fn starts_with_indicator(s: &str) -> bool {
matches!(
s.as_bytes().first(),
Some(
b'-' | b'?'
| b'!'
| b'&'
| b'*'
| b'['
| b']'
| b'{'
| b'}'
| b'|'
| b'>'
| b'\''
| b'"'
| b'%'
| b'@'
| b'`'
| b','
| b'#'
)
)
}
pub(crate) fn has_leading_or_trailing_space(s: &str) -> bool {
matches!(s.as_bytes().first(), Some(b' ' | b'\t'))
|| matches!(s.as_bytes().last(), Some(b' ' | b'\t'))
}
pub(crate) fn needs_quotes(s: &str) -> bool {
s.is_empty()
|| seems_scalar_typed(s)
|| has_ctrl_chars(s)
|| has_newline(s)
|| has_yaml_special(s)
|| has_leading_or_trailing_space(s)
|| starts_with_indicator(s)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn resolves_null() {
assert!(matches!(resolve_scalar("null".into()), Value::Null));
}
#[test]
fn resolves_tilde() {
assert!(matches!(resolve_scalar("~".into()), Value::Null));
}
#[test]
fn resolves_true() {
assert!(matches!(resolve_scalar("true".into()), Value::Bool(true)));
}
#[test]
fn resolves_int() {
assert!(matches!(resolve_scalar("42".into()), Value::UInt(42)));
}
#[test]
fn resolves_negative_int() {
assert!(matches!(resolve_scalar("-42".into()), Value::Int(-42)));
}
#[test]
fn resolves_big_uint() {
assert!(matches!(
resolve_scalar("18446744073709551610".into()),
Value::UInt(_)
));
}
#[test]
fn resolves_float() {
assert!(matches!(resolve_scalar("1.5".into()), Value::Float(_)));
}
#[test]
fn resolves_inf() {
let v = resolve_scalar(".inf".into());
assert!(matches!(v, Value::Float(f) if f == f64::INFINITY));
}
#[test]
fn resolves_neg_inf() {
let v = resolve_scalar("-.inf".into());
assert!(matches!(v, Value::Float(f) if f == f64::NEG_INFINITY));
}
#[test]
fn resolves_nan() {
let v = resolve_scalar(".nan".into());
assert!(matches!(v, Value::Float(f) if f.is_nan()));
}
#[test]
fn resolves_int_text() {
let v = resolve_scalar("42".into());
assert!(!matches!(v, Value::Float(_)));
}
#[test]
fn falls_back_to_string() {
assert!(matches!(resolve_scalar("hello".into()), Value::String(_)));
}
#[test]
fn preserves_borrow_on_string() {
let src = "hello";
let cow: Cow<'_, str> = Cow::Borrowed(src);
let v = resolve_scalar(cow);
if let Value::String(Cow::Borrowed(s)) = v {
assert!(std::ptr::eq(s, src));
} else {
panic!("expected borrowed string");
}
}
#[test]
fn yaml_int_decimal() {
assert!(matches!(parse_yaml_int("42"), Some(Value::UInt(42))));
}
#[test]
fn yaml_int_negative() {
assert!(matches!(parse_yaml_int("-42"), Some(Value::Int(-42))));
}
#[test]
fn yaml_int_plus_prefix() {
assert!(matches!(parse_yaml_int("+42"), Some(Value::UInt(42))));
}
#[test]
fn yaml_int_hex() {
assert!(matches!(parse_yaml_int("0xff"), Some(Value::UInt(255))));
}
#[test]
fn yaml_int_hex_mixed_case() {
assert!(matches!(parse_yaml_int("0xAbCd"), Some(Value::UInt(43981))));
}
#[test]
fn yaml_int_octal() {
assert!(matches!(parse_yaml_int("0o755"), Some(Value::UInt(493))));
}
#[test]
fn yaml_int_hex_no_sign() {
assert!(parse_yaml_int("+0xff").is_none());
assert!(parse_yaml_int("-0xff").is_none());
}
#[test]
fn yaml_int_empty_radix() {
assert!(parse_yaml_int("0x").is_none());
assert!(parse_yaml_int("0o").is_none());
}
#[test]
fn yaml_int_i64_min() {
assert!(matches!(
parse_yaml_int("-9223372036854775808"),
Some(Value::Int(i64::MIN))
));
}
#[test]
fn yaml_int_overflow_falls_back() {
assert!(parse_yaml_int("-99999999999999999999").is_none());
}
#[test]
fn yaml_int_garbage() {
assert!(parse_yaml_int("hello").is_none());
assert!(parse_yaml_int("3.14").is_none());
}
#[test]
fn yaml_float_decimal() {
assert!(matches!(parse_yaml_float("3.1"), Some(f) if (f - 3.1).abs() < 1e-9));
}
#[test]
fn yaml_float_scientific() {
assert!(matches!(parse_yaml_float("1e5"), Some(f) if f == 100_000.0));
}
#[test]
fn yaml_float_inf_variants() {
for s in [".inf", ".Inf", ".INF", "+.inf", "+.Inf", "+.INF"] {
assert_eq!(parse_yaml_float(s), Some(f64::INFINITY), "{s}");
}
}
#[test]
fn yaml_float_neg_inf_variants() {
for s in ["-.inf", "-.Inf", "-.INF"] {
assert_eq!(parse_yaml_float(s), Some(f64::NEG_INFINITY), "{s}");
}
}
#[test]
fn yaml_float_nan_variants() {
for s in [".nan", ".NaN", ".NAN"] {
assert!(matches!(parse_yaml_float(s), Some(f) if f.is_nan()), "{s}");
}
}
#[test]
fn yaml_float_accepts_int_text() {
assert!(matches!(parse_yaml_float("42"), Some(f) if f == 42.0));
}
#[test]
fn yaml_float_garbage() {
assert!(parse_yaml_float("hello").is_none());
assert!(parse_yaml_float(".inferno").is_none());
}
#[test]
fn resolves_hex() {
assert!(matches!(resolve_scalar("0xff".into()), Value::UInt(255)));
}
#[test]
fn resolves_octal() {
assert!(matches!(resolve_scalar("0o17".into()), Value::UInt(15)));
}
#[test]
fn resolves_plus_int() {
assert!(matches!(resolve_scalar("+42".into()), Value::UInt(42)));
}
}